Hybrid Search (RRF)

Hybrid search runs keyword (BM25) and vector retrieval together, then fuses their ranked lists with Reciprocal Rank Fusion (RRF), a score-agnostic method.

Published Updated

On this page

Definition

Hybrid search answers a query with two different retrievers at once and then merges their results. One retriever is lexical: it matches the literal words in the query, and its workhorse is BM25, the scoring function behind Elasticsearch, OpenSearch and most classic search engines. The other is dense or semantic: it turns the query and every document into embeddings and ranks documents by how close their vectors are, which is what vector search does. Each method is strong exactly where the other is blind, so hybrid search runs both and fuses their two ranked lists into one.

The reason this is a named technique, rather than an obvious "use both", is that fusing the lists is harder than it looks. A BM25 score and a cosine similarity are numbers on completely different scales, so you cannot just add them up. The method that solved this cleanly, and that has since become the default almost everywhere, is Reciprocal Rank Fusion (RRF): it throws away the two incompatible scores and combines the documents by their rank position alone. That one decision — fuse ranks, not scores — is what the rest of this page is about.

How It Works

Two retrievers, two blind spots

Lexical and dense retrieval fail in opposite ways, and that is the whole argument for combining them.

BM25 matches tokens. If you search a support corpus for reset 2FA on the X100, BM25 finds the documents containing the exact strings X100 and 2FA — product codes, error codes, part numbers, function names, people's surnames. These are precisely the tokens an embedding model tends to smear together, because a router model number carries almost no semantic content for it to encode. But BM25 has no idea that 2FA, two-factor authentication and multi-factor login are the same thing; a document that never uses the searcher's exact words is invisible to it, no matter how relevant.

Dense retrieval is the mirror image. An embedding model happily matches cancel my subscription to a document titled Ending your membership with no shared keywords, because it ranks by meaning. That is its superpower and also its weakness: it will confidently return semantically-adjacent documents that miss a critical exact term — the wrong product version, a similar-but-different API, a near-synonym that is legally distinct. Rare tokens and identifiers are where dense retrieval is weakest, which is exactly where BM25 is strongest.

Run both and you get two ranked lists that each contain relevant documents the other missed. Now you have to merge them.

The incomparable-score problem

