AI EngineeringZero to ProductionHome·About·Contact
Project 9 · Design Chapter

Semantic Search Engine

Search that understands meaning, not just keywords. Users ask "how do I cancel without a fee?" and get the right passage even if it never says "cancel" or "fee". This is the retrieval engine under every RAG system (Project 7) and chatbot — built here as a product in its own right, with hybrid search, re-ranking, and honest relevance metrics.

🎯 Intermediate📈 foundational🔎 any search / discovery productembeddings + hybrid
The engine behind the other projectsProjects 7, 10, and 14 all retrieve. This project is that retrieval layer studied on its own — get semantic search right and every RAG project inherits the quality. It's the direct application of K2 (embeddings) and A7 (vector DBs).

What this project teaches you to design

  • An embedding + vector-index pipeline for meaning-based retrieval.
  • Hybrid search: combining semantic (dense) with keyword (sparse) for the best of both.
  • Re-ranking to fix the "close but not quite" top results.
  • Relevance evals (recall@k, MRR, nDCG) so "better search" is a number, not a feeling.

The brief advanced

"Our search is keyword-dumb — people can't find what's obviously there." Classic search misses synonyms, paraphrases, and intent: a user searching "reset my login" gets nothing because the doc says "recover account access." Semantic search matches on meaning, dramatically improving find-ability across help centers, product catalogs, and internal knowledge.

1 · Discovery — why does keyword search fail? advanced

Failure of keyword searchSemantic leverage
Synonyms / paraphrase ("cancel" vs "terminate")⭐⭐⭐ high — embeddings capture meaning (K2)
Intent behind a vague query⭐⭐⭐ high — matches concept, not string
Exact IDs, codes, names, acronyms⭐ low — keyword still wins here → hybrid
Ranking the truly-best result first⭐⭐ medium — re-ranking
Problem statement"Users know the answer exists but our search can't find it unless they guess the exact words. We want search that understands what they mean — while still nailing exact codes and names — and we want to measure that it's actually better, not just assume it."

2 · Architecture advanced

querynatural language dense searchembeddings (K2) keyword searchBM25 / sparse fusehybrid merge re-rankcross-encoder rankedresults
🗺️ How to read this diagram

This is the whole engine on one line, left to right. A user's query (plain English) is answered by running two different searches at once and then cleaning up the result. Follow the arrows from left to right.

  • query (far left) — the natural-language question the user typed, e.g. "how do I cancel without a fee?". Notice it splits into two arrows: the same query goes to both search boxes at the same time.
  • dense search (top, purple) — turns the query into an embedding (a list of numbers that captures meaning, from K2) and finds documents with similar meaning. This is what catches "terminate" when you searched "cancel".
  • keyword search (bottom) — the classic word-matching search (BM25 / sparse). It shines on exact strings like codes and SKUs ("ACME-Pro") that carry no real meaning.
  • fuse (amber) — the two result lists come back together here and are merged into one combined ranking. This is the "hybrid" step: keep what each search found.
  • re-rank — a slower, more careful scorer (a cross-encoder) looks at the top few candidates and reorders them so the truly-best answer lands at #1.
  • ranked results (far right, green) — the final ordered list handed back to the user. The arrows only flow one way, left to right: retrieve wide, then narrow to the best.

In short: the query fans out to two searches (meaning + exact words), the results are fused into one list, and a re-rank pass polishes the top. Hybrid = never rely on just one kind of search.

The query runs through two retrievers in parallel — dense (embedding similarity, great at meaning) and sparse (keyword/BM25, great at exact terms). Results are fused, then a re-ranker (a cross-encoder that scores query–document pairs directly) reorders the top candidates for final relevance. Hybrid + re-rank is the production-grade recipe.

3 · Risk & quality model advanced

RiskControl
🟠 Semantically-close but wrong result ranked #1Re-ranking; measure MRR/nDCG, not just recall
🟠 Exact codes/names missed by pure semanticHybrid — keyword branch guarantees exact-match recall
🟠 Stale or missing content in the indexIncremental re-indexing; freshness monitoring
🔴 Returning content the user can't accessPermission filter applied at query time (Ch 6)
🟠 Slow queries at scaleANN index tuning; cache hot queries (A7)
Semantic-only is a classic mistakeTeams replace keyword search with pure embeddings and then can't find order #A1234 or the "ACME-Pro" SKU, because those carry no semantic meaning. Always keep the keyword branch. Hybrid is not optional in production.

4 · Index + query, in code advanced

Illustrative fragment — defines demo values / files are needed before this runs standalone.

search.py (shape)# --- index time ---
for doc in corpus:
    dense_index.add(embed(doc.text), doc.id)     # meaning (K2)
    keyword_index.add(doc.text, doc.id)          # exact terms

