Hybrid Search Measured: Where Fusion and Reranking Actually Help

BM25, dense, RRF and a cross-encoder reranker on SciFact, with paired confidence intervals. Fusion helped BM25 a lot and dense a little; the reranker hurt.


The standard RAG retrieval stack has become a ritual: embed the documents, keep a BM25 index too, fuse the two lists with reciprocal rank fusion, then run a cross-encoder reranker over the top of the fused list. Each step comes with a blog post saying it helps.

I wanted to know how much each step helps, with a confidence interval, on a dataset with real relevance labels. So I built the four pieces myself, in about 100 lines of numpy, and ran them on SciFact from BeIR: 5,183 scientific abstracts and 300 claims, each labelled with the abstracts that support or refute it.

The pieces

BM25 is lexical: a document scores on how many query terms it contains, weighted by how rare each term is across the corpus and discounted for document length. Stemmed, stopwords removed, k1 = 0.9 and b = 0.4.

Dense retrieval embeds query and documents with bge-small-en-v1.5 and ranks by cosine similarity. The whole thing is one matrix multiply.

Reciprocal rank fusion is the part people get mystical about, and it’s four lines:

def rrf(rankings, k=60):
    fused = Counter()
    for ranking in rankings:
        for rank, doc in enumerate(ranking, 1):
            fused[doc] += 1 / (k + rank)
    return [doc for doc, _ in fused.most_common()]

That’s the formula from Cormack, Clarke and Büttcher at SIGIR 2009, with their k of 60. It never looks at a score, which is the point: a BM25 score of 14.2 and a cosine of 0.83 have nothing to say to each other, but “third on one list and fifth on the other” does. For comparison I also ran convex fusion, which min-max normalises both score lists and averages them, which is what most vector databases do when you ask for hybrid.

Reranking takes the top 20 from a list and rescores each query-document pair with a cross-encoder, ms-marco-MiniLM-L-6-v2, which reads both texts together and can attend across them.

Every comparison below is paired: the same 300 queries under both systems, with a bootstrap over queries for the 95% interval. I’ve argued elsewhere that this is the only honest way to compare two runs, and this benchmark was built to practise it.

Results

Bar chart of nDCG@10 on SciFact: convex fusion 0.743, RRF fusion 0.734, dense 0.720, RRF plus rerank 0.706, dense plus rerank 0.702, BM25 0.680

SystemnDCG@10Recall@100MRRLatency per query
BM250.6800.9220.6522.1 ms
Dense (bge-small)0.7200.9530.6881.2 ms
RRF fusion0.7340.9650.706~3 ms
Convex fusion0.7430.9620.717~3 ms
Dense + rerank0.7020.9530.6721,437 ms
RRF + rerank0.7060.9650.6761,437 ms

And the paired comparisons, which are the part that matters:

ChangeΔnDCG@1095% CIQueries better / worse / tied
BM25 → RRF fusion+0.053+0.031 to +0.07682 / 21 / 197
Dense → RRF fusion+0.013−0.010 to +0.03757 / 38 / 205
Convex fusion → RRF fusion−0.009−0.025 to +0.00630 / 33 / 237
Dense → dense + rerank−0.018−0.044 to +0.00844 / 55 / 201
RRF fusion → RRF + rerank−0.028−0.054 to −0.00242 / 60 / 198

Fusion helps a lot, or a little, depending on where you start

If your system is BM25 today, adding a dense retriever and fusing is worth 5.3 points, and the interval is nowhere near zero. If your system is already a decent dense retriever, adding BM25 is worth 1.3 points and the interval includes zero. Same technique, same data; the value depends on what it’s being added to.

Where the gain lives is also worth seeing. Recall@100 went from 0.953 to 0.965: fusion put relevant documents into the candidate set for about 1 in 80 queries where dense retrieval alone had missed them. That’s the real job of a second retriever, catching the exact-term matches (a drug name, an error code, a gene symbol) that embeddings smooth over. If you want a quick test of whether your queries need it, look for identifiers in them.

RRF and convex fusion tie

