AI agents

Measuring Retrieval Quality: Hit Rate, MRR and Recall@k for RAG

Antonio Evans11 min read

A diagram showing a labeled test set of queries with relevant document IDs, two retrievers producing ranked lists, and a small table comparing hit rate, recall@10, and MRR for each system and their fused result.
Key points

Key points

  1. 1Hit rate asks whether at least one relevant chunk appears in the top k, recall@k asks what fraction of all relevant chunks appear there, and MRR rewards putting the first relevant chunk near the top.
  2. 2You cannot tell whether a bad answer came from retrieval or generation unless you score the retriever on its own against a labeled set of real queries.
  3. 3Roughly 200 labeled queries with a known relevant chunk each is enough to detect meaningful differences, and that is the same order of magnitude used in Anthropic's published evaluation sets.
  4. 4Ship a candidate retriever only if its gain in recall and MRR on your own queries is larger than the latency it adds, since reranking and fusion both cost milliseconds per query.

Label first, then measure

The most common mistake in RAG evaluation is scoring the whole pipeline. A wrong answer is evidence of nothing in particular until you know whether the right chunk was ever retrieved. If the correct passage never appears in the candidate list, the generator could not have produced a grounded answer, and prompt changes are wasted effort. If the correct passage is in position 4 of a list of 5, a reranker may fix it in an afternoon.

That separation is the standard recommendation: evaluate retrieval and generation separately before testing end to end, so you can attribute failures correctly (Anyscale's RAG evaluation guide). RAG is a multi component pipeline, and a retriever that floods context with irrelevant chunks causes hallucinations even with a strong generator, which is why retrieval performance sets the ceiling for answer quality.

So you need a retrieval test set: a list of queries, each with the chunks that actually answer it. Build it from real traffic, not from imagination. Pull 300 to 500 logged queries, cluster them into intents (lookup by identifier, comparison, how to, policy question, multi hop), and sample proportionally. Then for each query, have a person mark the chunks that answer it, searching the corpus by hand.

Two practical rules make labeling cheap enough to finish:

  • Label at the chunk level, not the document level. Retrieval returns chunks, and chunking decisions change what is retrievable. Different chunk sizes and boundaries shift accuracy substantially, and the best strategy varies by document type; NVIDIA's experiments found page level chunking gave the best average end to end accuracy at 0.648, while FinanceBench did best at 1,024 tokens and an earnings dataset at 512 (NVIDIA's chunking study). Your labels must match the grain you actually return.
  • Allow more than one relevant chunk. Most real questions have two to five passages that answer them fully or partially. Binary single answer labels push you toward metrics that hide misses.

