Skip to content

Commit fcfa7f5

Browse files
committed
chore: Add section explaining RRF
1 parent 491e098 commit fcfa7f5

1 file changed

Lines changed: 30 additions & 18 deletions

File tree

blog.md

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ Why does sparse matter when the dense input is already rich? Dense embeddings ex
187187
the method that retries payments" can surface `retryWithBackoff` even if no query word appears in the source — but that
188188
power trades precision for meaning, and rare or project-specific identifiers like `PlaceOrderRequest` get smoothed
189189
toward 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
191192
the original, so it handles both exact identifier lookups and natural-language queries that dense alone would miss.
192193

193194
So 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

202203
In 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

316327
https://qdrant.tech/articles/sparse-vectors/
328+
https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion

0 commit comments

Comments
 (0)