ponytail-audit 의 작은 over-engineering 잔여 3건 (output/동작 불변):
1. FusionPolicy 1-arm enum 제거 — kebab-search 내부 전용(외부 importer 0)이던
`enum FusionPolicy { Rrf { k_rrf } }` 를 `HybridRetriever.k_rrf: u32` 로 inline.
parse_fusion 은 u32 반환. RRF 수식 `Σ 1/(k_rrf+rank)` byte-identical.
2. dual YAML 통일 — serde_yaml(0.9) 를 maintained fork serde_yaml_ng(0.10) 로 일원화.
k8s manifest chunker + eval golden loader 2 site 마이그레이션(API drop-in),
workspace dep 에서 serde_yaml 제거. parse 결과 불변.
3. NliVerifier::hypothesis_token_count 의 `Ok(0)` default 제거 → required.
prod OnnxNliVerifier 가 이미 override 라 동작 불변, MockNliVerifier 에 explicit 추가.
문서 동기화: search/eval/parse component README + ARCHITECTURE 의 FusionPolicy/
serde_yaml 참조를 현 상태로 갱신. net −37줄. 적대적 검증 2렌즈(output-identity +
completeness-build) 통과, 영향 crate 테스트 + clippy -D warnings 클린.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
Search
검색 백엔드 — lexical (FTS5 BM25) + vector (LanceDB ANN) + hybrid (RRF fusion). 같은
Retrievertrait,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, k_rrf, default_k)
-lexical: Arc~dyn Retriever~
-vector: Arc~dyn Retriever~
-k_rrf: u32
-default_k: usize
}
class SearchMode {
<<enum>>
Lexical
Vector
Hybrid
}
Retriever <|.. LexicalRetriever
Retriever <|.. VectorRetriever
Retriever <|.. HybridRetriever
HybridRetriever --> LexicalRetriever
HybridRetriever --> VectorRetriever
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,highlightSQL 함수 호출. FTS5MATCH쿼리.- 구현:
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.k_rrf: u32— 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(Embeddertrait 포함) +kebab-config+kebab-store-sqlite+kebab-store-vector.kebab-embed-local은 caller 가 inject (forbidden direct dep). - 외부 lib:
rusqlite(FTS5 쿼리),globset(filter 매칭),serde_json,tracing. - 외부 서비스: 없음.
핵심 결정
-
세 retriever 모두 같은
Retrievertrait. 왜:HybridRetriever가Arc<dyn Retriever>두 개 받으니 lexical/vector 가 자기들도 trait object 가능. test 가CannedRetrievermock 으로 두 측 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 01 BM25 normalized, Vector cosine 01, Hybrid 0~?) 비교 가능하려면[0,1]정규화 필요. raw 를2/(k_rrf+1)로 나눔. (HOTFIXES "RRF fusion_score[0,1]정규화" 항목.) -
두 측
index_versionmismatch = warn (not error). 왜: lexical 이 v2, vector 가 v1 (re-embed 안 했음) 같은 stale state 가 운영 시 일어남. 즉시 fail = ingest 끝나기 전 search 막힘. warning 만 띄우고 계속 동작 = 사용자가 인지하고 re-index 결정. -
Embeddertrait (kebab-core) 만 의존,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), §7.2 (Retrievertrait):docs/superpowers/specs/2026-04-27-kebab-final-form-design.md - task spec:
- lexical:
tasks/p2/p2-2-search-lexical.md - vector + hybrid:
tasks/p3/p3-4-hybrid-fusion.md
- lexical:
- HOTFIXES (RRF
fusion_score [0,1]정규화):tasks/HOTFIXES.md