Pick the right way to fuse BM25 and dense scores before context assembly
Reciprocal Rank Fusion works on ranks, not raw scores, so it sidesteps the fact that BM25 and cosine live on incompatible scales, and it is parameter-free in practice.
Imagine two teachers grading your essay on different scales. One gives marks out of 100, the other gives stars from 0 to 5. Adding 87 plus 4 is nonsense, different units. Averaging after a normalization is fragile because one teacher might happen to give very spread-out scores this term. The robust trick: ask each teacher to rank all the essays from best to worst. Then combine the rankings. An essay that came in first for both teachers is clearly best; an essay that came in first for one and tenth for the other is middling. Rank-based fusion does not care about the units. RRF is the standard recipe for this kind of combination.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Hybrid retrieval needs a fusion step. Two retrievers each produced a ranked list of candidates; the pipeline has to combine them into a single ranking before assembly. Three of the four options listed are wrong in instructive ways. The fourth, Reciprocal Rank Fusion, is the production default for reasons worth understanding.
Why raw-score addition fails
BM25 and cosine similarity are computed by different formulas with different bounds.
BM25 for a query against a document is roughly the sum over query terms of idf(term) × tf(term, doc) × length_norm. The raw output depends on how rare the terms are in the corpus and how long the matched document is. A query against a long-document corpus with common terms produces scores around 1-3. A query with rare terms against short documents can produce scores around 20-30.
Cosine similarity between two unit-norm embeddings is bounded in [-1, 1] and in practice clusters in narrow bands like 0.4 to 0.9 for retrieval workloads.
The consequence
Adding raw BM25 to raw cosine means the ranking is dominated by whichever retriever happens to have a larger numeric scale this query. That domination shifts per query as the BM25 score distribution changes with the term rarity profile. The ranking is unstable. A retrieval system with query-dependent dominance is not a production system.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
def rrf_fuse(rank_lists: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
"""Reciprocal Rank Fusion across N ranked lists.
Each list is doc_ids ordered best-first.
Returns (doc_id, score) sorted by score desc.
"""
scores: dict[str, float] = {}
for ranks in rank_lists:
for r, doc_id in enumerate(ranks, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + r)
return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
# Example
dense_top = ["d3", "d1", "d7", "d2", "d5"]
bm25_top = ["d1", "d3", "d9", "d4", "d2"]
fused = rrf_fuse([dense_top, bm25_top])
# d1 and d3 dominate because they ranked high in both listsReal products, models, and research that use this idea.
- Elasticsearch 8.x and OpenSearch 2.x expose `rrf` as the documented fusion operator for hybrid queries.
- Vespa's hybrid retrieval guide uses RRF with k=60 as the default sample configuration.
What an interviewer would ask next. Try answering before peeking at the approach.
QWhen would you reach for a weighted RRF instead of vanilla RRF?
When one retriever is known to be substantially better on the workload; introduce per-retriever weights as w / (k + rank) and tune on a labelled eval.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Adding raw BM25 and cosine scores. BM25 can range 0 to 30+ depending on document length and term rarity; cosine is bounded around 0.4 to 0.9. The sum is meaningless and varies wildly per query.
60 second bullets to scan on the way to the call.
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.