Skip to content

Medical AI · Retrieval-augmented generation (RAG) · Lesson 4 of 7

Evaluating retrieval

Reviewed by Dr. Ram Kumar G, MBBS, MD (Internal Medicine), MPH (USA), on · Last updated

  • Intermediate
  • Technical focus
  • About 30 minutes
  • Includes exercise

Before this lesson: Embeddings and vector stores

In short

If retrieval fails, the model never sees the right evidence, so evaluate retrieval separately from answers. Build a set of realistic questions labelled with the passages that answer them, then measure how often the right passage appears in the top results (recall@k) and how high it ranks (mean reciprocal rank). Compare methods on the same set, and read every miss: small test sets are noisy, and the misses tell you what to fix.

You will learn to

  • Build a labelled retrieval test set with clinicians
  • Compute recall@k and mean reciprocal rank (MRR) in Python
  • Compare keyword, embedding and hybrid retrieval fairly
  • Use failure analysis to decide what to improve

Why evaluate retrieval on its own

An end-to-end check ('was the answer right?') mixes two failures: the right passage was not retrieved, or it was retrieved but the model misused it. Measuring retrieval separately shows which part of the system to fix. It is also cheap: once questions are labelled, every change can be re-scored in seconds.

MetricMeaningUse
Recall@kShare of questions whose correct passage appears in the top k resultsMain metric when the model sees the top k passages
Precision@kShare of the top k results that are relevantWhen irrelevant passages crowd the context
Mean reciprocal rank (MRR)Average of 1 / rank of the first correct passageRewards putting the right passage first
nDCG@kRank-weighted score allowing graded relevance (fully, partly, not relevant)When several passages are partly relevant

Building a labelled question set

  • Collect questions in the words real users would use: clinicians' shorthand, patients' plain language, typos.
  • For each question, have a clinician mark which passage or passages answer it. Include questions that the documents cannot answer, labelled as such.
  • Cover every section and the most safety-critical rules, not only easy questions.
  • Keep the set separate from any examples used to design the system, to avoid tuning to the test.

Scoring three retrieval methods

python
import re
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer

sections = [p.strip() for p in re.split(r"\n(?=## )", PROTOCOL)[1:]]
ids = ["glucose", "kidney", "foot", "hypo", "eye"]

tests = [   # (question, id of the section that answers it)
    ("How often should HbA1c be checked after changing treatment?", "glucose"),
    ("What HbA1c target suits most adults?", "glucose"),
    ("My sugar dropped really low, what now?", "hypo"),
    ("What should I do about a hypo?", "hypo"),
    ("How many grams of carbohydrate treat a low?", "hypo"),
    ("Is the diabetes pill safe with weak kidneys?", "kidney"),
    ("When do we need to stop the biguanide?", "kidney"),
    ("How often is urine albumin checked?", "kidney"),
    ("Numb toes test", "foot"),
    ("When is a foot ulcer an emergency?", "foot"),
    ("How often are retinal photos needed?", "eye"),
    ("When should eye screening start?", "eye"),
]
questions = [q for q, _ in tests]
relevant = [ids.index(r) for _, r in tests]

vec = TfidfVectorizer(stop_words="english").fit(sections + questions)
keyword = cosine_similarity(vec.transform(questions), vec.transform(sections))
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
embedding = (model.encode(questions, normalize_embeddings=True)
             @ model.encode(sections, normalize_embeddings=True).T)
Run the protocol block from the chunking lesson first. Twelve labelled questions; TF-IDF and embedding scores.

Recall@k and MRR

python
def ranks(scores):
    """1-based rank of the correct section for each question."""
    order = np.argsort(-scores, axis=1)
    return np.array([list(order[i]).index(relevant[i]) + 1 for i in range(len(relevant))])

def report(name, scores):
    r = ranks(scores)
    print(f"{name:<10} recall@1 {np.mean(r <= 1):.2f}   recall@3 {np.mean(r <= 3):.2f}   "
          f"MRR {np.mean(1 / r):.2f}")

def reciprocal_rank_fusion(*score_sets, k=60):
    """Hybrid search: add 1 / (k + rank) from each method."""
    fused = np.zeros_like(score_sets[0])
    for s in score_sets:
        order = np.argsort(-s, axis=1)
        for i in range(s.shape[0]):
            for rank, j in enumerate(order[i], start=1):
                fused[i, j] += 1 / (k + rank)
    return fused