The tempting merge is to add the scores: give each document its BM25 score plus its similarity score and sort by the sum. This is called CombSUM, and it quietly breaks, because the two numbers do not live on the same scale. A BM25 score has no upper bound — it grows with term frequency and corpus statistics — whereas a cosine similarity is confined to a fixed interval (in Azure AI Search's HNSW cosine configuration, for example, the reported range is 0.333 to 1.00). Add an unbounded number to one capped near 1 and the unbounded one wins every time: the vector signal becomes rounding noise on top of BM25.

You can try to fix this by normalizing each list's scores into [0, 1] before adding, but normalization is fragile. It depends on the maximum and minimum score in that particular query's results, so a single outlier document stretches the scale, and a query that happens to return only weak matches gets its mediocre top result promoted to a full 1.0. The scores were never designed to be comparable, and no amount of rescaling makes them so.

Reciprocal Rank Fusion: fuse the ranks instead

RRF's insight is to discard the scores entirely and keep only the one thing the two lists genuinely share: rank position. Rank 1 means "this retriever's best guess" in both lists, whether the underlying score was a BM25 value of 18 or a cosine similarity of 0.83. Ranks are inherently comparable, so fusing them needs no normalization and no tuning.

Each document gets a contribution from every list it appears in, and its final score is the sum:

RRFscore(d) = sum over lists i of  1 / (k + rank_i(d))

rank_i(d) is the document's position in list i, counting from 1, and k is a constant, conventionally 60. The reciprocal 1/(k + rank) is large for top-ranked documents and shrinks as rank grows, so being near the top of a list is worth a lot and being near the bottom is worth little. Summing across lists rewards consensus: a document that both retrievers rank highly beats one that a single retriever loves but the other never returned.

The constant k is a smoothing knob. Because it is added to every rank before taking the reciprocal, a larger k compresses the difference between rank 1 (1/61) and rank 2 (1/62) — the positions start to matter less and mere presence in a list matters more, which stops any one retriever from dragging its favorite straight to the top. A very small k does the opposite, making the first position dominate. The value 60 comes from the 2009 paper that introduced RRF and has become the default in production systems: it is the default rank_constant in Elasticsearch, and Azure AI Search documents that "the algorithm performs best when you set k to a small value, such as 60."

A worked fusion

Take one query — cancel my subscription — and the two ranked lists it produces. BM25 ranks by keyword overlap; the dense retriever ranks by meaning.

Lexical (BM25):

1. A  "How to cancel your subscription"
2. B  "Subscription billing FAQ"
3. C  "Cancel or pause your plan"
4. D  "Refund policy"
5. E  "Subscription tiers explained"

Dense (embeddings):

1. C  "Cancel or pause your plan"
2. A  "How to cancel your subscription"
3. F  "Ending your membership"
4. B  "Subscription billing FAQ"
5. G  "Turn off auto-renewal"

Note the disagreement. Document C is the dense retriever's top hit but only third on keywords. Documents F and G ("Ending your membership", "Turn off auto-renewal") share no keywords with the query, so BM25 never returned them at all — they exist only because the embedding model understood the intent. Now fuse with k = 60. For each document, add 1/(60 + rank) over the lists it appears in:

  • A: 1/(60+1) + 1/(60+2) = 1/61 + 1/62 = 0.01639 + 0.01613 = 0.03252
  • C: 1/(60+3) + 1/(60+1) = 1/63 + 1/61 = 0.01587 + 0.01639 = 0.03227
  • B: 1/(60+2) + 1/(60+4) = 1/62 + 1/64 = 0.01613 + 0.01563 = 0.03175
  • F: 1/(60+3) = 1/63 = 0.01587 (dense only)
  • D: 1/(60+4) = 1/64 = 0.01562 (BM25 only)
  • E: 1/(60+5) = 1/65 = 0.01538 (BM25 only)
  • G: 1/(60+5) = 1/65 = 0.01538 (dense only)

The fused order is A, C, B, F, D, E, G. Three things happened that neither list did alone. Document A wins even though it was top of only one list, because it was near the top of both — consensus beats a single strong vote. Document C, the dense favorite, lands a hair behind A rather than first, because its middling BM25 rank pulled it down. And document F, which BM25 could not see, still reaches fourth place on its dense rank alone, ahead of BM25's own fourth-place D — the fusion recovered a relevant paraphrase that pure keyword search would have buried. That is hybrid search doing its job in one arithmetic step.

Real-World Applications

RRF hybrid search is not a research curiosity; it is the shipped default in the major search platforms, which is the strongest evidence that the score-fusion alternatives lose in practice.

  • Elasticsearch and OpenSearch expose an rrf retriever that fuses a BM25 query with a vector (kNN) query. Elasticsearch's rank_constant parameter is the k above and defaults to 60, and its documentation notes RRF "requires no tuning" — the selling point is precisely that you do not have to reconcile the two score scales yourself.
  • Azure AI Search uses RRF automatically whenever a query runs more than one retrieval in parallel — a hybrid full-text-plus-vector query, or several vector queries — and returns the fused @search.score.
  • Vector databases and RAG stacks — Weaviate, MongoDB Atlas and others — offer hybrid search with RRF as the fusion step, because a retrieval-augmented generation system feeds retrieved passages to a language model, and the single most damaging failure in RAG is retrieving the wrong passage. Hybrid search widens what the retriever can find: the exact error code and the conceptual explanation both make the shortlist.

In a full production pipeline, RRF is usually the first fusion stage, not the last word. The pattern is retrieve → fuse → rerank: two (or more) retrievers produce candidate lists, RRF fuses them into one shortlist of maybe 50 to 100 documents, and a reranker — a slow, accurate cross-encoder — then rescores just that shortlist. Fusion is cheap and decides who gets on the shortlist; reranking is expensive and decides the final order of the shortlist. They are complementary, and most serious retrieval systems use both. RRF also composes naturally with more than two inputs: fusing a BM25 list, a dense list, and a GraphRAG-style structured retriever is the same sum over one more list.

Key Concepts

  • BM25 is the standard lexical scoring function. It ranks documents by how often the query's terms appear, damped so that repeating a word ten times is not ten times as good, and weighted so that rare terms count more than common ones. It knows nothing about meaning; it matches strings.
  • Dense retrieval ranks by embedding similarity and is the mechanism behind semantic search and vector search. It matches meaning and misses exact tokens.
  • Score-agnostic fusion is the property that makes RRF work: it uses only rank order, so it never has to answer the unanswerable question of how a BM25 score of 18 compares to a similarity of 0.83.
  • Consensus weighting falls out of summing reciprocals: documents ranked highly by multiple retrievers are rewarded, which is usually a good proxy for relevance and is what makes fusion beat either retriever alone.

Challenges

The strengths of RRF come with matching limitations, and they are specific to how it works — not generic caveats about search.

  • RRF cannot express confidence. By design it discards score magnitude, so it cannot tell "the top result is a perfect match" from "the top result is the least-bad of a weak batch". Both come out as rank 1. If you need an absolute relevance threshold — "is anything here good enough to answer at all?" — RRF's fused score will not give it to you, because that information was in the raw scores it threw away.
  • k is a weak lever, and that is the point. Tuning k shifts how sharply top ranks dominate, but the method was built to work well without tuning, so the payoff from sweeping k is usually small. Teams sometimes expect it to be the knob that fixes relevance; it is not. If fusion output is poor, the problem is almost always one of the input retrievers, not the value of k.
  • Fusion cannot recover recall the retrievers never had. A document that neither BM25 nor the dense retriever returned has rank infinity in both lists and a fused score of zero — it simply is not there. RRF reorders and merges what it is given; it cannot invent candidates. If both retrievers miss the answer (a badly chunked document, an out-of-domain query), no fusion method saves you.
  • Weighting and ties need care. Plain RRF treats every list as equally trustworthy, which is wrong if one retriever is much stronger for your data; systems like Azure AI Search add per-query weight multipliers to compensate. And because many documents can share the same low reciprocal (every document that appears in exactly one list at rank 5 gets the identical 1/65), tie-breaking has to be defined explicitly rather than left to the fused score.

Code Example

The whole algorithm is a few lines: for each ranked list, add 1/(k + rank) to each document's running total, then sort. This reproduces the worked example above; run it and it prints the same fused order.

def rrf(rankings, k=60):
    """Fuse ranked lists (each a list of doc ids, best first) with RRF."""
    scores = {}
    for ranking in rankings:
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] = scores.get(doc, 0.0) + 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