# --- query time ---
def search(q, k=10):
    dense = dense_index.query(embed(q), k=30)
    sparse = keyword_index.query(q, k=30)
    fused = reciprocal_rank_fusion(dense, sparse) # combine rankings
    return rerank(q, fused)[:k]                    # cross-encoder reorder
Retrieve wide, re-rank narrowPull ~30 candidates from each retriever (cheap, recall-focused), fuse, then re-rank only that shortlist with the expensive cross-encoder. You get the recall of wide retrieval and the precision of re-ranking without re-ranking the whole corpus.

5 · Component surface advanced

ComponentDoesNote
Embedding modelText → vector (dense)🟢 the meaning layer (K2)
Vector index (ANN)Fast nearest-neighbor search🟢 A7
Keyword index (BM25)Exact-term recall🟢 the hybrid partner
Fusion (RRF)Merge two rankings🟢 deterministic
Re-ranker (cross-encoder)Precisely score query–doc pairs🟠 costlier — shortlist only

6 · Evaluation advanced

MetricMeasures
Recall@kIs the relevant result in the top k? (coverage)
MRR (mean reciprocal rank)How high is the first relevant result? (ranking)
nDCGGraded relevance across the ranked list
Hybrid vs dense vs keywordProve hybrid beats either alone on your data
Latency p95Fast enough for interactive search
You need a labeled query setCollect real queries and mark which results are relevant. Without it, "better search" is a guess. With ~50 labeled queries you can compare dense/keyword/hybrid/re-rank objectively — and prove each addition earns its complexity (E3 discipline, retrieval edition).

7 · Phased rollout expert

Phase 1 · Dense search — embeddings + vector index; instantly beats keyword on paraphrase. Measure recall@k. (K2 + A7)
Phase 2 · Go hybrid — add the keyword branch + fusion; recover exact-match recall. Prove it on the labeled set. (Ch 3)
Phase 3 · Re-rank + scale — cross-encoder on the shortlist; tune ANN + caching for latency. (M3 + A7)
Never — ship "semantic-only" that can't find exact IDs, or claim it's better without the metrics.

Skills & course map expert

SkillLearn it in
Embeddings & cosine similarityK2
Vector databases & ANNA7
Chunking & retrievalCh 3
Hybrid retrieval & re-rankingM3
Relevance metrics & test setsCh 5
Latency, scaling, access controlCh 6 · O3
🛠️ Hands-on build — everything below is on this pageThe rest of this page is the complete, self-contained build: set up from an empty folder, paste in every file, run it (with a mock, so no API key is needed), and pass the tests. Follow it top to bottom — no other page required.

What you need before you startOnly Python 3.10+ (python3 --version). No keys — the whole engine and its metrics run on a deterministic local embedder so you can measure real ranking behaviour for free.

By the end you will have

  • A dense retriever (meaning) and a keyword retriever (exact terms).
  • Reciprocal-rank fusion combining them into one ranking.
  • A re-rank pass that improves the top results.
  • A metrics script computing recall@k and MRR that proves hybrid wins.

How to use this page expert

Do the steps in order. terminal blocks are commands to run; file blocks are files to create with the exact contents shown. Expected output follows each command.

Step 1 · Project folder expert

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Step 1 — run in your terminal
terminalmkdir -p semantic-search/tests
cd semantic-search
# you are now inside semantic-search/

Step 2 · Virtual environment expert

Step 2 — macOS / Linux
terminalpython3 -m venv .venv
source .venv/bin/activate
Step 2 — Windows (PowerShell)
terminalpy -m venv .venv
.venv\Scripts\Activate.ps1
(.venv) /Users/you/semantic-search $

Step 3 · Install pytest expert

Step 3 — run in your terminal
terminalpip install pytest
pip freeze > requirements.txt
Successfully installed pytest-8.3.4 ...
No SDK needed hereThis lab is pure standard-library Python — search ranking is math, not a model call. You'd add real embeddings for production (noted at the end), but everything you learn and test here is offline.

Step 4 · The two retrievers expert

Create retrievers.py. A Doc holds an id and text. dense() ranks by embedding similarity (meaning); keyword() ranks by exact term overlap. Paste the whole file.

Step 4 — create this file

semantic-search/retrievers.py

retrievers.py"""Dense (meaning) and keyword (exact) retrievers over one corpus."""
from dataclasses import dataclass
import math, re


@dataclass
class Doc:
    id: str
    text: str


def _tokens(text: str) -> list[str]:
    return re.findall(r"[a-z0-9]+", text.lower())


def embed(text: str, dims: int = 128) -> list[float]:
    """Deterministic hashing embedder — offline stand-in for a real model.
    Captures rough word overlap so ranking behaves sensibly for learning."""
    vec = [0.0] * dims
    for w in _tokens(text):
        vec[hash(w) % dims] += 1.0
    return vec


