What is the RRF (Reciprocal Rank Fusion) output ranking given these two retrievers' results?
You're combining two retrievers via Reciprocal Rank Fusion with k=60 (the standard). Dense retriever returns (in rank order): rank 1: chunk_A rank 2: chunk_B rank 3: chunk_C rank 4: chunk_D BM25 retriever returns (in rank order): rank 1: chunk_E rank 2: chunk_A rank 3: chunk_F rank 4: chunk_B RRF formula per chunk: score(c) = sum over retrievers of 1 / (k + rank_in_that_retriever). Unseen chunks in a retriever contribute 0 from that retriever. Question: what's the final RRF ranked top-4?
Sum 1/(60+rank) per retriever for each chunk. chunk_A wins on consensus (in both lists) over chunk_E, which tops only BM25. Final top-4: A, B, E, C.
Imagine two judges ranking the same contestants. Reciprocal Rank Fusion gives each contestant points based on where each judge placed them. Rank 1 is worth a bit more than rank 2, and so on. But the gaps are tiny, because a big buffer (60) is added to every rank. Then you add up a contestant's points from both judges. A contestant who placed second with both judges beats one who placed first with only one judge and was ignored by the other. Two agreeing judges outweigh one enthusiastic judge. That is the main idea. It rewards contestants both lists agree on, and it never has to compare the judges' raw scores, which might be on totally different scales.
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.
Reciprocal Rank Fusion is the standard method for merging the outputs of two or more retrievers into a single ranked list. In modern RAG, the two retrievers are almost always a dense embedding search and a lexical BM25 search. RRF combines them into one hybrid result. It ships as a built-in feature in Elasticsearch, OpenSearch, Weaviate, Qdrant, and LangChain. So it is worth being able to compute it by hand and explain why it works the way it does.
A naive reading says the chunk that tops a retriever should win the fused list. Yet BM25's top result lands third here, and a chunk that topped neither list ranks above it. Working through the arithmetic shows why.
The formula and why it only uses rank
RRF assigns each document a score that depends only on its position in each retriever's ranked output, never on the underlying similarity score. For a document d, the fused score is the sum over every retriever R of one divided by k plus that document's rank in R.
The constant k (canonically 60) is added to every rank before inverting. Its purpose is to compress the gap between adjacent ranks. With k=60, the difference between rank 1 (1/61) and rank 2 (1/62) is tiny. So no single retriever's top hit can dominate the fused ranking. A document missing from a retriever contributes nothing from that retriever, which is the same as treating its rank there as infinite.
The main reason RRF ignores raw scores is calibration. A dense retriever's cosine similarities and a BM25 retriever's term-frequency scores live on completely different, query-dependent scales. Cosine similarity is bounded and tends to cluster in a narrow band. BM25 is unbounded and depends on document length and corpus statistics that shift query to query. Linearly combining them needs per-corpus normalization that drifts as the index changes and has to be re-tuned every time the corpus grows. Collapsing everything to integer rank avoids that problem entirely. That is why RRF is the method teams reach for when they cannot afford a maintained calibration pipeline.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
from collections import defaultdict
def rrf(rankings, k=60):
# rankings: list of retriever outputs, each an ordered list of chunk ids
scores = defaultdict(float)
for ranked_list in rankings:
for rank, doc in enumerate(ranked_list, start=1): # ranks are 1-indexed
scores[doc] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
dense = ["chunk_A", "chunk_B", "chunk_C", "chunk_D"]
bm25 = ["chunk_E", "chunk_A", "chunk_F", "chunk_B"]
for doc, score in rrf([dense, bm25])[:4]:
print(f"{doc}: {score:.5f}")
# chunk_A: 0.03252 chunk_B: 0.03175 chunk_E: 0.01639 chunk_C: 0.01587 (C/F tie broken by sort)Real products, models, and research that use this idea.
- Elasticsearch and OpenSearch both ship a native RRF retriever to fuse BM25 with dense kNN results in hybrid search.
- Weaviate's hybrid search supports a ranked fusion mode that combines keyword (BM25F) and vector results.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow does the choice of k change which chunks win, and what happens as k approaches 0 or infinity?
Small k sharpens rank differences so a single rank-1 hit dominates. Large k flattens them so consensus wins. As k grows the scores converge, and the ordering tends toward the count of lists each chunk appears in.
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.
Assuming the chunk that ranks #1 in one retriever wins overall. RRF rewards consensus: a chunk ranked highly in both lists beats a chunk that tops only one.
60 second bullets to scan on the way to the call.
The RRF score formula and what the k constant does
Why RRF uses ranks instead of raw similarity scores
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.