A retrieval-augmented generation application may receive one ranked list from keyword search and another from vector search. Adding their raw scores can be misleading because the scores may use unrelated scales. Reciprocal rank fusion, or RRF, combines document positions instead. The Elastic reference documents the formula: each appearance contributes 1 divided by the sum of a constant and its one-based rank.
This tutorial implements that arithmetic independently. It does not require Elasticsearch, a vector database, an embedding service, or a language model. The two input lists are retrieval fixtures, so the checks establish ranking behavior rather than search quality.
Prerequisites
Use Python 3.12 or a newer supported Python 3 release. Save the following complete program as rrf.py. Document identifiers must be strings, and each input ranking must contain a document at most once.
from fractions import Fraction
def fuse(rankings, k=60):
if type(k) is not int or k < 1:
raise ValueError('k must be a positive integer')
scores = {}
for ranking in rankings:
if len(ranking) != len(set(ranking)):
raise ValueError('duplicate document in one ranking')
for rank, doc in enumerate(ranking, 1):
scores[doc] = scores.get(doc, Fraction()) + Fraction(1, k + rank)
return sorted(scores, key=lambda doc: (-scores[doc], doc))
assert fuse([['a', 'b', 'c'], ['c', 'b', 'd']]) == ['c', 'b', 'a', 'd']
assert fuse([['z'], ['a']]) == ['a', 'z']
assert fuse([]) == []
try:
fuse([['a', 'a']])
except ValueError:
pass
else:
raise AssertionError('duplicate accepted')
print('ranking:', ', '.join(fuse([['a', 'b', 'c'], ['c', 'b', 'd']])))
print('RRF fixture checks passed')
Run and expected output
python3 rrf.py
ranking: c, b, a, d
RRF fixture checks passed
Understand the result
Document c appears third in the first list and first in the second. Document b appears second in both. With this fixture and k equal to 60, c narrowly outranks b. Documents a and d each appear only once; a has the stronger position. A document absent from a list receives no contribution from that list.
The implementation uses Python Fraction arithmetic to make comparisons exact for this small teaching example. Sorting by the negative score gives descending relevance. The document identifier supplies a deterministic tie breaker, demonstrated by the separate a/z assertion. This prevents input arrival order from deciding equal-score results.
Edge cases and integration
Rejecting duplicate identifiers within one list prevents a retriever from accidentally voting twice for the same document. An empty collection of rankings produces an empty result. Use stable identifiers shared by every retriever: two identifiers for the same underlying document otherwise receive separate scores.
In a real RAG pipeline, run fusion after retrieval and before selecting context passages. Keep the retrieval windows explicit and comparable. A document excluded from every window cannot be recovered by this function. Also decide whether fusion operates on whole documents or individual chunks; silently mixing those levels can crowd the final context with passages from one source.
Troubleshooting
If a duplicate exception occurs, inspect the producing retriever and deduplicate before assigning ranks. If ranking quality disappoints, evaluate against labeled queries rather than treating the fixture order as evidence of relevance. This example has not measured answer accuracy, production latency, or an optimal value of k.
Continue with the AI tutorials and Python tutorials.