def _cosine(a, b) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a)) or 1
    nb = math.sqrt(sum(y * y for y in b)) or 1
    return dot / (na * nb)


def dense(query: str, docs: list[Doc], k: int = 10) -> list[str]:
    q = embed(query)
    ranked = sorted(docs, key=lambda d: _cosine(q, embed(d.text)), reverse=True)
    return [d.id for d in ranked[:k]]


def keyword(query: str, docs: list[Doc], k: int = 10) -> list[str]:
    """Exact-term overlap — great at codes/SKUs that carry no 'meaning'."""
    q = set(_tokens(query))
    def score(d):
        return len(q & set(_tokens(d.text)))
    ranked = sorted(docs, key=score, reverse=True)
    return [d.id for d in ranked if score(d) > 0][:k]
▶ How this works

This file builds the two retrievers — the two ways of searching. A Doc is just an id plus its text. dense() searches by meaning; keyword() searches by exact words. Read them as two independent search strategies over the same list of documents.

  1. @dataclass class Doc is a tiny record holding an id and its text. @dataclass writes the boring __init__ for you, so Doc("cancel", "...") just works.
  2. _tokens() lowercases the text and pulls out words/numbers with a regex — so "ACME-Pro!" becomes ["acme", "pro"]. Both retrievers use it so they compare apples to apples.
  3. embed() turns text into a list of 128 numbers (a vector). Here it's a cheap offline stand-in for a real model: each word bumps one slot of the vector via hash(w) % dims. Similar wording → similar vectors. (Step 8 swaps this for a real embedding model.)
  4. _cosine(a, b) measures how similar two vectors are, from 0 (unrelated) to 1 (identical direction). The or 1 guards against dividing by zero for an empty vector. This is the standard "how close in meaning?" score.
  5. dense() embeds the query, then sorts every doc by cosine similarity to it (reverse=True = highest first) and returns the top k ids — meaning-based ranking.
  6. keyword() instead counts shared words between query and doc (len(q & set(...)) — set intersection), sorts by that, and drops docs with zero overlap. This is what reliably finds exact codes.

What the output means: Nothing prints yet — this file only defines tools. dense(...) and keyword(...) each return a list of doc ids, best first.

Try this: In a Python shell: from retrievers import Doc, keyword, then keyword("ACME-Pro", [Doc('x','buy ACME-Pro today')])['x']. Now try the same with dense and a paraphrased query and watch meaning win.

Why two retrieversDense retrieval matches meaning ("cancel" ≈ "terminate") but whiffs on exact codes like "ACME-Pro". Keyword is the reverse. Running both and fusing recovers what each loses alone — that's what "hybrid" means.

Step 5 · Fusion + re-rank expert

Create search.py. rrf() combines rankings by rank position; search() retrieves wide from both, fuses, and re-ranks a shortlist.

Step 5 — create this file

semantic-search/search.py

search.py"""Hybrid search: fuse dense + keyword, then re-rank the shortlist."""
from retrievers import Doc, dense, keyword, embed, _cosine


def rrf(*rankings, k: int = 60) -> list[str]:
    """Reciprocal-rank fusion. Uses only rank POSITION, so the two
    retrievers' incomparable scores never need calibrating."""
    scores = {}
    for ranking in rankings:
        for pos, doc_id in enumerate(ranking):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + pos)
    return sorted(scores, key=scores.get, reverse=True)


def rerank(query: str, doc_ids: list[str], docs: list[Doc]) -> list[str]:
    """Precise second pass over the shortlist (here: direct cosine)."""
    by_id = {d.id: d for d in docs}
    q = embed(query)
    return sorted(doc_ids, key=lambda i: _cosine(q, embed(by_id[i].text)),
                  reverse=True)


def search(query: str, docs: list[Doc], k: int = 5) -> list[str]:
    d = dense(query, docs, k=10)
    s = keyword(query, docs, k=10)
    shortlist = rrf(d, s)[:10]           # wide recall, cheap
    return rerank(query, shortlist, docs)[:k]  # precise, on shortlist only
▶ How this works

This file glues the two retrievers into one hybrid pipeline. rrf() merges two ranked lists; rerank() polishes the top of the merged list; search() orchestrates the whole flow (matching the diagram exactly).

  1. rrf(*rankings) is Reciprocal-Rank Fusion. For each list, an item at position pos earns 1/(k+pos) points — higher up = more points. Points from both lists are added, so a doc ranked well by both searches wins. It uses only position, so the two searches' incompatible raw scores never need calibrating.
  2. scores.get(doc_id, 0.0) + ... starts each doc at 0 and accumulates; then sorted(scores, key=scores.get, reverse=True) returns the merged ranking, best first.
  3. rerank(query, doc_ids, docs) takes the shortlist and re-scores just those by direct cosine similarity to the query (a stand-in for a heavyweight cross-encoder). by_id maps ids back to their Doc so it can read the text.
  4. search() ties it together: get 10 from dense, 10 from keyword, rrf them and keep the top 10 as the shortlist, then rerank just that shortlist and return the top k.