bm25  = ["A", "B", "C", "D", "E"]   # lexical ranking, best first
dense = ["C", "A", "F", "B", "G"]   # semantic ranking, best first

for doc, score in rrf([bm25, dense]):
    print(f"{doc}  {score:.5f}")

Output:

A  0.03252
C  0.03227
B  0.03175
F  0.01587
D  0.01562
E  0.01538
G  0.01538

Two properties of RRF are visible directly in the code. Adding a third or fourth retriever changes nothing but the length of the outer loop — the sum absorbs any number of lists. And nowhere does a raw retriever score appear: the function only ever sees rank positions, which is the entire reason it can fuse lexical and dense results that were never measured on the same scale.

Frequently Asked Questions

Hybrid search runs two retrievers over the same query — a lexical one that matches exact words (usually BM25) and a dense one that matches meaning (embeddings) — and merges their two ranked lists into one. Each method covers the other's blind spot: keywords nail exact terms, IDs and rare tokens; vectors catch paraphrases and synonyms.
RRF is the standard way to combine ranked lists in hybrid search. It ignores each retriever's raw scores and uses only rank position, giving every document a score of 1/(k + rank) in each list and summing those across lists. Because it never compares a BM25 score to a cosine similarity, it sidesteps the problem that those numbers live on incompatible scales.
k is a smoothing constant added to every rank before taking the reciprocal. A larger k flattens the gap between rank 1 and rank 2, so no single list can dominate the fused order; a small k makes the top rank count for much more. The value 60 comes from the original 2009 paper and is the default in Elasticsearch (rank_constant) and Azure AI Search.
Because they are not on the same scale. A BM25 score has no upper bound while a cosine similarity is confined to a fixed interval, so naive addition lets BM25's larger magnitudes swamp the vector signal, and per-query normalization is fragile. RRF avoids the problem entirely by throwing the scores away and fusing ranks.
They are different stages of the same retrieve-then-rerank pipeline. Hybrid search with RRF is a fast, cheap way to build one good candidate list from two retrievers. Reranking then rescores the top of that list with a slower, more accurate cross-encoder. Fusion decides who makes the shortlist; reranking reorders the shortlist.

Continue Learning

Explore our use-case guides and prompts to deepen your AI knowledge.