report("Keyword", keyword)
report("Embedding", embedding)
report("Hybrid", reciprocal_rank_fusion(keyword, embedding))

# Keyword    recall@1 0.83   recall@3 0.92   MRR 0.88
# Embedding  recall@1 0.92   recall@3 1.00   MRR 0.94
# Hybrid     recall@1 0.92   recall@3 0.92   MRR 0.94
Continuing from the previous block: metrics for keyword, embedding and a hybrid of both.
  • Embeddings put the right section first for 11 of 12 questions and within the top 3 for all of them.
  • The hybrid did not beat embeddings here; it was slightly worse at recall@3. Hybrid search often helps on larger, more varied collections with exact terms such as drug names, but whether it helps on your data is an empirical question.
  • With 12 questions, one question is 8 percentage points. These differences are far too small to be confident about; real test sets need many more questions.

Reading the misses

python
for name, s in [("Keyword", keyword), ("Embedding", embedding),
                ("Hybrid", reciprocal_rank_fusion(keyword, embedding))]:
    r = ranks(s)
    misses = [(questions[i], int(r[i])) for i in np.where(r > 1)[0]]
    print(name, "misses:", misses)

# Keyword misses: [('What should I do about a hypo?', 4), ('Is the diabetes pill safe with weak kidneys?', 3)]
# Embedding misses: [('What should I do about a hypo?', 3)]
# Hybrid misses: [('What should I do about a hypo?', 4)]
Continuing from the previous block: which questions each method failed to rank first.
  • Every method missed the 'hypo' question, pointing to the abbreviation problem from the previous lesson rather than to the choice of method. Fixing the cause (expansion) beats switching methods.
  • Keyword search also missed 'weak kidneys', a paraphrase, which embeddings handle.
  • Keep a log of misses with their cause; it becomes the to-do list for chunking, expansion and model choice.

Retrieval quality is a safety property

  • In a clinical assistant, the questions most likely to be missed are often those phrased in shorthand or by worried patients. Include them deliberately, and track recall on safety-critical rules separately.

Exercise

Build and use a retrieval test set to choose and improve a retrieval method.

Data: Synthetic protocol and labelled questions in the notebook (Created for this course; not clinical guidance)

  1. Reproduce the three methods' recall@1, recall@3 and MRR.
  2. Add twelve more questions, including three the protocol cannot answer, and decide how to score 'no answer' questions.
  3. Apply the abbreviation expansion from the embeddings lesson and re-score all methods.
  4. Bootstrap the question set (resample with replacement 1,000 times) to get confidence intervals for recall@1 and see whether the methods really differ.
  5. Implement nDCG@3 with graded labels (2 = answers fully, 1 = partly relevant) and compare rankings.

The runnable notebook and worked solution will be published soon. You can work through the steps with the code on this page in the meantime.

Key takeaways

  • Evaluate retrieval separately: the model cannot use evidence it never sees.
  • Label realistic questions, including shorthand and unanswerable ones, with clinicians.
  • Use recall@k and MRR to compare methods on the same questions.
  • Small test sets are noisy; read every miss and fix causes, not just methods.

Terms used: BM25, Embedding, Recall@k, Reciprocal rank fusion (hybrid search)

Related: Embeddings and vector stores, Diagnostic accuracy metrics, Medical LLMs and their benchmarks

FAQ

What is recall@k?

The proportion of test questions for which a correct passage appears among the top k retrieved results.

What is mean reciprocal rank?

The average, across questions, of 1 divided by the rank of the first correct result. It is 1 when the right passage is always ranked first.

How many test questions do I need?

Enough to cover every important topic and phrasing style, and enough that differences between methods are larger than chance. Tens is a start; hundreds are better for decisions.

What is hybrid search?

Combining keyword and embedding retrieval, for example with reciprocal rank fusion, so that exact-term matches and meaning-based matches both contribute.

Who should label the test questions?

People who know the domain and the documents, typically clinicians, ideally with a second reviewer for safety-critical questions.

Educational content only. This material teaches methods; it is not clinical advice. Code, models and examples here are not validated medical devices and must not be used to make decisions about real patients without proper clinical validation, governance and any required regulatory approval. Examples use synthetic or openly licensed de-identified data only. Never paste identifiable patient information into public AI tools. Mentions of models, tools or vendors are for identification, not endorsement.