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

138 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 헬퍼 포함. |
## 구조
```mermaid
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
```mermaid
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) -> Self``config.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**.
**왜**: `HybridRetriever``Arc<dyn Retriever>` 두 개 받으니 lexical/vector 가 자기들도 trait object 가능. test 가 `CannedRetriever` mock 으로 두 측 inject 가능 — RRF 만 검증할 때 SQLite/Lance 부재해도 됨.
- **`HybridRetriever``Arc<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 0~1 BM25 normalized, Vector cosine 0~1, 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
- frozen 설계 §3.7 (SearchHit), §6.4 (`search.hybrid_fusion`/`rrf_k`/`default_k`/`snippet_chars`), §0 Q3 (citation), §1.6 (`--explain`), §7.2 (`Retriever` trait): [`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`](../../superpowers/specs/2026-04-27-kebab-final-form-design.md)
- task spec:
- lexical: [`tasks/p2/p2-2-search-lexical.md`](../../../tasks/p2/p2-2-search-lexical.md)
- vector + hybrid: [`tasks/p3/p3-4-hybrid-fusion.md`](../../../tasks/p3/p3-4-hybrid-fusion.md)
- HOTFIXES (RRF `fusion_score [0,1]` 정규화): [`tasks/HOTFIXES.md`](../../../tasks/HOTFIXES.md)