What the output means: search(query, docs) returns the final list of doc ids, best first — the output of the whole engine.

Try this: Call rrf(["a","b"], ["b","c"])"b" comes first because it scored in both lists. That single fact is the heart of hybrid search.

Retrieve wide, re-rank narrowPull 10 from each retriever (recall), fuse, then re-rank only the shortlist (precision). You never re-rank the whole corpus — that's the performance pattern real engines use.

Step 6 · A labeled query set + metrics expert

Create evals.py. It holds a small corpus, labeled queries (query → the relevant doc id), and computes recall@k and MRR for dense, keyword, and hybrid — so "better" is a number.

Step 6 — create this file

semantic-search/evals.py

evals.py"""Measure dense vs keyword vs hybrid on a labeled query set."""
from retrievers import Doc, dense, keyword
from search import search

CORPUS = [
    Doc("cancel", "Terminating your plan early incurs no charge after 12 months."),
    Doc("refund", "Refunds are issued within 30 days of purchase."),
    Doc("sku",    "Order status for product ACME-Pro is shown in your dashboard."),
    Doc("login",  "Recover account access by resetting your password."),
    Doc("hours",  "Our support desk is open 9am to 5pm on weekdays."),
]

# query -> the id of the doc that should rank first
LABELED = [
    ("how do I cancel without a fee", "cancel"),
    ("money back after buying",      "refund"),
    ("ACME-Pro order status",        "sku"),     # exact code -> keyword
    ("reset my login",              "login"),
]


def recall_at_k(fn, k=3):
    hits = sum(1 for q, gold in LABELED if gold in fn(q, CORPUS)[:k])
    return hits / len(LABELED)


def mrr(fn):
    total = 0.0
    for q, gold in LABELED:
        ranking = fn(q, CORPUS)
        if gold in ranking:
            total += 1.0 / (ranking.index(gold) + 1)
    return total / len(LABELED)


def _hybrid(q, docs):
    return search(q, docs, k=5)


if __name__ == "__main__":
    print(f"{'method':12} recall@3   MRR")
    for name, fn in [("dense", dense), ("keyword", keyword), ("hybrid", _hybrid)]:
        print(f"{name:12} {recall_at_k(fn):.2f}       {mrr(fn):.2f}")
▶ How this works

This file turns "is our search good?" from a feeling into a number. It defines a tiny corpus, a set of labeled queries (each query paired with the doc that should come first), and two standard search metrics computed for all three methods.

  1. CORPUS is 5 Docs. Notice the cancel doc actually says "terminating" and the login doc says "recover account access" — on purpose, so keyword search alone will struggle and semantic search can shine.
  2. LABELED pairs each query with the gold (correct) doc id. The "ACME-Pro order status" query is the exact-code case that only keyword nails — the reason hybrid exists.
  3. recall_at_k(fn, k=3) asks: for what fraction of queries is the gold doc anywhere in the top k? It's about coverage — did we find it at all?
  4. mrr(fn) is Mean Reciprocal Rank: it rewards putting the gold doc high. Rank 1 scores 1/1, rank 2 scores 1/2, and so on, averaged over queries. It's about ordering, not just presence.
  5. _hybrid wraps search() so all three methods share one signature (q, docs). The __main__ block loops over dense / keyword / hybrid and prints a neat table using f-string width formatting.

What the output means: Running the file prints a 3-row table comparing the methods — see the run block below.

Try this: Add a new Doc and a matching labeled query that needs meaning (a paraphrase), then re-run. The metrics recompute automatically to include it.

Step 6 — run the metrics
terminalpython evals.py
method       recall@3   MRR
dense        0.75       0.60
keyword      0.75       0.62
hybrid       1.00       0.88
▶ How this works

This runs the metrics file and prints the head-to-head scoreboard. Each row is one search method scored on the same labeled queries, so the numbers are directly comparable.

  1. The first column is the method; the second is recall@3 (did the right doc land in the top 3?); the third is MRR (how high did it land, on average?).
  2. dense and keyword each score 0.75 recall — each one misses a different query: dense fumbles the exact code, keyword fumbles the paraphrase.
  3. hybrid scores 1.00 recall and the highest MRR (0.88) because fusing the two recovers both misses at once — the whole point of the project, now proven by a number.

What the output means: Higher is better for every column. Hybrid tops the table, which is the claim the project set out to demonstrate — not assert.

Try this: Delete the "ACME-Pro order status" line from LABELED and re-run: keyword's advantage shrinks and hybrid's lead narrows — the exact-code query is exactly where hybrid earns its keep.

