qmd_py.search.hybrid¶
Hybrid query orchestration: query expansion, Reciprocal Rank Fusion, and
chunk-level reranking — the engine behind marq query. See
Search & query for the user-facing
side.
hybrid ¶
Hybrid query: query expansion + FTS/vector search per expanded sub-query + reciprocal rank fusion + chunk-level reranking - port of the TS reference's hybridQuery/reciprocalRankFusion/expandQuery/rerank (src/store.ts).
ExpandedQuery
dataclass
¶
One typed sub-query, from expansion or spelled out by the caller.
Attributes:
-
type(str) –"lex"(BM25 keywords),"vec"(semantic) or"hyde"(a hypothetical answer passage, embedded and searched as though it were a document). -
query(str) –The text to search with.
RankedResult
dataclass
¶
A result reduced to what fusion needs.
Deliberately narrower than SearchResult: RRF only needs identity
and position, so lexical and vector hits collapse to one shape before
being fused. file is the identity key that matches them up.
Attributes:
-
file(str) –Virtual URI - the key results are deduplicated on.
-
display_path(str) –<collection>/<path>. -
title(str) –Extracted heading or filename stem.
-
body(str) –Full document text.
-
score(float) –Fused RRF score, not the original engine's score.
RrfExplain
dataclass
¶
The fusion half of a --explain trace.
Attributes:
-
rank(int) –Position after fusion, 1-based.
-
weight(float) –How much the fused position counted toward the blend. 1.0 means the rerank pass did not run - either
--no-rerank, or the router failed and the query degraded to RRF ordering. -
score(float) –1 / rank, the positional component of the blend.
HybridQueryExplain
dataclass
¶
Deliberately simpler than the TS reference's HybridQueryExplain/ RRFContributionTrace (no per-list contribution breakdown) - the numbers that matter for understanding why a result ranked where it did (RRF rank/weight, rerank score, blended score) are all here.
HybridQueryResult
dataclass
¶
HybridQueryResult(
file: str,
display_path: str,
title: str,
body: str,
best_chunk: str,
best_chunk_pos: int,
score: float,
context: str | None,
docid: str,
explain: HybridQueryExplain | None = None,
)
One result from the full hybrid pipeline.
Attributes:
-
file(str) –Virtual URI,
marq://<collection>/<path>. -
display_path(str) –<collection>/<path>. -
title(str) –Extracted heading or filename stem.
-
body(str) –Full document text.
-
best_chunk(str) –The chunk that scored best against the query - what the reranker actually judged, not the whole body.
-
best_chunk_pos(int) –Its character offset, used to anchor snippets.
-
score(float) –Final ranking score: blended RRF position and rerank relevance, or plain
1 / rankwhen reranking was skipped. -
context(str | None) –Hierarchical context for this path, or None.
-
docid(str) –Six-char hash prefix.
-
explain(HybridQueryExplain | None) –Score trace when
QueryOptions.explainwas set.
ModelConfig
dataclass
¶
The three router model slugs the hybrid pipeline needs. They always travel together and always come from the same settings object, so every call site used to thread the same three arguments through by hand.
from_settings
classmethod
¶
Build from application settings - the usual construction path.
Source code in src/qmd_py/search/hybrid.py
QueryOptions
dataclass
¶
QueryOptions(
limit: int = 10,
min_score: float = 0.0,
candidate_limit: int = RERANK_CANDIDATE_LIMIT,
collection_name: str | None = None,
intent: str | None = None,
skip_rerank: bool = False,
explain: bool = False,
preexpanded: list[ExpandedQuery] | None = None,
)
Tunables for one hybrid query. Defaults match the previous
per-parameter defaults exactly, so QueryOptions() is the old
all-defaults call.
Bundled so adding a knob doesn't change hybrid_query's signature and
every caller in turn - the CLI query command, the MCP query tool,
and the benchmark runner all build one of these.
preexpanded
class-attribute
instance-attribute
¶
Typed sub-queries the caller spelled out (the lex:/vec:/hyde:
document syntax, or the MCP tool's searches). When set, the BM25
strong-signal probe and expand_query() are skipped entirely, and -
since there's no single canonical "original" query left once the
caller has enumerated the sub-queries - no list gets the 2x
"original" RRF weight; every list is explicit and weighted 1x.
_rerank_safe_text
async
¶
Truncates text to fit the router's per-pair rerank token budget,
measuring with the model's own tokenizer rather than guessing another
fixed chars/token ratio - that guess is exactly what broke the first
time (see module-level comment above).
Source code in src/qmd_py/search/hybrid.py
parse_structured_query ¶
Multi-line lex:/vec:/hyde: (+ optional intent:) query
syntax - port of the TS reference's parseStructuredQuery, minus its
strict error-throwing for malformed input: an unprefixed line among
several, or no typed lines at all, just falls through to None
(treated as an ordinary single query, auto-expanded) rather than
raising - a softer degrade that seemed more appropriate for a CLI
argument than a hard parse error.
Source code in src/qmd_py/search/hybrid.py
validate_typed_queries ¶
First problem found among explicitly typed sub-queries, or None.
Deliberately applied only to sub-queries the caller spelled out -
the lex:/vec:/hyde: document syntax and the MCP query tool's
searches - never to expand_query()'s LLM-generated variants: a
stray -term in those is the model's doing, not a user mistake worth
failing the search over.
Source code in src/qmd_py/search/hybrid.py
expand_query
async
¶
expand_query(
llm_client: LlmClient,
query: str,
model: str,
intent: str | None = None,
) -> list[ExpandedQuery]
Typed query variants (lex/vec/hyde) for RRF fusion - port of the TS
reference's expandQuery().
Uses JSON-schema-constrained chat completion rather than the TS
reference's local node-llama-cpp GBNF-grammar-constrained generation.
llama.cpp's server does accept a raw grammar field over HTTP, but
that specific line-grammar (type ": " content "\n") proved
unreliable in manual testing against this project's own
qwen2.5-3b-instruct-q4_k_m preset: the model satisfied the grammar's
syntax while ignoring its semantics (emitting dozens of "lex: " lines,
never "vec"/"hyde"). JSON-schema output was reliable in the same
testing and is the more portable mechanism for a pure-HTTP client
generally, so that's what qmd-py builds on instead.
Source code in src/qmd_py/search/hybrid.py
reciprocal_rank_fusion ¶
reciprocal_rank_fusion(
result_lists: list[list[RankedResult]],
weights: list[float] | None = None,
k: int = 60,
) -> list[RankedResult]
Fuse several ranked lists into one by reciprocal rank.
Each list contributes weight / (k + rank + 1) per document, summed
across lists, so appearing in several lists beats ranking highly in
one. Scores from the underlying engines are ignored entirely - only
positions matter, which is what makes lexical and vector results
comparable at all.
Parameters:
-
result_lists(list[list[RankedResult]]) –Ranked lists, best first. Empty lists are harmless.
-
weights(list[float] | None, default:None) –Per-list multipliers, positional. Lists beyond the end default to 1.0, so a short list is not an error.
-
k(int, default:60) –Damping constant. Larger flattens the difference between ranks; the default of 60 is the value from the original RRF paper.
Returns:
-
list[RankedResult]–One entry per distinct
file, ordered by descending fused score. -
list[RankedResult]–Documents ranked first in any list get a small bonus, and ones in
-
list[RankedResult]–the top three a smaller one, to break ties toward strong single
-
list[RankedResult]–signals.
Source code in src/qmd_py/search/hybrid.py
_hybrid_rrf_weights ¶
Original-query retrieval paths (the primary evidence) get 2x weight; expansion-derived (lex/vec/hyde) lists stay at 1x regardless of insertion order.
Source code in src/qmd_py/search/hybrid.py
hybrid_query
async
¶
hybrid_query(
session: AsyncSession,
user: CurrentUser,
query: str,
llm_client: LlmClient,
models: ModelConfig,
options: QueryOptions | None = None,
) -> list[HybridQueryResult]
BM25 + vector + query expansion + RRF + chunked reranking - port of
the TS reference's hybridQuery. See QueryOptions for the tunables.