Convex fusion edged RRF by 0.9 points with an interval that includes zero and 237 of 300 queries unchanged. I’d call that a tie, and a tie goes to the method with no parameters to tune. RRF has one constant that the original authors set once and never changed; convex fusion has a weight and a normalisation scheme, and the min-max step is brittle when one list’s scores have a long tail. Use RRF unless you have a measurement that says otherwise.

The reranker made it worse

This is the row I didn’t expect. Reranking the top 20 cost 1.8 points over dense retrieval and 2.8 over fusion, and the second interval excludes zero. It also cost about 1.4 seconds per query on CPU, against 1 to 3 milliseconds for everything else.

Two things are going on. First, a reranker can only reorder what it’s given. Recall@100 is identical with and without it, because the candidate set didn’t change; it can’t rescue a document retrieval missed. Second, this reranker was trained on MS MARCO, which is web search questions over web passages. SciFact is scientific claims over abstracts. A 22-million-parameter cross-encoder with the wrong training distribution lost to a 33-million-parameter bi-encoder with a better one. “Cross-encoders are more accurate” is true in-distribution and on the leaderboards; it’s not a law.

I’m not saying rerankers don’t work. Larger ones, and ones tuned to your domain, usually do. I’m saying that on this corpus the default recipe’s final step was a 700× latency cost for a worse ranking, and nobody would have noticed without a labelled set, because the results still look plausible.

Most queries don’t change at all

Look at the tied column. Between 182 and 237 of 300 queries produced the identical top-10 under both systems in every comparison. Decisions about hybrid search are decided by the 20 to 40% of queries where the systems disagree, which is why per-query win and loss counts tell you more than an average, and why a change can look like a win on the dashboard and lose on more queries than it wins (reranking: 44 better, 55 worse).

What I’d do

  1. Start from a strong dense retriever and measure before adding BM25. Expect a small gain concentrated in identifier-heavy queries. If your queries have none, you may not need it.
  2. Fuse with RRF, k = 60. Nothing to tune, nothing to normalise.
  3. Treat a reranker as a hypothesis. Build a labelled set of your own queries first (50 is enough to catch a regression this size), and only keep the reranker if it wins with the interval clear of zero. Keep the paired bootstrap in CI.
  4. Chase recall before precision. Fusion’s gain was mostly recall. If the right document isn’t in the top 100, no amount of reordering helps, and chunking decides that before any of this runs.

Limits

One dataset. I planned to run NFCorpus and FiQA too, and the NFCorpus run did not finish on the 8 GB laptop I had, so these are SciFact numbers and SciFact is a particular kind of corpus: short, dense scientific prose with a lot of exact terminology. BM25 parameters weren’t tuned. Both models are small; a larger reranker or one trained on scientific text would likely change the last two rows. Reranking depth was 20.

Code, tests (including a hand-checked RRF case and nDCG against a worked example) and the full results: ajinb/hybrid-retrieval-bench. python bench.py --datasets scifact reproduces everything above; it downloads the dataset and both models on first run. For the basics, start with what RAG is and vector index trade-offs.

Frequently asked questions

What is reciprocal rank fusion?

A way to combine ranked lists from different retrievers without comparing their scores. Each document gets the sum over lists of 1 divided by (k plus its rank), with k conventionally 60, as in the 2009 paper by Cormack, Clarke and Büttcher. Documents that rank well in several lists rise to the top. Because it uses ranks only, BM25 scores and cosine similarities never have to be put on the same scale.

Does hybrid search beat dense retrieval?

It depends on what you'd have otherwise. On SciFact, fusing BM25 with a dense retriever gained 5.3 nDCG@10 points over BM25 alone, with a confidence interval well clear of zero, but only 1.3 points over the dense retriever alone, with an interval that includes zero. Most of the gain shows up as recall: fusion found relevant documents in the top 100 for more queries than either system alone.

Does a reranker always improve retrieval?

No. On SciFact, reranking the top 20 with ms-marco-MiniLM-L-6-v2 lowered nDCG@10 by 1.8 points over dense retrieval and 2.8 points over fusion, and cost about 700 times the latency. A cross-encoder trained on web search passages is not automatically better than a strong bi-encoder on scientific abstracts. Measure it on your own queries before paying for it.

Comments