@@ -187,7 +187,8 @@ Why does sparse matter when the dense input is already rich? Dense embeddings ex
187187the method that retries payments" can surface ` retryWithBackoff ` even if no query word appears in the source — but that
188188power trades precision for meaning, and rare or project-specific identifiers like ` PlaceOrderRequest ` get smoothed
189189toward neighboring concepts in the model's vector space. BM25 fills exactly that gap: it matches tokens literally with
190- no compression, and ** semcode's** code-aware tokenization splits ` PlaceOrderRequest ` into ` Place Order Request ` alongside
190+ no compression, and ** semcode's** code-aware tokenization splits ` PlaceOrderRequest ` into ` Place Order Request `
191+ alongside
191192the original, so it handles both exact identifier lookups and natural-language queries that dense alone would miss.
192193
193194So the full picture is:
@@ -200,7 +201,8 @@ are computed in the same pipeline step and stored together as a single point in
200201## Section 4 — What goes into Qdrant: the named-vector schema
201202
202203In Section 3 it's explained that we have two inputs per symbol — dense and sparse — stored together in Qdrant.
203- This section explains * how* they are stored: the shape of a single stored point and why that shape matters at query time.
204+ This section explains * how* they are stored: the shape of a single stored point and why that shape matters at query
205+ time.
204206
205207### Named vectors: two vectors, one point
206208
@@ -248,23 +250,32 @@ diff metadata as dense-only points.
248250
249251---
250252
251- ## Section 5 — Hybrid retrieval at query time (RRF in one Qdrant call)
252-
253- - The query goes through * both* encoders: dense (full model) and sparse (tokenizer + BM25)
254- - One Qdrant ` query_points ` call does the fusion server-side:
255- ```
256- FusionQuery(fusion=Fusion.RRF),
257- prefetch=[
258- Prefetch(query=dense_vec, using="text-dense", limit=K*2),
259- Prefetch(query=sparse_vec, using="text-sparse", limit=K*2),
260- ]
261- ```
262- - Reference: ` server/store/qdrant.py:203-223 `
263- - How RRF works in one paragraph: each retriever returns a ranked list, RRF scores each doc by ` Σ 1/(k + rank_i) ` , ties
264- broken by combined rank. No tuning of weights needed.
265- - Why this beats weighted sum: scale-free, doesn't depend on score calibration between dense cosine and BM25
266- - Reference: ` server/tools/search.py:20-78 `
253+ ## Section 5 — Hybrid retrieval at query time
267254
255+ At query time, the same two-track split like in the ingestion phase runs in reverse. The query string goes through both
256+ encoders — the dense model turns it into a floating-point vector, the BM25 turns it into a sparse vector.
257+ Both are sent to Qdrant in a single call, which runs each retriever independently, ranks the top K×2 candidates
258+ from each, and produces two separate ranked lists.
259+
260+ Qdrant then uses ** Reciprocal Rank Fusion (RRF)** to merge those two ranked lists into one before returning the
261+ final top K results. The merge looks like this step by step, using the query _ "find the method that retries
262+ failed payments"_ as an example:
263+
264+ 1 . Dense retriever returns its ranked list:
265+ ` [retryWithBackoff (rank 1), processPayment (rank 2), PlaceOrderRequest (rank 3), ...] `
266+ 2 . Sparse retriever returns its ranked list:
267+ ` [PlaceOrderRequest (rank 1), retryWithBackoff (rank 2), handleTimeout (rank 3), ...] `
268+ 3 . RRF scores each result with ` 1 / (k + rank) ` from every list it appears in, then sums those contributions
269+ 4 . Everything is re-sorted by that combined score → one final list:
270+ ` [retryWithBackoff, PlaceOrderRequest, processPayment, handleTimeout, ...] `
271+
272+ ` retryWithBackoff ` ranked first in dense and second in sparse — both retrievers agreed, so it floats to the top.
273+ ` PlaceOrderRequest ` ranked first in sparse (exact token match) but third in dense — it still surfaces near the top
274+ because the sparse retriever was confident. ` processPayment ` only appeared in one list despite a good dense rank,
275+ so it scores lower.
276+
277+ RRF rewards consistent rank across retrievers. The score it produces answers a simpler question:
278+ "how consistently did this result appear near the top across both dense and sparse retrievers?"
268279---
269280
270281## Section 6 — Indexing flow: incremental, content-addressed
@@ -314,3 +325,4 @@ diff metadata as dense-only points.
314325## Reference
315326
316327https://qdrant.tech/articles/sparse-vectors/
328+ https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion
0 commit comments