Retrieval Metrics: Recall@k, MRR and nDCG@k
Given a ranked list of retrieved doc ids and graded relevance labels `qrels: dict[doc, int]` (0 = not relevant), implement `recall_at_k`, `mrr` (reciprocal rank of the first relevant doc, 0 if none) and `ndcg_at_k` with gain `2**rel - 1` and discount `log2(rank + 1)`. Duplicate ids in the ranking count only once. Then `evaluate(runs, qrels, k)` averages each metric over queries, **skipping** queries with no relevant documents and reporting how many were skipped.
Each metric answers a different question: recall@k asks 'did the evidence reach the reader model at all' (the one that matters most for RAG), MRR asks 'how far down is the first hit' (matters for single-answer UX), and nDCG rewards putting the most relevant documents highest with graded labels. Queries with no relevant docs make recall and nDCG undefined; counting them as 0 or 1 silently shifts the average, so skip and report them. Deduplicate retrieved ids, or a retriever returning the same chunk twice gets double credit.
Try it first.
tests.py · 10 passing
ranking = ["d1", "d2", "d3", "d4"]
rel = {"d2": 3, "d4": 1, "d9": 2, "d5": 0}
assert recall_at_k(ranking, rel, 3) == 1 / 3
assert recall_at_k(ranking, rel, 4) == 2 / 3
assert mrr(ranking, rel) == 0.5
L = math.log2(3)
assert abs(ndcg_at_k(ranking, rel, 3) - (7 / L) / (7 + 3 / L + 1 / 2)) < 1e-12
assert ndcg_at_k(["d2", "d9", "d4"], rel, 3) == 1.0
assert recall_at_k(["d2", "d2", "d2"], rel, 3) == 1 / 3
assert mrr(["x", "x", "d4"], rel) == 0.5
assert recall_at_k(ranking, {"d1": 0}, 3) is None and ndcg_at_k(ranking, {}, 3) is None
res = evaluate({"q1": ranking, "q2": ["d9"], "q3": ["zz"]},
{"q1": rel, "q2": {"d9": 1}, "q3": {}}, 3)
assert res["queries"] == 2 and res["skipped"] == 1
assert abs(res["recall"] - (1 / 3 + 1) / 2) < 1e-12 and res["mrr"] == 0.75For RAG, recall@k at the k you actually pass to the model is the headline metric, but labels are incomplete: unjudged documents are treated as irrelevant, which penalises a new retriever that finds good unlabelled docs. Pool results from several systems for judging, or use an LLM judge on unjudged hits and audit a sample. Then connect retrieval metrics to answer quality, since a recall gain that does not move end-to-end accuracy is not worth its latency.