Skip to content
advanced Phase 7 · Embeddings and Vector Search

Evaluating Embedding Quality

Measure retrieval quality with labeled pairs, recall at k, and downstream gains.

55m
0 problems
Topic Progress 0%

Labels Before Metrics

Every retrieval metric is an average over labeled query-to-document pairs, which makes the labels the instrument. A broken instrument produces confident nonsense — and the most common breakage is measuring the team's own vocabulary instead of the customer's.

Twelve queries, every one written by people who built the index, every one returning the right document. The demo passed, the feature shipped, and the first week of production produced tickets titled 'search returns nothing for our own part numbers'.

Queries written by the team flatter retrieval because the team already knows the corpus vocabulary. Real queries arrive abbreviated, misspelled, one clause long, phrased in the customer's words instead of the documentation's. Closing that gap is a measurement problem, and measurement starts with labels:

labeled set = [ (query, {relevant doc ids}), ... ]     # a few hundred pairs
        ▲                        ▲
  support tickets, search    marked by a person, blind
  logs, failed searches      to what the index returned

A few hundred pairs is enough to expose large quality gaps. On twelve queries, models recalling 0.92 and 0.71 are statistically indistinguishable — the ordering is luck. Over three hundred pairs the confidence interval narrows to a couple of points and the ranking means something.

Sources of labels, in ascending order of trustworthiness:

Source Cost What it actually measures
queries written by the team free the vocabulary you already share with the corpus
search logs with click feedback near-zero, already exists what users asked and what they accepted
domain expert marks sampled pairs hours correctness without click bias
pairs harvested from failure tickets free, biased to failures precisely the cases that generate complaints

The subtle trap is labels marked after seeing the rankings. A judge who has already read the retrieved list tends to call the top result relevant, and the evaluation then certifies whatever the current model already does. Mark blind, freeze the set before tuning, and treat every production miss as a new case rather than a one-off complaint. How the set gets built decides what the numbers can say; Building Eval Datasets covers the pipeline in depth.

Recall@k and MRR, Computed

Two numbers cover most retrieval questions: did the right document make the list, and how high did it sit.

def recall_at_k(ranked, relevant, k):
    return len(set(ranked[:k]) & set(relevant)) / len(relevant)

def reciprocal_rank(ranked, relevant):
    for i, doc in enumerate(ranked, start=1):   # rank is 1-based
        if doc in relevant:
            return 1 / i
    return 0.0                                  # never returned: full miss

def evaluate(run, labels, k=5):
    r1, rk, rrs = [], [], []
    for query, relevant in labels.items():
        ranked = run[query]
        r1.append(recall_at_k(ranked, relevant, 1))
        rk.append(recall_at_k(ranked, relevant, k))
        rrs.append(reciprocal_rank(ranked, relevant))
    n = len(labels)
    return {'recall@1': round(sum(r1) / n, 4),
            'recall@5': round(sum(rk) / n, 4),
            'mrr':      round(sum(rrs) / n, 4)}

labels = {
    'refund timing':     {'policy-refunds', 'faq-returns'},
    'reset my password': {'acct-recovery'},
    'enterprise sso':    {'pricing-enterprise'},
}
run = {
    'refund timing':     ['policy-refunds', 'faq-shipping', 'faq-returns'],
    'reset my password': ['faq-login', 'acct-recovery', 'policy-privacy'],
    'enterprise sso':    ['faq-login', 'policy-refunds', 'pricing-enterprise'],
}
print(evaluate(run, labels))

Output:

{'recall@1': 0.1667, 'recall@5': 1.0, 'mrr': 0.6111}

Four decisions in that code carry the interpretation:

  • / len(relevant) gives partial credit on multi-label queries: finding one of two relevant documents is 0.5, not success. Averaging per query afterwards keeps a query with four labels from counting as four queries.
  • enumerate(start=1) because rank is 1-based — reciprocal rank of a rank-0 position would be a division by zero, and off-by-one here silently doubles every score.
  • return 0.0 on a total miss rather than skipping the query. A miss is a zero, not a missing data point; skipping it inflates the average by shrinking the denominator.
  • Averaging per query, not per pair. recall@k answers "for what share of questions did the right chunk enter the list" — the unit of experience is the query, not the label.

What the Numbers Decide

The point of the metrics is to make decisions with them. An illustrative run on 320 labeled pairs from one product's documentation:

Model recall@1 recall@5 MRR Footprint
generic, 1,536-d 0.58 0.87 0.66 baseline
retrieval-tuned, 1,536-d 0.71 0.92 0.77 about 2x the price
retrieval-tuned, truncated to 256-d 0.64 0.90 0.71 index 6x smaller

Read it as three separate decisions. The tuned model buys 13 points of recall@1 for double the price — worth it when retrieval feeds answers, probably not worth it for a deduplicator. Truncation gives back 7 points of the tuned model's gain while shrinking the index sixfold, which is the dimension trade from Embedding Models Explained made concrete. And recall@5 moving only 5 points across all three rows says the top of the list is where the models differ, not the candidate set.

k is its own decision. Recall@k rises with k and so does the prompt: every extra chunk is tokens on every request, so pick k where recall flattens and pay for nothing beyond it (Context Window Budgeting).

Offline numbers are a floor, not the finish line. Retrieval can improve while answers get worse — chunks that are relevant but too fragmentary to support a sentence will raise recall@k and lower answer quality. Close the loop by running the whole pipeline over the same queries and grading the answers, with Offline Eval Harnesses for the mechanics and LLM-as-a-Judge for grading free text.

Failure modes of the evaluation itself:

  • Judges who saw the rankings — relevance marks converge on what the current model returns.
  • Selecting and reporting on the same set — the winning model's score is optimistically biased; hold a set back you never tune against.
  • Judging only the top 5 while quoting recall@10 — the labels do not cover the list you are measuring.
  • A number without its k, set size, and model string — "recall 0.87" is uninterpretable without all three.
  • No retained failure archive — every regression is rediscovered from scratch.

Quiz

1. A run reports recall@5 = 1.0 and recall@1 = 0.17. What does that combination indicate?

Question 1 options

2. What does reciprocal rank of a relevant document at rank 4 contribute to MRR?

Question 2 options

3. Why must relevance labels be marked before — and independently of — seeing the ranked results?

Question 3 options

4. Retrieval metrics improve after a model swap, but users report worse answers. Which explanation fits?

Question 4 options

Flashcards

Question

What does recall@k measure?

Answer

The share of labeled relevant documents that appear in the top k results, averaged over queries. It answers whether the right chunks enter the candidate list the LLM sees.

Question

What does MRR capture that recall@5 does not?

Answer

Position. MRR averages 1/rank of the first relevant hit, so burying the answer at rank 10 costs 0.1 while recall@5 counts it only as present or absent.

Question

How large a labeled set is useful for comparing embedding models?

Answer

A few hundred query-to-document pairs. Hundreds separate models a couple of points apart; a dozen hand-written queries cannot distinguish 0.92 from 0.71 with any confidence.

Question

Why not trust public embedding leaderboards when choosing a model?

Answer

Leaderboards measure public corpora and generic queries. Your part numbers, jargon, and query phrasing shift the ranking, so the winner has to be decided on your own labeled pairs.

Question

When can offline retrieval metrics mislead about product quality?

Answer

When relevant chunks are too fragmentary to support an answer. Recall@k rises while end-to-end answer quality falls — so the eval loop must grade full answers over the same queries.

Revision Notes

Key Takeaways

  • 1. A few hundred blind-labeled pairs beat any number of self-written demo queries
  • 2. recall@k and MRR answer different questions — presence in the list versus position in it — and the gap between them is the diagnosis
  • 3. Model and dimension choices are decided on your domain, because leaderboard corpora are not yours
  • 4. Offline retrieval metrics are a floor: close the loop over full answers before declaring a win

Interview Tips

  • • Write recall@k and MRR from memory, then explain why you average per query and return 0 for a miss instead of skipping it
  • • Given 'recall@5 is 1.0 but users still complain', reason from the spread: ordering, reranking, or unusable chunks — not a new embedding model
  • • Describe your labeling pipeline end to end: sources, blind marking, freeze-before-tuning, and how failures enter the set
  • • Be ready to defend a model swap with three numbers — recall@1, recall@5, MRR — plus the cost delta, and to say which you would give up first

Cheat Sheet

Embedding Evaluation — Cheat Sheet

Metrics

recall@k = |top_k ∩ relevant| / |relevant|     # partial credit per query
rr       = 1 / rank_of_first_relevant          # 0 if never returned
mrr      = mean(rr over queries)

Average per query, never per pair — a query with four labels must not count as four queries.

Reading the spread

Pattern Diagnosis
recall@5 high, recall@1 low ordering problem: fusion weights, reranking
recall@5 low candidate problem: model, chunking, or index
recall high, answers bad chunks relevant but unusable: evaluate end-to-end

Labeling

  • few hundred pairs, marked blind, frozen before tuning
  • sources: failed-search tickets > expert marks > clicked logs > team-written queries
  • every production miss becomes a permanent case

Deciding: pick k where recall flattens against prompt cost; compare models and widths on your own set; hold back a set you never tune against.

Failure modes: judging after seeing rankings · select-and-report on the same set · judging top-5 but quoting recall@10 · a number without k, set size, and model string · no retained failure archive.

Continue Learning

Before You Start

  • No prerequisites listed.

Next Topics

Related Reading