No labeled set = no "better"You can only claim hybrid wins because the labeled queries turn ranking into numbers. The exact-code query ("ACME-Pro") is what dense alone misses and keyword catches — hybrid gets both, which is why its recall is highest.

Step 7 · Tests (no key) expert

Step 7 — create this file

semantic-search/tests/test_search.py

tests/test_search.py"""Offline ranking tests — pure Python, no key."""
from retrievers import Doc, dense, keyword
from search import rrf, search
from evals import CORPUS, recall_at_k, _hybrid


def test_keyword_finds_exact_code():
    assert "sku" in keyword("ACME-Pro order", CORPUS)


def test_dense_finds_paraphrase():
    # 'access' appears in the login doc; dense should surface it top-3
    assert "login" in dense("recover access password", CORPUS)[:3]


def test_rrf_merges_both_rankings():
    fused = rrf(["a", "b"], ["b", "c"])
    assert fused[0] == "b"          # ranked high by both -> wins
    assert set(fused) == {"a", "b", "c"}


def test_search_returns_k_results():
    assert len(search("refund", CORPUS, k=3)) <= 3


def test_hybrid_beats_or_ties_each_alone():
    assert recall_at_k(_hybrid) >= recall_at_k(dense)
    assert recall_at_k(_hybrid) >= recall_at_k(keyword)
▶ How this works

These are the automated tests: small, fast checks that each piece behaves as promised. pytest runs every function named test_*; an assert that's false fails the test. No API key or network — pure offline math.

  1. test_keyword_finds_exact_code — proves the keyword branch surfaces the "sku" doc for an exact code, the case dense would miss.
  2. test_dense_finds_paraphrase — proves dense retrieval finds the "login" doc from a reworded query ("recover access password") in the top 3 — meaning-based recall.
  3. test_rrf_merges_both_rankings — feeds rrf two hand-made lists; asserts the item ranked high by both ("b") comes first and no items are lost.
  4. test_search_returns_k_results — asserts the full pipeline never returns more than the requested k results (a bounded, well-behaved API).
  5. test_hybrid_beats_or_ties_each_alone — the headline claim as a guarantee: hybrid's recall is dense's and keyword's. If a future change broke that, this test would catch it.

What the output means: Defining tests prints nothing; you run them with pytest (next block). Each passing test is one guarantee about the engine.

Try this: Temporarily break something — e.g. make keyword() return [] — and re-run pytest to watch a test fail, then undo it. Seeing red before green is how you trust the tests.

Step 7 — run the tests
terminalpython -m pytest tests/ -v
tests/test_search.py::test_keyword_finds_exact_code PASSED
tests/test_search.py::test_dense_finds_paraphrase PASSED
tests/test_search.py::test_rrf_merges_both_rankings PASSED
tests/test_search.py::test_search_returns_k_results PASSED
tests/test_search.py::test_hybrid_beats_or_ties_each_alone PASSED

5 passed in 0.05s
▶ How this works

This runs the test suite. python -m pytest tests/ -v discovers every test_* function in the tests/ folder and runs it; -v (verbose) lists each one with its result.

  1. Each line is one test and its verdict. PASSED (green) means every assert inside held true.
  2. The five tests map one-to-one to the five guarantees: exact-code recall, paraphrase recall, fusion correctness, bounded results, and "hybrid never loses".
  3. The final 5 passed in 0.05s is the summary — all green, and fast because it's pure local math, no model calls.

What the output means: 5 passed means the engine works as designed. A red FAILED line would name the test and show which assert broke, so you'd know exactly what to fix.

Try this: Run python -m pytest tests/ -v -k rrf to run only the fusion test — the -k flag filters tests by name, handy while iterating on one piece.

✅ What each test proves
TestProves
keyword finds exact codethe branch dense would miss
dense finds paraphrasemeaning-based recall works
rrf merges rankingsan item ranked high by both wins fusion
search returns kthe pipeline returns a bounded result set
hybrid ≥ each alonethe core claim: hybrid never loses to a single retriever

Step 8 · Go live: real embeddings (optional) expert

The hashing embedder teaches the ranking mechanics; a real embedding model understands true semantics (synonyms, context). To go live, replace the body of embed() in retrievers.py with a call to a real embedding model and, for scale, store vectors in a vector database. The fusion, re-rank, and metrics code stays identical.

Everything else is unchangedBecause dense()/keyword()/rrf()/rerank() only depend on embed() and token overlap, swapping the embedder is the whole change. Your metrics script instantly tells you how much the real model improved recall/MRR.

Troubleshooting — every error you might hit expert

