Files
kebab/docs/components/search/README.md
th-kim0823 af8c162e09 docs(components): per-group contributor reference (12 그룹)
docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹
페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요
type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec
의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring
다이어그램 + 진입 가이드 보유.

ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로
교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입
링크 추가.

이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md
의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md,
per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:05:32 +09:00

7.3 KiB
Raw Blame History

Search

검색 백엔드 — lexical (FTS5 BM25) + vector (LanceDB ANN) + hybrid (RRF fusion). 같은 Retriever trait, SearchMode 로 dispatch.

구성 crate

Crate 역할
kebab-search LexicalRetriever (P2-2) + VectorRetriever (P3-4) + HybridRetriever (P3-4). 모두 kebab-core::Retriever 구현. citation hydration 헬퍼 포함.

구조

classDiagram
    class Retriever {
        <<trait kebab-core>>
        search(query) Vec~SearchHit~
        index_version() IndexVersion
    }
    class LexicalRetriever {
        +new(store, index_version) Self
        +with_settings(store, snippet_chars, ...)
        -store: Arc~SqliteStore~
    }
    class VectorRetriever {
        +new(store, vector_store, embedder, ...) Self
        -store: Arc~SqliteStore~
        -vector_store: Arc~dyn VectorStore~
        -embedder: Arc~dyn Embedder~
    }
    class HybridRetriever {
        +new(cfg, lexical, vector) Self
        +with_policy(lex, vec, FusionPolicy, k)
        -lexical: Arc~dyn Retriever~
        -vector: Arc~dyn Retriever~
        -fusion: FusionPolicy
        -default_k: usize
    }
    class FusionPolicy {
        <<enum>>
        Rrf{k_rrf}
    }
    class SearchMode {
        <<enum>>
        Lexical
        Vector
        Hybrid
    }
    Retriever <|.. LexicalRetriever
    Retriever <|.. VectorRetriever
    Retriever <|.. HybridRetriever
    HybridRetriever --> LexicalRetriever
    HybridRetriever --> VectorRetriever
    HybridRetriever ..> FusionPolicy
    HybridRetriever ..> SearchMode : dispatch

Data flow

flowchart LR
    Q["SearchQuery<br/>{text, mode, k, filters}"]
    Disp["mode dispatch"]
    Lex["LexicalRetriever<br/>SQLite FTS5 + bm25<br/>+ snippet/highlight"]
    Vec["VectorRetriever<br/>Embedder.embed(query)<br/>→ VectorStore.search<br/>→ SQLite hydrate"]
    Fan["k × HYBRID_FANOUT_MULTIPLIER (2)<br/>각 측 fanout"]
    RRF["RRF fusion<br/>score = Σ 1 / (k_rrf + rank_m)<br/>fusion_score / (2 / (k_rrf+1))<br/>→ [0,1]"]
    Merge["chunk 양측 등장 시<br/>lexical snippet 우선<br/>(FTS5 highlight 가 사용자 친화)"]
    Hits["Vec~SearchHit~<br/>(snippet, citation,<br/>heading_path, retrieval)"]
    Q --> Disp
    Disp -->|Lexical| Lex --> Hits
    Disp -->|Vector| Vec --> Hits
    Disp -->|Hybrid| Fan --> Lex
    Disp -->|Hybrid| Fan --> Vec
    Lex -.-> RRF
    Vec -.-> RRF
    RRF --> Merge --> Hits

주요 type / trait / 함수

Trait (kebab-core):

  • Retriever::search(&SearchQuery) -> Result<Vec<SearchHit>>.
  • Retriever::index_version() -> IndexVersion — hybrid 가 두 측 version 다르면 stale-index 경고.

LexicalRetriever (kebab-search::lexical):

  • LexicalRetriever::new(store: Arc<SqliteStore>, index_version: IndexVersion) -> Self.
  • LexicalRetriever::with_settings(store, snippet_chars, ...)snippet, highlight SQL 함수 호출. FTS5 MATCH 쿼리.
  • 구현: SELECT chunk_id, bm25(...), snippet(...), highlight(...) FROM chunks_fts WHERE chunks_fts MATCH ? .... citation_helper 가 Citation::Line { L_start, L_end } / Page { p } 등 분기.