If your corpus is small enough that the whole knowledge base fits in the prompt, stop. Anthropic notes that knowledge bases under about 200,000 tokens, roughly 500 pages, can be included directly and need no retrieval at all (Anthropic's Contextual Retrieval post). Everything below assumes you are past that point.

The three metrics, and what each one hides

For a single query with a set of labeled relevant chunks, define the retrieved list as the top k results your system returns. Then:

  • Hit rate@k (also called Pass@k): 1 if at least one relevant chunk is in the top k, else 0. Averaged over queries, it tells you how often the retriever gives the generator anything useful to work with.
  • Recall@k: the number of relevant chunks in the top k divided by the total number of relevant chunks for that query. Averaged over queries.
  • MRR: the mean of 1 divided by the rank of the first relevant chunk. A query whose first relevant chunk is at rank 1 contributes 1.0, rank 2 contributes 0.5, rank 3 contributes 0.33, and a query with nothing relevant in the top k contributes 0.

The definitions follow the standard information retrieval formulations described in Evidently's RAG evaluation guide and Anyscale's metrics reference. The same sources note that hit rate is a basic functioning signal, MRR suits factoid questions where the first correct result suffices, and precision@k matters when accuracy beats completeness.

MetricQuestion it answersBlind toUse it when
Hit rate@kDid we retrieve anything useful at all?Whether the rest of the list is junkMonitoring and smoke tests
Recall@kWhat share of the answering passages did we get?Ordering within the top kMulti passage answers, synthesis, comparison questions
MRRHow high is the first correct hit?Missed second and third relevant chunksFactoid lookup where one passage answers
Precision@kWhat share of the returned list is relevant?Anything beyond position kTight context budgets, small k

A worked example makes the difference concrete. Take five queries, k=5, and one relevant chunk per query except query 3, which has three relevant chunks at ranks 1, 4, and 9.

QueryRelevant chunk ranksHit@5Recall@5Reciprocal rank
Q1111.001.00
Q2311.000.33
Q31, 4, 910.671.00
Q4700.000.00
Q5211.000.50

Hit rate@5 is 4/5 = 0.80. Recall@5 is (1 + 1 + 0.67 + 0 + 1) / 5 = 0.734. MRR is (1 + 0.33 + 1 + 0 + 0.5) / 5 = 0.566. The hit rate looks healthy, but two of five queries put the answer below rank 2, and one has relevant content at rank 9 that k=5 will never see. If your generator needs three passages per answer, hit rate is flattering you.

This is why hit rate alone is a trap. It answers "is anything there" and stops. Recall@k answers "is everything there," and MRR answers "is it near the top." Report all three, plus the k you actually feed the model, and look at the histogram of first relevant ranks, not just the mean.

MRR has a hard ceiling worth knowing: with one relevant chunk per query, the best possible MRR is 1.0, but a system that is perfect at rank 1 for 80 percent of queries and rank 3 for the other 20 percent scores 0.8 + 0.2/3 = 0.867. If your MRR sits near 0.9, you have almost no room left and should be working on recall and multi passage coverage instead. The formulation of MRR and average precision in a RAG context is walked through in this evaluation write up.

Building and sizing the labeled set

Start with the queries users actually type. A retrieval test set of 200 queries with labeled relevant chunks is enough to see differences of a few points in recall@k; below about 100, one hard query moves the average enough to flip your decision. Anthropic's public contextual retrieval evaluation used 248 queries over nine codebases, each with a golden chunk, and reported Pass@10 rising from about 87 percent to about 95 percent after adding context to chunks (the Claude cookbook walkthrough).

That example is worth studying for method, not for the numbers. They scored pass@k, which is hit rate under another name, held the corpus fixed, changed one variable, and reported the same metric before and after. You can copy that discipline.

Practical steps:

  1. Sample queries stratified by intent and by whether they contain identifiers (SKUs, error codes, ticket numbers, statute references). Lexical retrieval shines on identifiers, dense retrieval on paraphrase, and you need both represented in the same set.
  2. Hide the query's own document from anyone labeling, so they cannot just point at the source. Force them to judge whether the chunk answers the question.
  3. Record the chunk id, not a text snippet. Text drifts; ids stay stable and let you rerun the set after re chunking.
  4. Freeze a held out slice, say 20 percent of queries, and only look at it when deciding to ship. Otherwise you will tune chunk size against the same 200 queries until they are memorized.

If hand labeling is too slow to start, generate candidate query answer pairs with a model and have a person verify and correct. Treat generated labels as drafts. Retrieval metrics are only as trustworthy as the labels underneath them, and synthetic sets miss the messy phrasing real users produce, a limitation noted in the RAG evaluation guide from Evidently.

In code, scoring is short. Keep it in version control next to the config it scores:

def score_queries(runs, k):
    # runs: list of (relevant_ids: set, ranked_ids: list)
    hits, recalls, rrs = [], [], []
    for relevant, ranked in runs:
        top = ranked[:k]
        found = relevant.intersection(top)
        hits.append(1.0 if found else 0.0)
        recalls.append(len(found) / len(relevant) if relevant else 0.0)
        rr = 0.0
        for position, doc_id in enumerate(top, start=1):
            if doc_id in relevant:
                rr = 1.0 / position
                break
        rrs.append(rr)
    n = len(runs)
    return sum(hits) / n, sum(recalls) / n, sum(rrs) / n

Report recall at two values of k: your generation cutoff (what the model sees) and a wider candidate cutoff (what a reranker could see). The gap between them is the reranker's opportunity.

Isolating the retriever from the generator

To attribute failures cleanly, hold generation fixed and swap only the retrieval configuration. Concretely:

  • Pin the embedding model, the chunk boundaries, the index, and the k. Change one thing per run.
  • Log for every query: the ranked chunk ids, scores, and retrieval latency. Without the ranked list you cannot recompute a different metric later.
  • Score retrieval first. Only then assemble prompts and score generation, using a separate judge or human review for faithfulness to the retrieved context. Anyscale's guide recommends exactly this order, because end to end scores alone cannot tell you which stage failed.
  • Keep a no answer bucket. Some queries in production have no relevant chunk in the corpus. If your test set has none, you cannot see a retriever that confidently returns junk for unanswerable questions. Track precision for those separately.

One subtlety: chunking changes both what is retrievable and what "relevant" means. If you experiment with smaller chunks for factoid queries and larger or page level chunks for analytical ones, re label or map labels to the new units before comparing scores. NVIDIA's study found optimal chunk size varied by query type within similar document types, favoring 256 to 512 tokens for factoid queries and page level boundaries for complex analytical ones (NVIDIA). Comparing a 512 token index against a page level index on stale labels is a measurement error, not a model improvement.

The decision rule for fusion and reranking

Now the question practitioners actually ask: should I fuse a keyword retriever with the dense one, add a cross encoder reranker, or both?

Start from why fusion helps. BM25 and dense retrieval fail in complementary ways. BM25 is strong on exact strings like product codes and error identifiers and blind to paraphrase; dense retrieval is the reverse. Combining them by rank rather than raw score, using reciprocal rank fusion, avoids the problem that BM25 scores are unbounded positive numbers while cosine similarity is bounded between -1 and 1, so naively weighted blends let BM25 dominate (a reference on hybrid search and RRF). OpenSearch's write up makes the same point: rank based fusion is stable across differing score distributions and resistant to outliers, where min max and L2 normalization are not (OpenSearch on RRF). A PostgreSQL walk through shows the same architecture with BM25 and dense vectors fused by RRF inside the database (pgEdge).

Reranking is a different lever. It does not add candidates; it reorders them. A cross encoder scoring each (query, chunk) pair cannot scan a large corpus at query time, so the correct pattern is retrieve a wide candidate set of tens to hundreds, then rerank that shortlist (Qdrant's hybrid search with reranking tutorial). That means reranking can only fix what your first stage already fetched. This is the single most useful fact for the decision: if recall@50 is 0.70 and recall@5 is 0.65, a reranker can at best get you to 0.70. If recall@50 is 0.70 and recall@5 is 0.30, a reranker is the obvious next move.

Anthropic's numbers illustrate the composition. Contextual embeddings reduced failed retrievals by 49 percent, and adding contextual BM25 plus reranking reduced failures by 67 percent (Anthropic). Each layer contributed, and the combined effect exceeded either alone. That is a general pattern, but the size of each step is your own data's business.

So use this rule. Run your frozen test set through the current retriever and each candidate, and record recall@k at your generation cutoff, MRR, and p50 and p95 latency.

  1. Compute the ceiling: recall at the wide candidate cutoff of the cheapest configuration. If your best first stage recalls 0.90 at the wide cutoff, no reranker can exceed 0.90 at the generation cutoff.
  2. Estimate the reranker's realistic gain as recall@wide minus recall@cutoff, discounted. Ordering gains usually capture part of that gap, not all of it.
  3. Do the same for fusion: rerun with the keyword retriever added and RRF applied, and measure the delta in recall@k and MRR over the single retriever, plus the extra latency of running two queries and merging.
  4. Ship if the measured gain in recall@k, or in MRR where ordering is what your task needs, is larger than the latency you add, and if the gain survives on the held out slice. A change that moves recall by less than the noise on 200 queries, roughly a point or two, is not a result.
  5. If neither fusion nor reranking moves recall@k, the problem is upstream: chunk boundaries, embedding quality, or missing documents. Change the index, not the ranking.

Worked example. Suppose recall@5 is 0.62, MRR is 0.55, recall@50 is 0.81, and p95 retrieval latency is 40 ms. Adding a cross encoder over the top 50 candidates costs, say, 120 ms at p95 and lifts recall@5 to 0.74 with MRR 0.71. Fusion alone costs 25 ms and lifts recall@5 to 0.69 with MRR 0.60. Under the rule, fusion is the cheaper win; the reranker is justified only if your users tolerate the latency and your product depends on ordering, as it does for single answer lookups. If recall@50 had been 0.63, no reranker would have been worth building, and the honest next step would be fixing chunking.

For the latency side, measure on your own hardware and traffic shape. Reranker cost spans orders of magnitude, from a few milliseconds for distilled cross encoders to well over a hundred for large ones on long candidate lists, per a survey of reranker patterns (a comparison of reranker types and costs). The abstraction in that piece also gives the right mental model: a first stage ANN retrieval of a few hundred candidates, then a reranker scoring each against the query, then the top passages to the model.

One more option to test before either: adding context to chunks. Prefixing each chunk with a short note about where it sits in the document, generated by a model, improved top 20 chunk retrieval failure rate by 35 percent averaged across data sources in Anthropic's testing (the Claude cookbook). It is an indexing change rather than a query time change, so it costs nothing in latency. If your chunks read as fragments, try it before adding infrastructure.

Instrumenting production without fooling yourself

Offline metrics decide what to build; production signals tell you whether it worked. Capture the query, the ranked chunk ids, the retrieval latency, and whether the answer was accepted (thumbs up, citation clicked, follow up question, escalation). Do not compute hit rate in production, since you have no labels there. Instead watch the shape of the distribution: the share of queries where the model was given zero chunks below a relevance threshold, and retrieval latency at p95, both of which move before quality complaints arrive.

Log the ranked list itself, not only the final answer. When a user reports a bad answer, you can then check whether the needed chunk was in the list, which turns a vague complaint into either a labeling task or an engineering one, and you can add that query to the frozen set.

When you need to swap the embedding model or reranker without rewriting client code, running the models behind one endpoint keeps the evaluation harness unchanged while the backend changes. A single API in front of text, image and embedding models is the kind of setup that makes swapping one variable per run practical; Quanta Inference is that option if you would rather not wire a separate SDK per provider.

FAQ

FAQ

How many queries do I need in a retrieval test set?

Around 200 labeled queries is a workable minimum for detecting differences of a few points in recall@k, and below 100 single hard queries can flip your conclusion. Stratify by intent and by whether queries contain exact identifiers, and freeze a held out slice for the ship decision. Published evaluations in this space often use a few hundred queries, for example 248 in Anthropic's contextual retrieval set.

Should I use hit rate or recall@k as my primary metric?

Use recall@k when answers are assembled from several passages, which covers most synthesis and comparison questions. Use hit rate when one passage suffices and you mainly need to know retrieval is functioning. Report MRR alongside both, because neither hit rate nor recall tells you whether the relevant chunk sits at rank 1 or rank 5, and that position drives answer quality for single answer lookups.

Does a reranker improve recall?

No. A reranker reorders a candidate list your retriever already produced, so it can move relevant chunks up but cannot add chunks that were never fetched. Its maximum possible gain is the gap between recall at the wide candidate cutoff and recall at the cutoff fed to the model. If that gap is small, fix chunking or add a second retriever instead.

Why does reciprocal rank fusion beat weighted score blending?

BM25 scores are unbounded positive numbers and cosine similarities are bounded between -1 and 1, so a weighted blend gives BM25 effective dominance unless you normalize, and normalization is sensitive to outliers. RRF combines only rank positions, so it treats scores from different retrievers consistently without tuning. OpenSearch's documentation describes this stability as the main advantage over min max and L2 normalization.

How do I know a retrieval change is worth its latency?

Measure recall@k and MRR for the current and candidate configurations on your frozen set, along with p50 and p95 latency, and ship only if the quality gain exceeds the added latency and holds on the held out slice. Gains within the noise of your set size, a point or two on 200 queries, are not evidence. If no ranking change moves recall, the bottleneck is indexing, not ranking.

Sources

Sources

  1. RAG evaluation
  2. Finding the Best Chunking Strategy for Accurate AI Responses
  3. Contextual Retrieval in AI Systems - Anthropic
  4. A complete guide to RAG evaluation: metrics, testing and ...
  5. How to Evaluate Retrieval Quality in RAG Pipelines (part 2)
  6. Enhancing RAG with contextual retrieval | Claude Cookbook
  7. Hybrid Search: BM25, Vector & Reranking Reference 2026
  8. Introducing reciprocal rank fusion for hybrid search
  9. BM25, Sparse Vectors, and Reciprocal Rank Fusion
  10. Qdrant Hybrid Search with Reranking
  11. 12 Vector Rerankers Compared: Cost vs Quality
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles