n4nAI

LlamaIndex retrieval evaluation: precision vs recall

A practical comparison of precision vs recall evaluation in LlamaIndex retrieval systems, with code examples and a decision framework for choosing the right metric.

n4n Team5 min read1,059 words

Audio narration

Coming soon — every post will get a voice note here.

If you’re running a llamaindex precision vs recall evaluation tutorial for the first time, you’ll quickly realize these metrics pull in opposite directions. Precision asks “of the chunks I retrieved, how many are actually relevant?” Recall asks “of all relevant chunks in the corpus, how many did I find?” Optimizing for one often degrades the other. This post breaks down how LlamaIndex measures both, where the trade-offs bite, and how to pick the right approach for your use case.

Understanding precision and recall in retrieval

Retrieval evaluation in LlamaIndex centers on the RetrieverEvaluator class, which compares a retriever’s output against a ground-truth dataset of (query, relevant_doc_ids) pairs. The mechanics are straightforward: for each query, you retrieve top-k nodes, then compute overlap with the expected set.

from llama_index.core.evaluation import RetrieverEvaluator
from llama_index.core.schema import NodeWithScore

evaluator = RetrieverEvaluator(
    retriever=my_retriever,
    metrics=["precision", "recall", "hit_rate", "mrr"],
)
results = await evaluator.aevaluate_dataset(dataset)

Precision@k = |retrieved ∩ relevant| / k. Recall@k = |retrieved ∩ relevant| / |relevant|. Hit rate@k = 1 if any relevant doc appears in top-k, else 0. MRR (mean reciprocal reward) weights the rank of the first relevant hit.

The key insight: these metrics assume your ground truth is complete. In practice, it rarely is. Annotators miss relevant chunks, corpora grow, and “relevance” is often subjective. Treat the numbers as directional, not absolute.

LlamaIndex evaluation framework overview

LlamaIndex provides two evaluation paths: the built-in RetrieverEvaluator for offline dataset-based evaluation, and ResponseEvaluator/FaithfulnessEvaluator for end-to-end RAG quality. For pure retrieval comparison, stick with RetrieverEvaluator.

You need a LabelledRetrieverDataset — essentially a JSONL file with queries and expected node IDs:

{"query": "How do I configure async in FastAPI?", "relevant_docs": ["node_12", "node_45"]}
{"query": "What's the default timeout for httpx?", "relevant_docs": ["node_7"]}

Generate this dataset manually for small corpora, or use LlamaIndex’s generate_question_context_pairs utility to bootstrap from documents (then human-review the output). The utility uses an LLM to synthesize questions from your chunks — useful for cold starts, but expect 30-50% noise.

from llama_index.core.evaluation import generate_question_context_pairs
from llama_index.core import SimpleDirectoryReader

documents = SimpleDirectoryReader("./docs").load_data()
dataset = generate_question_context_pairs(
    documents,
    llm=OpenAI(model="gpt-4o-mini"),
    num_questions_per_chunk=2,
)
dataset.save_json("eval_dataset.json")

Precision-focused evaluation

Precision optimization matters when false positives are costly: customer-facing search where irrelevant results erode trust, legal discovery where reviewing junk documents burns billable hours, or any pipeline where downstream LLM context windows are tight and you can’t afford noise.

LlamaIndex’s precision@k tells you how “clean” your top-k is. But precision alone is dangerous — a retriever that returns one perfect result and nine empty slots scores 10% precision@10 but 100% precision@1. Always pair it with hit rate or MRR.

# Precision-focused config: tight top-k, aggressive reranking
from llama_index.core.postprocessor import SentenceTransformerRerank

retriever = index.as_retriever(similarity_top_k=20)
reranker = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-6-v2",
    top_n=5,
)

# Pipeline: retrieve 20, rerank to 5
nodes = retriever.retrieve(query)
filtered = reranker.postprocess_nodes(nodes, query_str=query)

Reranking is the primary precision lever. Cross-encoders (like the MS MARCO model above) score query-document pairs jointly, catching semantic mismatches that bi-encoders miss. Cost: ~50-200ms per query per 20 candidates on CPU, more on GPU. Budget accordingly.

Precision-focused teams should also track false positive rate by category. Tag your eval queries by type (factual, procedural, comparative) and see where precision collapses. In our experience, procedural queries (“how do I…”) suffer most from keyword overlap false positives — e.g., retrieving “how to deploy” docs for “how to rollback” queries.

Recall-focused evaluation

Recall optimization matters when missing relevant information is costly: research assistants, compliance search, debugging aids where the “needle in haystack” must be found. The trade-off: you’ll feed more noise to the generator, increasing token spend and hallucination risk.