VectorRetriever (kebab-search::vector):

  • VectorRetriever::new(store: Arc<SqliteStore>, vector_store: Arc<dyn VectorStore>, embedder: Arc<dyn Embedder>, ...).
  • 구현: query text → embedder.embed(EmbeddingKind::Query, ...)vector_store.search(vec, k, filters) → SQLite 로 hydrate (snippet, citation 등은 vector hit 의 chunk_id 로 SELECT).
  • Arc<dyn Embedder> runtime injection — concrete adapter (kebab-embed-local::FastembedEmbedder) 는 caller 가 wire.

HybridRetriever (kebab-search::hybrid):

  • HybridRetriever::new(&Config, Arc<dyn Retriever> lex, Arc<dyn Retriever> vec) -> Selfconfig.search.hybrid_fusion ("rrf") + config.search.rrf_k 읽음. 두 retriever 의 index_version 가 다르면 tracing::warn.
  • FusionPolicy::Rrf { k_rrf } — default 60. with_policy 헬퍼로 explicit 지정 가능.
  • 상수: DEFAULT_K = 10 (query.k == 0 fallback), DEFAULT_K_RRF = 60, HYBRID_FANOUT_MULTIPLIER = 2.
  • merge rule: 양측 등장 chunk 의 snippet / citation / heading_path 는 lexical 측에서 가져옴 (FTS5 highlight 가 vector 의 truncated text 보다 user-relevant).

외부 의존

  • crate dep: kebab-core + kebab-config + kebab-store-sqlite + kebab-store-vector + kebab-embed (trait re-export). kebab-embed-local 은 caller 가 inject (forbidden direct dep).
  • 외부 lib: rusqlite (FTS5 쿼리), globset (filter 매칭), serde_json, tracing.
  • 외부 서비스: 없음.

핵심 결정

  • 세 retriever 모두 같은 Retriever trait. : HybridRetrieverArc<dyn Retriever> 두 개 받으니 lexical/vector 가 자기들도 trait object 가능. test 가 CannedRetriever mock 으로 두 측 inject 가능 — RRF 만 검증할 때 SQLite/Lance 부재해도 됨.

  • HybridRetrieverArc<dyn Embedder> 직접 안 받음. : vector retriever 가 이미 embedder 보유. hybrid 는 mode == Hybrid 시 양측 fanout, 직접 embedding 안 함 → vector 측이 자기 embedder 사용. concrete adapter (kebab-embed-local) 가 hybrid 에 노출 안 됨 → forbidden dep 깨끗.

  • RRF fanout = k * 2. : spec literal 의 floor. lexical 과 vector 의 disjoint set (한쪽만 surface 한 chunk) 이 충분히 넓어야 fused top-k 가 의미 있음. 비용 linear, recall 회복 큼.

  • 양측 등장 chunk = lexical snippet 우선. : vector 의 raw chunk text 는 보통 truncated (snippet_chars 짧음, BM25 highlight 없음). FTS5 의 snippet() + highlight() 가 user-perceived relevance 강함. citation/heading_path 도 lexical 결과가 정확 (vector 측은 SQLite hydrate 값 같지만 lexical 일관성).

  • fusion_score [0, 1] 정규화 (post-merge hotfix). : raw RRF score 는 Σ 1/(k_rrf + rank) 라 max 가 2/(k_rrf+1) (양측 모두 rank=1). mode 간 (Lexical 01 BM25 normalized, Vector cosine 01, Hybrid 0~?) 비교 가능하려면 [0,1] 정규화 필요. raw 를 2/(k_rrf+1) 로 나눔. (HOTFIXES "RRF fusion_score [0,1] 정규화" 항목.)

  • 두 측 index_version mismatch = warn (not error). : lexical 이 v2, vector 가 v1 (re-embed 안 했음) 같은 stale state 가 운영 시 일어남. 즉시 fail = ingest 끝나기 전 search 막힘. warning 만 띄우고 계속 동작 = 사용자가 인지하고 re-index 결정.

  • kebab-embed (trait crate) 만 의존, kebab-embed-local (concrete) 금지. : future MVP 의 swap 가능성 (candle, ollama-embed 등). kebab-search 가 concrete 어댑터 import 하면 kebab-embed-local 의 fastembed dep (큰 ONNX runtime) 이 search 에 강제 → unrelated build 비용. caller 가 runtime inject.

관련 spec / HOTFIXES