A learner searches for “money back policy RF-204.” Dense retrieval may recognize “money back” as related to refunds. Lexical retrieval may preserve the exact policy ID. How can you use both signals without adding incomparable raw scores?
Before you begin: Understand lexical retrieval, embeddings, and ranked search results.
Keep the two retrievers' roles clear
Lexical methods score matches between query terms and document text, often accounting for frequency and document length. Dense methods compare learned representations. Each can miss cases the other finds, and each can return irrelevant results.
Hybrid search combines their evidence. This can be done with calibrated score combinations, rank fusion, or a later reranker. The combination should be evaluated on your task rather than assumed to be an automatic improvement.
Avoid adding arbitrary score scales
A lexical score of 12 and a cosine similarity of 0.8 are not directly comparable measurements. Adding them without calibration can make one retriever dominate just because of its numerical scale.
Reciprocal rank fusion, or RRF, combines ranks instead. For each result list, a document at rank r receives a contribution 1 / (c + r), where ranks start at one and c is a positive smoothing constant. Contributions are summed across lists. The constant controls how sharply top ranks differ; it is not a probability parameter.
Work through two lists
Suppose lexical search returns A, B, C and dense search returns B, D, A. With c = 10, A receives 1/11 + 1/13, while B receives 1/12 + 1/11. B ranks ahead because both retrievers place it near the top.
This is an invented example. It shows agreement between rank lists, not proof that B answers the question. If both retrievers share a blind spot, fusion can reinforce it.
Run the fusion
This complete Python 3 program deduplicates each input list so a repeated ID within one retriever does not earn extra votes. It uses stable ID ordering to break ties.
# Runnable: Python 3, standard library.
def rrf(rankings, constant=10):
if constant <= 0:
raise ValueError('Use a positive smoothing constant')
scores = {}
for ranking in rankings:
unique = list(dict.fromkeys(ranking))
for rank, doc_id in enumerate(unique, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1/(constant + rank)
return sorted(scores, key=lambda doc_id: (-scores[doc_id], doc_id))
assert rrf([['A', 'B', 'C'], ['B', 'D', 'A']])[0] == 'B'
assert rrf([['A', 'A'], ['B']]) == ['A', 'B']
print(rrf([['A', 'B', 'C'], ['B', 'D', 'A']]))
Apply permissions consistently
Both retrievers must search the authorized collection or apply equivalent trusted filtering. Fusion should not reintroduce a record excluded by one path for access reasons. Keep source IDs consistent across indexes so the same passage is recognized as the same result.
Updates and deletions must reach both paths. A current dense index combined with a stale lexical index can surface superseded evidence. Test version handling in the combined result, not only in each retriever separately.
Evaluate the added complexity
Compare lexical-only, dense-only, and fused retrieval on exact IDs, paraphrases, rare terms, ambiguous wording, and missing-answer questions. Measure task relevance and latency. If fusion improves paraphrases but pushes exact policy matches down, investigate result depths, weights, or a dedicated exact-ID rule.
A reranker can inspect the fused candidate set, but it cannot recover a document absent from every candidate list. Retrieval depth and reranking cost should be chosen together.
Predict a failure
An irrelevant welcome page ranks first in both lists because every notice repeats its generic event wording. RRF ranks it first too. Is the fusion implementation necessarily broken?
Inspect the candidate evidence
Not necessarily. RRF is rewarding agreement, which is what it is designed to do. Improve the retrievers, source preprocessing, or task-specific reranking and evaluate again. Rank fusion does not understand relevance independently of its input rankings.
Next, you will build a search project that displays evidence directly and measures ranking before adding generated answers.
Reference
Elasticsearch's RRF reference documents the rank-fusion approach and its parameters. The local code above is a small explanatory implementation.