LlamaIndex’s recall@k caps at 1.0 once all relevant docs are retrieved. But the denominator — total relevant docs — is only as good as your ground truth. If annotators found 3 relevant chunks but 5 actually exist, your max measurable recall is 60%.

# Recall-focused config: wide retrieval, hybrid search
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.retrievers.bm25 import BM25Retriever

vector_retriever = index.as_retriever(similarity_top_k=50)
bm25_retriever = BM25Retriever.from_defaults(
    index=index,
    similarity_top_k=50,
)

fusion_retriever = QueryFusionRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    similarity_top_k=50,
    num_queries=4,  # query expansion
    mode="reciprocal_rerank",
)

Hybrid search (vector + BM25) is the main recall lever. BM25 catches exact keyword matches that embeddings miss — error codes, API names, version numbers. Query expansion (generating sub-queries via LLM) helps with underspecified queries. Both increase latency and token cost.

A practical pattern: retrieve 50-100 via hybrid, then rerank to 10-20 for the generator. This preserves recall while limiting context bloat. The reranker becomes your precision guardrail.

Comparison: precision vs recall configurations

Dimension Precision-optimized Recall-optimized
Top-k (initial) 10-20 50-100
Retrieval strategy Vector only, high similarity threshold Hybrid (vector + BM25) + query expansion
Reranking Mandatory, cross-encoder, top_n=3-5 Optional, lighter model, top_n=10-20
Latency (p50) 150-400ms 400-1200ms
Token cost/query Low (3-5 chunks × 512 tokens) Medium-high (10-20 chunks × 512 tokens)
Generator load Clean context, fewer hallucinations Noisy context, needs stronger prompt guardrails
Failure mode Misses relevant docs (false negatives) Floods generator with noise (false positives)
Eval metric priority Precision@k, MRR, faithfulness Recall@k, hit_rate, answer_relevancy
Best for Customer support, legal, high-stakes QA Research, debugging, exploratory search

Trade-offs in practice

The precision-recall curve isn’t theoretical — it shows up in three concrete places:

1. Chunking strategy. Smaller chunks (256 tokens) increase recall by isolating relevant passages, but hurt precision because context fragments across chunks. Larger chunks (1024+) improve precision per chunk but dilute relevance density. We’ve found 512 tokens with 50-token overlap to be a reasonable default for technical docs; tune per corpus.

2. Embedding model choice. General-purpose embeddings (text-embedding-3-small, bge-small-en) favor recall — they’re trained for broad semantic similarity. Domain-adapted embeddings (fine-tuned on your corpus) shift toward precision by learning your terminology. The cost: you need labeled pairs to fine-tune, and re-embedding the corpus on every model update.

3. Metadata filtering. Hard filters (date ranges, product versions, access control) improve both metrics by shrinking the search space before semantic matching. But they require reliable metadata extraction at ingestion time. If your metadata is 80% accurate, filters introduce 20% false negatives. Audit your metadata quality before relying on it.

Which to choose

Choose precision-optimized when:

  • Building customer-facing search or chat where irrelevant answers damage credibility
  • Downstream LLM calls are expensive (GPT-4, Claude Opus) and context budget is tight
  • Your domain has high vocabulary overlap between distinct topics (e.g., medical specialties sharing terminology)
  • You can invest in human-annotated eval sets and cross-encoder reranking

Choose recall-optimized when:

  • Building internal research tools where missing a document is worse than skimming extras
  • Queries are often underspecified or exploratory (“tell me about authentication”)
  • You have cheap generator tokens (local models, GPT-4o-mini) and can absorb noise
  • Your corpus has sparse keyword signals (error codes, config keys) that embeddings miss

Choose hybrid (the pragmatic default):

  • Retrieve 50 via hybrid (vector + BM25)
  • Rerank to 10 with a fast cross-encoder (ms-marco-MiniLM-L-6-v2)
  • Evaluate both precision@10 and recall@50 on a representative dataset
  • Adjust top-k and reranker threshold until the precision-recall trade-off matches your cost function
# The pragmatic default pipeline
from llama_index.core import QueryBundle
from llama_index.core.postprocessor import SentenceTransformerRerank

def retrieve_with_fallback(query: str, top_k: int = 10):
    # Wide hybrid retrieval
    nodes = fusion_retriever.retrieve(query)
    
    # Precision guardrail
    reranker = SentenceTransformerRerank(
        model="cross-encoder/ms-marco-MiniLM-L-6-v2",
        top_n=top_k,
    )
    return reranker.postprocess_nodes(nodes, query_str=query)

Run your eval dataset weekly. When precision@10 drops below 0.6 or recall@50 below 0.7, investigate: corpus drift, embedding degradation, or annotation gaps. The metrics are only as good as the discipline behind them.

Tagsllamaindexprecisionrecallevaluation

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All llamaindex retrieval evaluation & metrics posts →