AI

WritingSearch series

Hybrid Search

Keyword search and vector search fail in opposite directions. Run both, merge the results, and you keep the strengths of each.

By Sachin Gupta7 min read
Portrait of Sachin Gupta rendered in binary

Part six of the search series. BM25 is strong on exact terms and blind to synonyms; vector search is the reverse. Hybrid search runs both and fuses the two ranked lists. The hard part is combining incompatible scores, which Reciprocal Rank Fusion (RRF) sidesteps by using ranks instead. Worked examples plus an interactive RRF explorer.

BM25 and vector search are each excellent at what the other is bad at. The obvious move is to run both and combine them. The obvious move is also the hard one, because their scores do not mean the same thing. This is how hybrid search pulls it off.

Give a hiring shortlist to two people: a clerk who matches the exact keywords on each resume, and a recruiter who understands what the job actually needs. Each catches what the other misses. You would not trust just one and bin the other; you would merge their shortlists. Hybrid search does exactly that with keyword results and vector results.

Hybrid search sends one query to two retrievers at once, keyword search (BM25) for exact terms and vector search for meaning. Each returns a ranked list, and Reciprocal Rank Fusion merges the two rankings into one blended result list

Two strengths, two blind spots

Keyword search and vector search fail in opposite directions, which is exactly why they pair well.

BM25 owns exact terms. Product codes, error strings, function names, people's names, rare jargon. Search for error E1483 and BM25 lands the document with that exact code on top. A vector model has likely never seen that string, so it places it more or less at random.

Vectors own meaning. Search for the app keeps crashing and a vector model happily returns a document titled "resolving unexpected application shutdowns", even though the two share no words. BM25 returns nothing useful, because there is no term overlap to score.

Hybrid search runs both retrievers on every query and merges their results, so you keep the exact-term precision of BM25 and the paraphrase-tolerance of vectors at the same time.

The hard part is merging

Running both is trivial. Combining their two ranked lists is not, because the scores are not comparable. BM25 scores are unbounded and query-dependent, so a score of 12 means nothing on its own. Cosine similarities live in a small fixed range. Add them together and, depending on the query, BM25 either drowns out the vector scores or disappears under them.

You can normalize each list's scores into a 0-to-1 range and take a weighted sum, a "convex combination", and some systems do. But score distributions shift from query to query, so the weighting is fiddly to tune and brittle in production. There is a simpler idea that avoids the whole problem.

Reciprocal Rank Fusion

The trick is to throw the scores away and keep only the ranks. Reciprocal Rank Fusion (RRF) gives each document a fused score by summing, across every list it appears in, one over a constant plus its rank in that list:

RRF(d) = sum over lists L of  1 / (k + rank_L(d))

rank_L(d) is the document's position in list L (1 for the top result), and k is a constant, conventionally 60. A document ranked first in a list contributes 1/(k+1); ranked tenth, 1/(k+10). The reciprocal makes top ranks count for the most, and k controls how sharply: with k = 60, 1/61 and 1/62 are almost equal, so the gap between rank 1 and rank 2 is tiny. The practical effect is that a document appearing in both lists usually beats one that is merely first in a single list. Consensus wins, and a strong result that only one retriever found still gets a fair score.

Because it uses ranks alone, RRF needs no score normalization and does not care that BM25 and cosine sit on different scales. That is the entire reason it became the default way to fuse.

Here it is on one query. A keyword list and a semantic list fuse into one, with the math shown per document, and the rank constant k is yours to move:

Interactive · Reciprocal Rank FusionOpen in Visualizers →
Query: “reset my password”
Keyword (BM25)
matches literal terms
  1. 1Reset your password
  2. 2Password reset email issues
  3. 3Change password in settings
  4. 4Forgot your username
  5. 5Cannot sign in
Semantic (vector)
matches meaning
  1. 1Recover a locked account
  2. 2Reset your password
  3. 3Cannot sign in
  4. 4Change password in settings
  5. 5Password reset email issues
Fused (RRF)
score = 1/(k+rank) summed
  1. 1Reset your password0.0325
    K#1 V#2
  2. 2Password reset email issues0.0315
    K#2 V#5
  3. 3Change password in settings0.0315
    K#3 V#4
  4. 4Cannot sign in0.0313
    K#5 V#3
  5. 5Recover a locked account0.0164
    K— V#1
  6. 6Forgot your username0.0156
    K#4 V—
A document ranked high in either list scores well; ranked high in both, it wins. No score normalization, just ranks. Larger k flattens the gap between ranks.

In production

Elasticsearch ships RRF as a first-class retriever, with a rank_constant that defaults to 60, and OpenSearch offers hybrid search that fuses a lexical query and a neural one. In both, the recipe is identical: retrieve with BM25, retrieve with vectors, fuse the two rankings, and return the blend.

Hybrid gives you a strong candidate set, but the ordering is still just fusion arithmetic. The final move in search is to take the top of that set and look at it much harder with a heavier model that reads each query and document together, then reorder. That is reranking, and it is the last part.

Related

Tagged