┌─cat blog/rrf-reciprocal-rank-fusion.md┐
2026-05-22 · 1 min read
RRF is a simple, elegant way to combine multiple ranked result lists into a single ranking — without caring about the actual scores.
The Problem with Raw Scores
Hybrid search typically involves two retrievers running in parallel:
- Vector search returns a cosine similarity score between
0.0and1.0— a well-defined, bounded value. - Lexical search (e.g. BM25) returns an arbitrary relevance score — the ordering is meaningful, but the score itself has no meaning outside of that specific result set. A score of
42.7from BM25 tells you nothing on its own.
Since the two score ranges are completely incompatible, you can't just add them together or average them. You need a score-agnostic merging strategy.
The Idea
RRF throws away the raw scores entirely and works only with rank positions. For each document, its RRF score is the sum of a reciprocal rank contribution from every retriever that returned it:
RRF(d) = Σ 1 / (k + rank(d))
Where:
rank(d)is the position of documentdin a given retriever's result list (1-indexed)kis a smoothing constant (typically60) that prevents top-ranked results from dominating too heavily- The sum is taken over all retrievers that returned
d
A document that ranks #1 in both vector and lexical search gets a much higher combined score than one that ranks #1 in only one. Documents that don't appear in a retriever's results simply don't contribute to the sum from that retriever.
RRF Implementation in Nexus
func mergeResults(lex, vec []store.ChunkResult) ([]MergedResult, error) { const k = 60.0 // smoothing constant rrfScores := make(map[string]float64) chunkMap := make(map[string]store.ChunkResult) for rank, item := range lex { rrfScores[item.Id] += 1.0 / (float64(rank+1) + k) chunkMap[item.Id] = item } for rank, item := range vec { rrfScores[item.Id] += 1.0 / (float64(rank+1) + k) chunkMap[item.Id] = item } var results []MergedResult for id, score := range rrfScores { results = append(results, MergedResult{ ChunkResult: chunkMap[id], RRFScore: score, }) } sort.Slice(results, func(i, j int) bool { return results[i].RRFScore > results[j].RRFScore }) if len(results) > 10 { results = results[:10] } return results, nil }
Both result lists are iterated independently. Each chunk accumulates score contributions from whichever retrievers found it. The final list is sorted by RRF score descending and capped at the top 10 results.
└────────────────────────────────────────┘