Back
intermediate

Embeddings and search

How hybrid search works

Implement reciprocal rank fusion and investigate when combining retrievers improves or dilutes evidence.

Lesson 25 of 44About 30 min with practice

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.

Why are two good scores not automatically additive?

Lexical and embedding systems produce scores through different calculations. Adding them directly gives the numerically larger scale more influence, even when that scale does not represent greater relevance. Rank-based fusion offers one way to combine orderings without pretending the score units match.

Inspect the candidates each method contributes before tuning the fusion. A lexical match can rescue an exact identifier, while semantic retrieval can rescue a paraphrase. The challenge asks you to make the combination rule explicit and evaluate the result on both types of query.

Example

Lexical scores are 14.2 and 9.8; vector scores are 0.83 and 0.79.

What changes

Add the raw values without calibration or a defined weighting rule.

Result

The lexical scale dominates the arithmetic, regardless of the intended importance of each signal.

A combined score needs a justified transformation. Rank fusion is a strategy to evaluate, not a universal guarantee.

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.

python
# 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.

Practice with feedback

Lesson challenge

Fuse two result lists without inventing a score scale

BM25 returns scores like 14.2 and 9.8. Your embedding search returns 0.83 and 0.79. Someone proposes adding them together.

Combine lexical and semantic retrieval by rank, and keep permissions consistent.

Check your understanding

Question 1 of 3
What is wrong with adding them?
Score: 0/0

Your task

Implement RRF over two lists, with the permission filter applied consistently.

These notes stay on this page. Download them before leaving. Code in this field is not executed.

What to include

  • Rank starts at 1, not 0
  • A document appearing in only one list still gets a score
  • The permission filter is applied to both inputs before fusion
  • You show a concrete case where fusion beats either list alone
Compare with a worked answer

Here is one way to answer. Check how it uses the information in the task.

def rrf(lists, k=60):
    scores = {}
    for ranked in lists:
        for rank, doc in enumerate(ranked, start=1):
            scores[doc] = scores.get(doc, 0.0) + 1.0 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

lex = ['n12', 'n03', 'n44', 'n07']       # exact term matches
vec = ['n44', 'n21', 'n12', 'n09']       # semantic matches
print(rrf([lex, vec]))
# ['n12', 'n44', 'n03', 'n21', 'n07', 'n09']
#
# n12 and n44 rise because both retrievers found them, which neither list
# expresses on its own: lexical had n44 third, semantic had n12 third.
# Documents found by only one retriever are kept, just lower.

# Permissions: filter identically, before fusion.
def search(query, viewer):
    allowed = visibility_for(viewer)              # e.g. {'public','members'}
    lex = bm25_search(query, visibility__in=allowed, k=50)
    vec = vector_search(query, visibility__in=allowed, k=50)
    return rrf([lex, vec])

# The bug this prevents: our first version filtered only the vector side,
# because the BM25 index had no visibility column. A members-only notice
# reached a signed-out visitor at fused rank 2. The fix was in the index, not
# in the fusion, which is the point: fusion cannot repair an unfiltered input.

When you are signed in, opening the challenge carries your edited working notes into its draft in this browser. The challenge has its own completion record. Practising here does not award points or mark it complete.

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.

Practise this lesson

Fuse two result lists without inventing a score scale

Combine lexical and semantic retrieval by rank, and keep permissions consistent.

About 10 min55 points3 checks and one applied task
Loading your lesson progress...