Hybrid search¶
AsyncHybridSearcher(text_store, finder).search(query, top_k, mode=..., filters=...)
runs one or both retrieval legs:
mode |
Runs | Without a finder |
|---|---|---|
"lexical" |
BM25 over the text index | unaffected |
"semantic" |
the vector memory, scoring each article by its best chunk | raises SearchQueryError |
"hybrid" (default) |
both at once, then fuses the two rankings | runs the lexical leg only and reports skipped=("semantic",) |
- What the embedder sees. The semantic leg embeds the query's words, not
its syntax. Operators, field scopes, negated terms, and prefix terms are
dropped, so
quasar -dwarfis embedded asquasar, andquasar OR blazarthe same asquasar blazar. A hybrid query made only of prefix terms skips the semantic leg; in semantic mode it raisesSearchQueryError. - Degraded and skipped legs.
SearchOutcome.degradednames legs that were attempted and failed. In hybrid mode, anEmbeddingErroris logged throughlogger, and the lexical results are returned.SearchOutcome.skippednames legs that had nothing to run. Tell the user about both. A lexical failure is always raised, because it means the local index is broken. - Fusion. Reciprocal rank fusion reads only the order of each list, so
BM25's corpus-dependent scale never skews the blend. Pass
strategy=normalized_score_fusionwhen score gaps should count, andfusion=FusionParams(weights=(1.0, 2.0))to weigh the lexical and semantic lists, in that order. - Hits. A
FusedHitcarrieslexical_rankandsemantic_rank(Nonewhere that leg did not return it),title,metadata,snippet,highlights, andsnippets. Show ranks, never the fused score as a percentage. - Snippets. A record the lexical leg returned keeps its lexical snippets,
one per matching field. A record found only by the semantic leg gets a
snippet of the chunk that ranked it, with the query's words highlighted
where they occur in it, as a single
Snippetwhose field is"body". That passage matched by meaning, so it may highlight nothing; checklexical_rank is Noneto label it, for example "related passage", or read the abstract withtext_store.get_documentsinstead. - Candidate pool. Each leg fetches
HybridParams.candidate_poolrecords (default 100, and never fewer thantop_k) before fusion, so a record ranked 40th lexically and 3rd semantically can still reach the top 20. The semantic leg asks the vector memory forcandidate_pool × chunk_pool_factorchunks (default factor 5). Raise the factor when long articles fill the top chunks and the pool comes back short.