trendingzones
← Back to the Intermediate level

AI & ML — ADVANCED

Hybrid Search: Combining BM25 and Semantic Search with RRF

Every trade-off covered so far points to the same conclusion: don’t pick one method — combine them. Reciprocal Rank Fusion is the specific, well-established technique production systems actually use to do that.

The Quick Answer

Reciprocal Rank Fusion (RRF) merges two or more ranked lists — a BM25 ranking and a semantic search ranking, say — into one combined ranking, using only each document’s positionin each list rather than its raw score. That’s the key design decision: a BM25 score and a cosine similarity score live on entirely different scales, so there’s no principled way to average them directly. Rank position is directly comparable across any two ranking methods, which is exactly why RRF works as a general-purpose fusion technique regardless of what produced each input list.

The RRF Formula, in Code

For every document, RRF sums a contribution from each ranked list it appears in: 1 divided by a constant k (60 in the original paper) plus that document’s rank in that particular list.

def reciprocal_rank_fusion(ranked_lists, k=60):
    scores = {}
    for ranked_list in ranked_lists:
        for rank, doc_id in enumerate(ranked_list, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    return sorted(scores.items(), key=lambda item: item[1], reverse=True)

# BM25 ranks the exact error-code document first (precise match).
# Semantic search misses it, ranking it last instead.
bm25_ranking = ["doc_errorcode", "doc_generic1", "doc_generic2"]
semantic_ranking = ["doc_generic1", "doc_generic2", "doc_errorcode"]

fused = reciprocal_rank_fusion([bm25_ranking, semantic_ranking])
for doc_id, score in fused:
    print(f"{doc_id}: {score:.5f}")

# doc_generic1: 0.03252
# doc_errorcode: 0.03227
# doc_generic2: 0.03200

doc_errorcodewas ranked dead last by semantic search alone, and would have been completely absent from a semantic-only top-2 result. In the fusion, BM25’s confident first-place ranking for it still contributes real weight, pulling it up to 2nd place overall — recovering exactly the kind of result the Introduction and Intermediate levels showed semantic search alone can miss.

Try it interactively below:

Source: Cormack, Clarke & Buettcher, “Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods,” SIGIR 2009

Why Production RAG Systems Rely on This

Every failure mode covered across this topic has a direct, practical cost in a real RAG system: a missed exact match means the model never sees the one chunk with the real answer, and the precision and recall of retrieval directly bounds how good the final generated answer can possibly be. Hybrid search with RRF is the standard production answer: run BM25 and semantic search as two independent passes over the same chunked documents, then fuse their rankings — so a query gets the benefit of exact-match precision on identifiers and codes, and semantic generalization on paraphrased, differently-worded questions, without having to guess in advance which kind of query is coming.

Fun Fact

RRF was originally published in 2009 — well before embeddings and vector databases became commonplace — as a general way to combine any set of ranked lists, from any source at all. Its adoption for combining keyword and semantic search is a much more recent application of a technique that predates the problem it’s now best known for solving.

Test Yourself

Why does Reciprocal Rank Fusion use each document’s rank rather than its raw score from BM25 or semantic search?

In the worked example, what happened to a document ranked last by semantic search but first by BM25 once fused?

What is the standard default value of the constant k in the RRF formula, as used in the original paper?