ef.evaluation¶
Evaluation hookpoints — measuring retrieval and RAG quality.
Phase 7 makes an indexed ef corpus measurable. It stays a facade: it
computes the standard, deterministic metrics and provides the standard data
shapes, but it never synthesizes an answer and never requires an LLM (the §6
boundary). LLM-judged metrics are reached through a thin Ragas bridge.
Two journeys (ef_use_cases.md §F7–F8), two evaluation conventions
(ef_design_notes.md §6):
Retrieval evaluation —
evaluate_retrieval(), BEIR/MTEB-shaped. The inputs are the BEIR triple — a corpus, a set ofqueriesandqrels(graded relevance judgements) — and the primary metric is NDCG@10. The metric primitives (ndcg_at_k(),recall_at_k(), …) are pure functions, usable on their own.read_beir()/write_beir()move the triple to and from the on-diskcorpus.jsonl/queries.jsonl/qrels.tsvformat.RAG evaluation —
evaluate_rag(), RagasSingleTurnSample-shaped. A sample is(user_input, response, retrieved_contexts, reference);RagSamplenames that shape.evaluate_rag()computes onlydeterministic, reference-based lexical metrics (
exact_match(),token_f1(),context_recall(),context_precision()) — no LLM.For the LLM-judged metrics (faithfulness, answer relevancy, …) hand the samples to
as_ragas_dataset()and run Ragas with your own LLM.
Example — score a retriever against a tiny BEIR-shaped triple:
>>> qrels = {'q1': {'d1': 1.0, 'd2': 1.0}, 'q2': {'d3': 1.0}}
>>> queries = {'q1': 'first query', 'q2': 'second query'}
>>> rankings = {'first query': ['d1', 'd9', 'd2'], 'second query': ['d9', 'd3']}
>>> def retriever(query, *, limit=10):
... return rankings[query][:limit]
>>> report = evaluate_retrieval(retriever, qrels, queries, k_values=(1, 3))
>>> report.metrics['recall@3']
1.0
>>> round(report.primary_at(1), 3) # NDCG@1, averaged over the two queries
0.5
Module Attributes
Graded relevance judgements — |
|
The retrieval metrics |
Functions
|
Discounted cumulative gain of |
|
Normalized DCG at cutoff |
|
Fraction of the relevant docs that appear in the top |
|
Fraction of the top |
|
Reciprocal of the rank of the first relevant result ( |
|
Average precision of |
|
Score a retriever against a BEIR-shaped evaluation set. |
|
Load a BEIR-format dataset from disk into the in-memory evaluation triple. |
|
Write an evaluation triple to disk in BEIR format — the inverse of |
|
|
|
Token-overlap F1 of |
|
Lexical recall of the |
|
Fraction of the retrieved contexts that lexically overlap the |
|
Score RAG samples with deterministic, reference-based lexical metrics. |
|
Convert |
Classes
One end-to-end RAG interaction — the unit |
|
|
The outcome of |
|
The outcome of |
- ef.evaluation.Qrels¶
Graded relevance judgements —
{query_id: {doc_id: relevance}}. Arelevanceof0means judged non-relevant;> 0means relevant (graded judgements feedndcg_at_k(), binary relevance the rest). This is the in-memory form of a BEIRqrels.tsv.
- ef.evaluation.RETRIEVAL_METRICS: dict[str, Callable[[Sequence[str], Mapping[str, float], int], float]] = {'map': <function average_precision>, 'mrr': <function reciprocal_rank>, 'ndcg': <function ndcg_at_k>, 'precision': <function precision_at_k>, 'recall': <function recall_at_k>}¶
The retrieval metrics
evaluate_retrieval()can compute, each keyed by the short name used in themetrics=argument and in report keys. Public so downstream packages (e.g.ir.eval) can consume this single source of truth — the name→function registry and the set of valid metric names — instead of hand-mirroring it.
- class ef.evaluation.RagEvalReport(metrics, coverage, n_samples)[source]¶
Bases:
objectThe outcome of
evaluate_rag()— lexical RAG metrics over a sample set.metricsmaps each metric to its mean over the samples it applied to;coveragerecords how many samples that was (a metric needing areferenceskips samples without one).n_samplesis the total scored.>>> report = RagEvalReport( ... metrics={'exact_match': 0.5}, coverage={'exact_match': 4}, n_samples=4) >>> report.metrics['exact_match'] 0.5
- class ef.evaluation.RagSample[source]¶
Bases:
TypedDictOne end-to-end RAG interaction — the unit
evaluate_rag()scores.The field names match Ragas’
SingleTurnSampleexactly, so aRagSampleis the interchange shapeapp_ef/srag/raglabproduce andas_ragas_dataset()forwards unchanged.efitself fills onlyretrieved_contexts(fromretrieve());responseis the caller’s LLM output —efdoes not synthesize it.- Keys:
user_input: the user’s question. response: the generated answer (the caller’s LLM produced it). retrieved_contexts: the context passages handed to the LLM — what
retrieve()returned, as text.reference: the ground-truth answer, if known. reference_contexts: the ground-truth context passages, if known.
- class ef.evaluation.RetrievalEvalReport(metrics, per_query, n_queries, k_values)[source]¶
Bases:
objectThe outcome of
evaluate_retrieval()— metrics over a query set.metricsmaps a"<name>@<k>"key (e.g."ndcg@10") to the mean of that metric over every scored query;per_querykeeps the same keys per query for drill-down.n_queriesis how many queries were actually scored (a query with no positive judgement is skipped).>>> report = RetrievalEvalReport( ... metrics={'ndcg@10': 0.83}, per_query={}, n_queries=12, k_values=(10,)) >>> report.primary 0.83
- ef.evaluation.as_ragas_dataset(samples)[source]¶
Convert
RagSamples into a RagasEvaluationDataset.The hookpoint for the LLM-judged RAG metrics
efdeliberately does not compute itself (faithfulness, answer relevancy, …).RagSamplealready mirrors Ragas’SingleTurnSamplefield-for-field, so this is a thin, lossless adapter. Run the evaluation with your own LLM:from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy dataset = as_ragas_dataset(samples) result = evaluate(dataset, metrics=[faithfulness, answer_relevancy])
- ef.evaluation.average_precision(ranked, relevant, k=None)[source]¶
Average precision of
ranked— the per-query term of MAP.The mean of the precision measured at each relevant result’s rank.
koptionally caps the ranking.- Return type:
>>> average_precision(['d1', 'x', 'd2'], {'d1': 1.0, 'd2': 1.0}) 0.8333333333333333
- ef.evaluation.context_precision(reference, retrieved_contexts)[source]¶
Fraction of the retrieved contexts that lexically overlap the
reference.A deterministic proxy for retrieval precision — a context counts as useful if it shares at least one (normalized) token with the reference answer.
- Return type:
>>> context_precision('paris', ['Paris is in France.', 'Unrelated text.']) 0.5
- ef.evaluation.context_recall(reference, retrieved_contexts)[source]¶
Lexical recall of the
referencetokens by the retrieved contexts.The fraction of the (normalized) reference’s distinct tokens that appear anywhere in
retrieved_contexts— a deterministic proxy for “did retrieval surface the information the answer needs”. For the LLM-judged context recall, useas_ragas_dataset()and Ragas.- Return type:
>>> context_recall('paris france', ['Paris is the capital of France.']) 1.0 >>> context_recall('paris france', ['London is in England.']) 0.0
- ef.evaluation.dcg_at_k(ranked, relevant, k)[source]¶
Discounted cumulative gain of
rankedat cutoffk.sum(grade_i / log2(i + 2))over the topkresults (i0-indexed), withgrade_ithe relevance of thei-th doc (0if unjudged) — the linear-gain DCG that BEIR /pytrec_evaluse.- Return type:
>>> round(dcg_at_k(['d1', 'x', 'd2'], {'d1': 1.0, 'd2': 1.0}, 3), 4) 1.5
- ef.evaluation.evaluate_rag(samples, *, metrics=('exact_match', 'token_f1', 'context_recall', 'context_precision', 'non_empty_rate'))[source]¶
Score RAG samples with deterministic, reference-based lexical metrics.
Each sample is a
RagSample—(user_input, response, retrieved_contexts, reference).evaluate_ragcomputes only metrics that need no LLM:exact_match(),token_f1(),context_recall(),context_precision()andnon_empty_rate(the share of samples that retrieved any context). This keepsefa facade — it never synthesizes an answer and never calls a model (§6).For the LLM-judged metrics — faithfulness, answer relevancy, … — pass the same samples to
as_ragas_dataset()and run Ragas with your own LLM.- Parameters:
samples (
Iterable[RagSample|Mapping[str,Any]]) – an iterable ofRagSamples (or plain mappings of the same shape).metrics (
Sequence[str]) – which metrics to compute — any ofexact_match/token_f1/context_recall/context_precision/non_empty_rate. The reference-based ones skip samples that carry noreference.
- Return type:
- Returns:
- Raises:
ValueError – for an unknown metric name, or if
samplesis empty.
>>> samples = [ ... {'user_input': 'capital of France?', 'response': 'Paris', ... 'reference': 'Paris', ... 'retrieved_contexts': ['Paris is the capital of France.']}, ... {'user_input': '2 + 2?', 'response': 'five', 'reference': 'four', ... 'retrieved_contexts': []}, ... ] >>> report = evaluate_rag(samples) >>> report.metrics['exact_match'] 0.5 >>> report.metrics['non_empty_rate'] 0.5
- ef.evaluation.evaluate_retrieval(retriever, qrels, queries, *, k_values=(1, 5, 10, 100), metrics=('ndcg', 'recall', 'precision', 'mrr', 'map'), limit=None)[source]¶
Score a retriever against a BEIR-shaped evaluation set.
For every query judged in
qrels, theretrieveris run, its results are mapped back to source document ids (_result_doc_id(), de-duplicated to the best rank per source), and each requested metric is computed at each cutoff ink_values. The report holds the mean of each"<metric>@<k>"over all scored queries — primary metric NDCG@10.The natural pairing with
ingest():corpus, queries, qrels = read_beir("path/to/beir/dataset") index = ingest(corpus, embedder="st:all-MiniLM-L6-v2") report = evaluate_retrieval(index, qrels, queries) report.primary # mean NDCG@10- Parameters:
retriever (
Any) – what answers a query — aSearchableCorpus, aSourceManager, or any callablequery -> ranked results. Results may beSearchHits,Segments or bare doc-id strings.qrels (
Mapping[str,Mapping[str,float]]) – the relevance judgements —{query_id: {doc_id: relevance}}. Queries with no positive judgement are skipped.queries (
Mapping[str,str]) – the query texts —{query_id: query_text}. A judged query absent here cannot be run and is skipped.metrics (
Sequence[str]) – which metrics to compute — any ofndcg/recall/precision/mrr/map.limit (
int|None) – how many results to retrieve per query.Noneusesmax(k_values); raise it when sources segment into many pieces (de-duplication to source level can otherwise shorten the ranking).
- Return type:
- Returns:
- Raises:
ValueError – for an unknown metric name, or if no query can be scored.
>>> qrels = {'q1': {'d1': 1.0}, 'q2': {'d2': 1.0}} >>> queries = {'q1': 'alpha', 'q2': 'beta'} >>> def retriever(query, *, limit=10): ... return {'alpha': ['d1', 'd2'], 'beta': ['d1', 'd2']}[query][:limit] >>> report = evaluate_retrieval(retriever, qrels, queries, k_values=(1, 2)) >>> report.metrics['recall@2'], report.n_queries (1.0, 2)
- ef.evaluation.exact_match(response, reference)[source]¶
1.0ifresponseequalsreferenceafter normalization, else0.0.- Return type:
>>> exact_match('The Eiffel Tower', 'eiffel tower') 1.0 >>> exact_match('Paris', 'London') 0.0
- ef.evaluation.ndcg_at_k(ranked, relevant, k)[source]¶
Normalized DCG at cutoff
k—ef’s primary retrieval metric.dcg_at_k()divided by the ideal DCG (the DCG of the best possible ranking of the judged docs).1.0is a perfect ranking;0.0when no relevant doc was judged.- Return type:
>>> ndcg_at_k(['d1', 'd2'], {'d1': 1.0, 'd2': 1.0}, 2) # perfect order 1.0 >>> round(ndcg_at_k(['x', 'd1'], {'d1': 1.0}, 2), 4) # relevant doc at rank 2 0.6309
- ef.evaluation.precision_at_k(ranked, relevant, k)[source]¶
Fraction of the top
kresults that are relevant.- Return type:
>>> precision_at_k(['d1', 'x', 'd2'], {'d1': 1.0, 'd2': 1.0}, 2) 0.5
- ef.evaluation.read_beir(directory, *, qrels=None)[source]¶
Load a BEIR-format dataset from disk into the in-memory evaluation triple.
Reads
corpus.jsonl(records{"_id", "title", "text"}),queries.jsonl(records{"_id", "text"}) and aqrelsTSV (query-id,corpus-id,scorecolumns). The corpus text is the record’stitleandtextjoined — the BEIR convention.- Parameters:
- Return type:
tuple[dict[str,str],dict[str,str],dict[str,dict[str,float]]]- Returns:
a
(corpus, queries, qrels)triple —corpusandqueriesare{id: text}dicts,qrelsis{query_id: {doc_id: relevance}}. Feed it straight toevaluate_retrieval()(after indexingcorpuswithingest()).- Raises:
FileNotFoundError – if
corpus.jsonl,queries.jsonlor the qrels file is missing.
- ef.evaluation.recall_at_k(ranked, relevant, k)[source]¶
Fraction of the relevant docs that appear in the top
kresults.- Return type:
>>> recall_at_k(['d1', 'x', 'd2'], {'d1': 1.0, 'd2': 1.0}, 2) 0.5 >>> recall_at_k(['d1', 'x', 'd2'], {'d1': 1.0, 'd2': 1.0}, 3) 1.0
- ef.evaluation.reciprocal_rank(ranked, relevant, k=None)[source]¶
Reciprocal of the rank of the first relevant result (
0if none).Averaged over queries this is MRR.
koptionally caps how far down the ranking to look.- Return type:
>>> reciprocal_rank(['x', 'd1', 'd2'], {'d1': 1.0}) 0.5 >>> reciprocal_rank(['x', 'd1'], {'d1': 1.0}, k=1) # relevant doc past the cutoff 0.0
- ef.evaluation.token_f1(response, reference)[source]¶
Token-overlap F1 of
responseagainstreference(the SQuAD F1).The harmonic mean of token precision and recall over the normalized token bags — a graded credit where
exact_match()is all-or-nothing.- Return type:
>>> token_f1('the quick brown fox', 'a quick brown fox') 1.0 >>> round(token_f1('quick brown fox', 'quick brown dog'), 4) 0.6667
- ef.evaluation.write_beir(directory, corpus, queries, qrels)[source]¶
Write an evaluation triple to disk in BEIR format — the inverse of
read_beir().Creates
directory(and parents) and writescorpus.jsonl,queries.jsonlandqrels.tsv. Use it to snapshot anefcorpus as a reusable benchmark (ef_use_cases.md§H4).- Parameters:
- Return type: