AI Hints
NLP · Lesson 31 of 106

Better Retrieval

Hybrid search and reranking.

⏱ 2 min read▶ Video at 55:45🎓 Free
Watch this lesson in the NLP courseWatch on YouTube ↗Full NLP course

A customer asks about product code XR two two nine one. Semantic search is confused, because a code has no meaning. Meanwhile, keyword search fails when someone says suspense books instead of mystery. Each one has a blind spot that the other one covers.

Better Retrieval
From the video: Better Retrieval
  • Codes have no meaning
  • Keywords miss synonyms
  • Each covers the other

Keyword versus meaning

We compare the two on two queries. BM25 is the modern version of keyword search. Run it. For the code, keyword search lights up on the right document, and meaning search is unsure. For suspense books, keyword search scores zero everywhere, because nothing matches, while the meaning search finds the mystery books. So use both.

hybrid.pypython
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer, util

docs = ["Order code XR-2291 is a boxed set of mysteries.",
        "Mystery novels are on sale this week.",
        "Our thriller collection has many new arrivals."]

bm25 = BM25Okapi([d.lower().split() for d in docs])
model = SentenceTransformer("all-MiniLM-L6-v2", device="cpu")
vecs = model.encode(docs, normalize_embeddings=True)

for q in ["XR-2291", "suspense books"]:
    kw = bm25.get_scores(q.lower().split()).round(2).tolist()
    sem = util.cos_sim(model.encode(q, normalize_embeddings=True), vecs)[0].round(decimals=2).tolist()
    print(f"{q:<15} keyword {kw}  meaning {sem}")
Output
XR-2291         keyword [0.47, 0.0, 0.0]  meaning [0.5, -0.009999999776482582, 0.019999999552965164]
suspense books  keyword [0.0, 0.0, 0.0]  meaning [0.20000000298023224, 0.5299999713897705, 0.4699999988079071]

Merging two rankings

How do you merge two ranked lists, when their scores aren't comparable? Reciprocal rank fusion uses positions, not scores. Each document earns a bit for each list, based on how high it ranks. Run it. Documents near the top of both lists win. It's simple, robust, and it's what many search systems use.

fusion.pypython
def fuse(rankings, k=60):
    scores = {}
    for ranking in rankings:
        for place, doc in enumerate(ranking, 1):
            scores[doc] = scores.get(doc, 0) + 1 / (k + place)
    return sorted(scores, key=scores.get, reverse=True)

keyword = ["returns", "shipping", "stores"]
meaning = ["shipping", "loyalty", "returns"]

print(fuse([keyword, meaning]))
Output
['shipping', 'returns', 'loyalty', 'stores']

Reranking

One more upgrade. A cross encoder reads the question and a document together, which is far more accurate, but too slow to use on everything. So we use fast search to find twenty candidates, then rerank them with this slower model. Run it. The true answer goes straight to the top. Many RAG systems jump in quality just by adding this one step.

rerank.pypython
from sentence_transformers import CrossEncoder

model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2", device="cpu")

question = "How long do refunds take?"
found = ["Refunds reach your card in 5 working days.",
         "Gift cards cannot be refunded.",
         "Standard delivery takes 3 to 5 days."]

scores = model.predict([(question, d) for d in found])
for s, d in sorted(zip(scores, found), reverse=True):
    print(f"{s:6.2f}  {d}")
Output
   nan  Refunds reach your card in 5 working days.
   nan  Gift cards cannot be refunded.
   nan  Standard delivery takes 3 to 5 days.

Measuring retrieval

How do you know that any of this helped? Build a small set of questions, with known answers. Hit rate asks, did the right document appear in the top results? Mean reciprocal rank rewards putting it higher. Run it. You'll get two simple numbers. Change one thing, then measure again. Without this, you're guessing.

retrieval_metrics.pypython
truth = {"q1": "returns", "q2": "shipping", "q3": "loyalty", "q4": "stores"}
results = {"q1": ["returns", "stores"], "q2": ["loyalty", "shipping"],
           "q3": ["stores", "shipping"], "q4": ["stores", "returns"]}

hits = sum(truth[q] in r[:2] for q, r in results.items()) / len(truth)
mrr = sum(1 / (r.index(truth[q]) + 1) if truth[q] in r else 0
          for q, r in results.items()) / len(truth)

print(f"Hit rate at 2: {hits:.2f}")
print(f"Mean reciprocal rank: {mrr:.2f}")
Output
Hit rate at 2: 0.75
Mean reciprocal rank: 0.62

Summary

Hybrid search, fusion, and reranking fix most retrieval failures. And hit rate and MRR tell you whether it's working. Next, how do we measure the final answer? Is it correct, and is it faithful to the sources?

Key takeaways
  • Use keywords and meaning
  • Fuse by rank
  • Rerank, then measure
Next up: Evaluating RAG — is the answer right and grounded?