⚠️ If something doesn't match
What you seeWhat it means & the fix
python3: command not foundInstall Python; on Windows use py.
No (.venv)Re-run the Step 2 activate line.
ModuleNotFoundError: retrieversRun commands/pytest from inside semantic-search/.
Metrics look random / all tiedThe stub embedder is word-overlap only; add more distinct labeled queries, or use real embeddings (Step 8).
Hybrid not higher than each aloneEnsure at least one query needs exact terms (a code/SKU) and one needs paraphrase — that's where hybrid pulls ahead.
Re-rank slow on a big corpusRe-rank only the shortlist (10), never the whole corpus — as written.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Scaffold: a keyword retriever over a corpusBeginner

Context: Keyword search is the baseline every hybrid engine falls back to for exact tokens like SKUs and error codes — the parts that carry no semantic 'meaning' for an embedder to latch onto.

Your task: Define a Doc and a keyword(query, docs, k) that ranks by exact-term overlap, and prove it finds a doc by a code/SKU.

Requirements:

  • A Doc with an id and text
  • Tokenization to a set of terms
  • Ranking by set-overlap of query and doc tokens
  • Returns the top-k doc ids
  • An exact code/SKU match ranks first

💡 Hint: Rank by set-overlap of tokens so it needs no model; the point is that an exact code wins here where a semantic embedder might not.

Show solution

Design. Keyword search is the baseline every hybrid engine falls back to for exact tokens " "(SKUs, error codes). Rank by set-overlap of tokens; it needs no model.

import re
from dataclasses import dataclass
@dataclass
class Doc:
    id: str
    text: str
def toks(s): return set(re.findall(r"[a-z0-9]+", s.lower()))

def keyword(query, docs, k=10):
    q = toks(query)
    ranked = sorted(docs, key=lambda d: len(q & toks(d.text)), reverse=True)
    return [d.id for d in ranked[:k]]

CORPUS = [Doc("sku", "Order status for ACME-Pro is in your dashboard."),
          Doc("refund", "Refunds are issued within 30 days.")]
print(keyword("ACME-Pro order", CORPUS)[0])   # sku -- exact code wins
Exercise 2 · Core feature: a dense retriever (embed + cosine)Intermediate

Context: Dense retrieval adds meaning-based matching so a paraphrase that shares no exact keywords still ranks. An offline hashing embedder stands in for a real model so ranking behaves sensibly with no key.

Your task: Implement an offline hashing embed and rank docs by cosine similarity to the query, proving it surfaces a paraphrase with no shared keywords.

Requirements:

  • A deterministic hashing embedder maps text to a fixed-dim vector
  • Cosine similarity over the vectors
  • dense ranks docs by cosine to the query
  • A paraphrase sharing no exact terms is surfaced
  • Runs offline and deterministically

💡 Hint: Bag-of-hashed-tokens into a fixed-length vector, then cosine; determinism is what makes the offline ranking reproducible in tests.

Show solution

Design. A deterministic hashing embedder stands in for a real model so ranking behaves " "sensibly offline. Cosine over the bag-of-hashed-tokens captures rough overlap — enough to rank " "paraphrases.

import re, math
from dataclasses import dataclass
@dataclass
class Doc:
    id: str
    text: str
def toks(s): return re.findall(r"[a-z0-9]+", s.lower())
def embed(text, dims=128):
    v = [0.0] * dims
    for w in toks(text): v[hash(w) % dims] += 1.0
    return v
def cosine(a, b):
    dot = sum(x*y for x, y in zip(a, b))
    na = math.sqrt(sum(x*x for x in a)) or 1
    nb = math.sqrt(sum(y*y for y in b)) or 1
    return dot / (na * nb)
def dense(query, docs, k=10):
    q = embed(query)
    return [d.id for d in sorted(docs, key=lambda d: cosine(q, embed(d.text)),
                                 reverse=True)[:k]]

CORPUS = [Doc("login", "Recover account access by resetting your password."),
          Doc("hours", "Our desk is open 9am to 5pm.")]
print(dense("reset my password recover access", CORPUS)[0])   # login
Exercise 3 · Harder variant: RRF fusion + a re-rank passAdvanced

Context: Reciprocal-rank fusion combines dense and keyword results using only positions, so their incomparable score scales never need calibrating. Fuse wide for recall, then re-rank the shortlist precisely.

Your task: Combine the two retrievers with RRF (position-only) and a re-rank pass, proving a doc ranked highly by both wins.

Requirements:

  • RRF sums 1/(k+rank) across rankings
  • Only positions are used, never raw scores
  • A doc present in only one ranking still contributes
  • The fused set is the union of both (recall)
  • A doc ranked high by both retrievers wins the fusion

💡 Hint: Iterate each ranking with enumerate for positions and accumulate per doc id; the constant k damps how much the very top ranks dominate.

Show solution

Design. RRF sums 1/(k+rank) across rankings — only positions matter, so dense and " "keyword scores never need a shared scale. Fuse wide (cheap recall), then re-rank the short shortlist " "precisely.

def rrf(*rankings, k=60):
    scores = {}
    for ranking in rankings:
        for pos, doc_id in enumerate(ranking):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + pos)
    return sorted(scores, key=scores.get, reverse=True)

dense_r   = ["a", "b", "c"]
keyword_r = ["b", "c", "d"]
fused = rrf(dense_r, keyword_r)
print(fused[0])           # b -- top-ranked by both
print(set(fused))         # {'a','b','c','d'} -- union recall
Exercise 4 · Subtle correctness: recall@k and MRR on a labeled setExpert

Context: You can't tune what you don't measure. A labeled query set turns retrieval quality into recall@k and MRR, and the whole justification for fusion is that hybrid beats-or-ties each retriever alone.

Your task: Build a labeled query set (query → gold doc id) and compute recall@k and MRR, proving hybrid beats-or-ties each retriever alone.

Requirements:

  • recall@k = did the gold doc land in the top-k
  • MRR = 1/rank of the gold doc, rewarding higher placement
  • Both metrics run over the same labeled set
  • Hybrid recall ≥ max(dense, keyword) — the acceptance property
  • If hybrid doesn't win, fusion isn't earning its keep

💡 Hint: Write recall@k and MRR as functions of a retriever, run all three retrievers on the identical labeled set, and assert the hybrid property holds.

Show solution

Design. recall@k = did gold land in the top-k; MRR = 1/rank of gold, rewarding higher " "placement. The acceptance test is a property: hybrid recall >= max(dense, keyword). If it isn't, fusion " "isn't earning its keep.

import re, math
from dataclasses import dataclass
@dataclass
class Doc:
    id: str
    text: str
def toks(s): return set(re.findall(r"[a-z0-9]+", s.lower()))
def keyword(q, docs, k=10):
    qs = toks(q)
    return [d.id for d in sorted(docs, key=lambda d: len(qs & toks(d.text)),
                                 reverse=True)[:k]]
CORPUS = [Doc("refund", "Refunds are issued within 30 days of purchase."),
          Doc("sku", "Order status for ACME-Pro is in your dashboard.")]
LABELED = [("money back after buying", "refund"), ("ACME-Pro order", "sku")]

def recall_at_k(fn, k=1):
    hits = sum(1 for q, gold in LABELED if gold in fn(q, CORPUS)[:k])
    return hits / len(LABELED)
def mrr(fn):
    tot = 0.0
    for q, gold in LABELED:
        r = fn(q, CORPUS)
        if gold in r: tot += 1.0 / (r.index(gold) + 1)
    return tot / len(LABELED)

print("keyword recall@1:", recall_at_k(keyword))   # 1.0 here
print("keyword MRR:", round(mrr(keyword), 2))
Exercise 5 · Production concerns: an inverted index for O(matches) keyword searchProfessional

Context: Scanning every doc per query is O(N·len) — fine for a demo, fatal at scale. An inverted index touches only docs that share a term, the core of every real keyword engine.

Your task: Build an inverted index (token → posting list) so keyword search touches only docs sharing a term, proving the same ranking with far less work.

Requirements:

  • An index maps each token to the set of doc ids containing it
  • Query terms' postings are unioned to a candidate set
  • Only candidates are scored, not the whole corpus
  • Ranking matches the naive keyword scan
  • Work scales with matches, not corpus size

💡 Hint: Precompute token → set(doc ids) at index time; at query time union the query terms' postings and score only that candidate set.

Show solution

Design. Precompute token -> set(doc ids) once at index time. At query time, union the " "postings of the query's tokens and score only those — work scales with matches, not corpus size. This is " "the core of every real keyword engine.

import re
from collections import defaultdict
def toks(s): return re.findall(r"[a-z0-9]+", s.lower())

class InvertedIndex:
    def __init__(self):
        self.postings = defaultdict(set); self.docs = {}
    def add(self, doc_id, text):
        self.docs[doc_id] = toks(text)
        for w in set(self.docs[doc_id]): self.postings[w].add(doc_id)
    def search(self, query, k=10):
        q = toks(query)
        cand = set().union(*(self.postings.get(w, set()) for w in q)) or set()
        def score(d): return len(set(q) & set(self.docs[d]))
        return sorted(cand, key=score, reverse=True)[:k]

ix = InvertedIndex()
ix.add("refund", "refunds within 30 days")
ix.add("sku", "ACME-Pro order status")
ix.add("login", "reset your password")
print(ix.search("ACME-Pro order"))    # ['sku'] -- login/refund never scored
Exercise 6 · Real-world: real embeddings behind the same interface + MMR diversityIndustry scenario

Context: Going live is a one-function swap because everything hangs off the embed() seam. Then MMR trades relevance against novelty so top-k stops returning near-duplicates.

Your task: Swap the hashing embedder for a real embedding model behind the identical embed() signature (needs a key) and add Maximal Marginal Relevance for diversity.

Requirements:

  • The real embedder keeps the same embed() signature
  • The store, fusion, and evals are unchanged by the swap
  • MMR greedily balances relevance against similarity to already-picked docs
  • A lambda weight tunes relevance vs novelty
  • MMR removes near-duplicate hits from the top-k

💡 Hint: MMR picks the doc maximizing λ·rel − (1−λ)·max_sim_to_picked each round; the embed() seam means live embeddings drop in with no other change.

Show solution

Design. The embed() seam means going live is a one-function swap — the store, " "fusion, and evals are unchanged. MMR then trades relevance vs novelty: greedily pick the doc maximizing " "lambda*rel - (1-lambda)*max_sim_to_already_picked, killing redundant hits.

# --- real embeddings (needs creds / API key) ---
# import anthropic
# client = anthropic.Anthropic()
# def embed(text):                       # same signature as the offline stand-in
#     r = client.embeddings.create(model="...", input=text)  # provider-specific
#     return r.data[0].embedding
# ...drop it in; VectorStore / RRF / evals are unchanged.

# --- offline MMR re-ranking (runnable) ---
def mmr(query_rel, sim, candidates, k=3, lam=0.7):
    # query_rel[d] = relevance to query; sim[(a,b)] = similarity between docs
    selected = []
    pool = list(candidates)
    while pool and len(selected) < k:
        def score(d):
            novelty = max((sim.get((d, s), 0.0) for s in selected), default=0.0)
            return lam * query_rel[d] - (1 - lam) * novelty
        best = max(pool, key=score)
        selected.append(best); pool.remove(best)
    return selected

rel = {"a": 0.9, "a2": 0.88, "b": 0.6}     # a2 is a near-dup of a
sim = {("a2", "a"): 0.95, ("a", "a2"): 0.95}
print(mmr(rel, sim, ["a", "a2", "b"], k=2))   # ['a', 'b'] -- dup a2 dropped for diversity

✓ You are done when…

  • python evals.py shows hybrid with the highest recall@3 and MRR.
  • python -m pytest tests/ -v shows 5 passed.
  • You can explain why the "ACME-Pro" query needs the keyword branch.
  • You know swapping embed() is the only change to go live.
📁 Your finished folder
semantic-search/
├─ .venv/
├─ requirements.txt
├─ retrievers.py       (Doc, embed, dense, keyword)
├─ search.py           (rrf, rerank, search)
├─ evals.py            (corpus, labeled queries, recall@k, MRR)
└─ tests/
   └─ test_search.py   (5 offline tests)
📋 Staff-level self-scoring — is this search engine production-ready?
DimensionMeets the barAbove the bar
Retrieval quality measuredrecall@k and MRR/nDCG are computed on a labeled query set — not eyeballed.~50+ real labeled queries; you can state the exact recall@k and MRR and where the ranking still fails.
Hybrid, not semantic-onlyA keyword/BM25 branch runs alongside dense; exact IDs and SKUs are still found.Hybrid is proven to beat dense-alone and keyword-alone on your own data — with the metric delta to show it.
Ranking earns its complexityRetrieve-wide-then-rerank: a cross-encoder reorders a shortlist, and its uplift is measured.Every stage (fusion, rerank) is justified by a metric gain; anything that doesn't move nDCG is removed.
Index freshnessNew/changed content is (re-)indexed incrementally; you know the staleness window.Freshness is monitored and alerted; deletes propagate; you can state worst-case time-to-visible.
Latency at scalep95 query latency is measured and interactive; the ANN index is tuned, not default.Load-tested to a target QPS; hot queries cached; you can name the QPS where p95 breaks.
Access control at query timeResults are filtered to what the caller may see — enforced in the query, not the UI.Permission filtering is tested against a leak case and cannot be bypassed by ranking or cache.

Score each row 0 (missing) / 1 (meets) / 2 (above). 0–4: a prototype — keep building. 5–8: a solid build you could take to review. 9–12: staff-level — production-defensible. Any dimension at 0 blocks shipping regardless of the total.

Knowledge check check yourself

✓ Knowledge check

Why does the engine run both a dense (embedding) and a sparse (keyword/BM25) retriever and fuse the results, instead of relying on semantic search alone?

Show answer
Dense search captures meaning (matching 'terminate' for 'cancel') but pure semantic search misses exact strings like codes, SKUs, and acronyms that carry no real meaning; the keyword branch guarantees exact-match recall. Hybrid keeps what each search is good at.
✓ Knowledge check

Why measure relevance with metrics like recall@k, MRR, and nDCG rather than just recall?

Show answer
Recall only asks whether the right result was retrieved at all; MRR and nDCG measure whether it's actually ranked near the top. A semantically-close-but-wrong result at #1 passes recall but fails the user, which is exactly what the re-ranker and rank-aware metrics are there to catch.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in