ir.eval¶
Capability-discovery evaluation — measuring how well a corpus is found.
ir.eval answers the question that the rest of ir raises: *does retrieval
actually surface the right capability?* It is a thin, deterministic scoring layer
over the substrate already in place — ir.retrieve.search for ranking and
ef.evaluation for the metric mathematics — plus the genuinely new pieces a
capability-discovery eval needs that generic retrieval evaluation does not.
Design stance (retrieval-centric, reuse ef)¶
ir’s corpora are documents — skills, packages, reports — each a
name + description (+ body), joined on a stable artifact_id. They are not
a typed function-calling registry with argument signatures. So the eval that
fits is retrieval of the right artifact, not argument-dict scoring:
recall@k / NDCG@k / MRR / MAP — computed by
ef.evaluation’s pure, tested primitives (ef.evaluation.recall_at_k(), … ) and driver (ef.evaluation.evaluate_retrieval()).ir.evaldoes not reimplement them; it adapts anircorpus toef’s retriever contract and reads back aef.evaluation.RetrievalEvalReport.distractor-robustness curve — retrieval accuracy as the catalog grows (1 gold + N−1 distractors). The single most diagnostic capability-discovery metric, and one
efdoes not provide.failure-mode taxonomy — every gold case is classed
hit_rank_1/surfaced_low_rank/retrieval_missso a headline number decomposes into why it is what it is.abstention — an optional score-threshold proxy for “no artifact applies” cases. True abstention is a selection concern (a selector commits or refuses); here it is a retrieval-side diagnostic, clearly labelled as such.
The unit of evaluation is a DiscoveryCase: an intent (query) and
the gold artifact_ids that should answer it (empty gold = an
abstention case). Cases are plain data — save_cases() / load_cases()
round-trip them as JSONL so an evaluation set is a committable, reproducible
fixture (the corpora themselves are machine-specific and live, so freezing the
cases is what makes a run repeatable; validate_cases() flags gold ids that
have since drifted out of the corpus).
Case generation (back-translation from a corpus with name-masking) needs an
LLM and is deliberately out of scope here — this module scores a given set
of cases offline, with no network and no model download. (Dense scoring needs
only numpy; lexical / hybrid ranking additionally need the optional
vd dependency — a hybrid run that silently fell back to dense because vd
was absent is flagged in the report.)
Quick start:
import ir
from ir import eval as ev
corpus = ir.build(ir.CorpusSource.from_skills()) # or ir.open_corpus("skills")
cases = ev.load_cases("skills_eval.jsonl")
report = ev.evaluate_discovery(corpus, cases, mode="hybrid")
print(report) # NDCG@10 + taxonomy
# Just the BEIR-shaped metrics (pure ef), e.g. to A/B dense vs hybrid:
ev.retrieval_report(corpus, cases, mode="dense").primary # mean NDCG@10
ev.retrieval_report(corpus, cases, mode="hybrid").primary
Module Attributes
Rank cutoffs reported by default. |
|
Metrics reported by default — the |
|
Default ranking mode for the eval adapter (hybrid is |
|
Catalog sizes swept by |
|
Defaults for |
|
|
|
|
|
|
|
Default objective |
|
The scalar selection metrics a sweep can optimize / rank by. |
|
Weight on sensitivity (catch true positives) vs specificity (reject out-of-scope queries) when |
|
Graded relevance levels for the package-relevance harness, weakest -> strongest. |
|
Graded relevance gains per level — fed straight to |
Functions
|
Adapt an |
|
Calibrate the absolute abstention |
|
A/B (or N-way) regression gate over indexing / embedder configurations. |
|
The set of |
|
Derive the |
|
Convenience: a distractor curve over a source's scope from single-gold cases. |
|
Retrieval accuracy as the catalog grows — the needle-in-a-haystack curve. |
|
Score a corpus against |
|
Score one theme's named distractor / hard-positive sets with a single probe. |
|
Score a selector against |
|
Fraction of named distractors that appear in the top- |
|
Count of artifacts at each level for |
|
Load a persisted |
|
Read |
|
Read |
|
Return the |
|
Fraction of named hard-positives in the top- |
|
Score a corpus's retrieval against the cases — the pure- |
|
Write |
|
Write |
|
Tune the selector: score a grid of commit knobs against the cases. |
|
Build |
|
Build |
|
Gold ids a case references that are absent from the corpus. |
Classes
|
The outcome of |
|
One discovery probe: an intent and the artifact(s) that should answer it. |
|
The outcome of |
|
A calibrated absolute abstention floor for one |
|
The outcome of |
|
The two named diagnostic sets for one theme. |
|
One artifact's graded relevance to one or more themes. |
|
One cell of a selector sweep: a parameter setting and the report it scored. |
|
The outcome of |
|
The outcome of |
- class ir.eval.ComparisonReport(k, themes, labels, baseline, metrics, deltas)[source]¶
Bases:
objectThe outcome of
compare_indexings()— an N-way indexing/embedder bake-off.- k¶
the rank cutoff all
@kmetrics use.
- themes / labels
the themes scored and the corpus labels compared.
- baseline¶
the label every
regressions()check compares against (the first entry ofcorpora).
- metrics¶
{label: {theme: {"ndcg", "fp_rate"?, "hard_positive_recall"?}}}.
- deltas¶
{theme: {artifact_id: {"role", "by_label": {label: {"rank", "score"}}}}}for every named FP/FN id — the per-package effect.
- regressions(*, threshold=0, baseline=None)[source]¶
Named packages that got WORSE than
baseline— drives a pytest gate.For a
hard_positive, worse = its rank dropped (grew larger) by more thanthresholdpositions. For adistractor, worse = its rank rose (grew smaller, more prominent) by more thanthreshold. A package absent from a ranking is treated as rankinf(worst), so a vanished hard-positive and a newly-appearing distractor both register. Returns one dict per regressing(theme, id, label).
- ir.eval.DFLT_CALIB_SENSITIVITY_WEIGHT = 0.5¶
Weight on sensitivity (catch true positives) vs specificity (reject out-of-scope queries) when
calibrate_min_score()picks the abstention floor:objective = w·sensitivity + (1−w)·specificity.0.5is balanced (its argmax matches Youden’s J / balanced accuracy). Lower it to lean precision — abstain more readily — per ir_01 §3’s stance that a padded commit is the costlier failure for an agent surface than a dropped gold.
- ir.eval.DFLT_CURVE_MODE = 'dense'¶
Defaults for
distractor_robustness_curve()— deliberately notDFLT_MODE: the curve builds throwaway in-memory sub-corpora, so it favours the fast, always-offline dense + hashing path. Passmode="hybrid"/embedder="default"to measure the production configuration instead.
- ir.eval.DFLT_DISTRACTOR_SIZES = (1, 4, 8, 16, 32, 64, 128)¶
Catalog sizes swept by
distractor_robustness_curve()(RAG-MCP-style).
- ir.eval.DFLT_K_VALUES = (1, 5, 10)¶
Rank cutoffs reported by default.
- ir.eval.DFLT_METRICS = ('ndcg', 'recall', 'precision', 'mrr', 'map')¶
Metrics reported by default — the
ef.evaluationretrieval metric names.
- ir.eval.DFLT_MODE = 'hybrid'¶
Default ranking mode for the eval adapter (hybrid is
ir’s strongest).
- ir.eval.DFLT_SWEEP_MAX_K = (1, 2, 3, 5, 8)¶
max_kvalues swept bysweep_selector()(the commit-size cap).
- ir.eval.DFLT_SWEEP_MIN_SCORE = (None,)¶
min_scorefloors swept bysweep_selector(). Default(None,)— the absolute floor is mode-specific (cosine / RRF / BM25 scales differ), so it is left off unless the caller supplies calibrated values to sweep.
- ir.eval.DFLT_SWEEP_OBJECTIVE = 'selection_f1'¶
Default objective
SelectionSweep.best()optimizes — the standard precision/recall balance.mean_selected_sizeis the only metric minimized; every other is maximized (see_SWEEP_MINIMIZE).
- ir.eval.DFLT_SWEEP_REL = (0.4, 0.5, 0.6, 0.7, 0.8, 0.9)¶
relvalues swept bysweep_selector()(relative-to-top keep band).
- class ir.eval.DiscoveryCase(query, gold=(), corpus=None, source_id=None, metadata=<factory>)[source]¶
Bases:
objectOne discovery probe: an intent and the artifact(s) that should answer it.
- query¶
the user intent / search query.
- gold¶
the
artifact_ids that correctly answerquery(one for a single-answer case, several when capabilities overlap). An empty tuple marks an abstention case — no artifact applies.
- corpus¶
optional name of the corpus the case targets (provenance / for multi-corpus case files).
- source_id¶
optional
artifact_idthe query was generated from (back-translation provenance).
- metadata¶
free-form per-case metadata (difficulty, generator, …).
- class ir.eval.DiscoveryReport(retrieval, failure_classes, n_cases, n_gold, n_abstention, primary_k=10, abstention_accuracy=None, mode='hybrid', vd_degraded=False)[source]¶
Bases:
objectThe outcome of
evaluate_discovery().Bundles the standard retrieval metrics with the capability-discovery extras:
- retrieval¶
the
ef.evaluation.RetrievalEvalReportover the gold-bearing cases.
- failure_classes¶
counts per failure class (
hit_rank_1/surfaced_low_rank/retrieval_missfor gold cases;abstention_ok/false_action/abstention_unscoredfor abstention cases).
- n_cases / n_gold / n_abstention
case counts.
- primary_k¶
the cutoff
primaryreports at (default 10).
- abstention_accuracy¶
accuracy on the abstention slice when an
abstain_thresholdwas supplied, elseNone.
- mode¶
the ranking mode the report was produced with — recorded so a number is never mistaken for a different configuration’s.
- vd_degraded¶
True when
modewaslexical/hybridbutvdwas unavailable, so ranking silently fell back to dense (the scores are not the requested configuration’s).
- ir.eval.LEVEL_GAINS = {'core': 3.0, 'none': 0.0, 'strong': 2.0, 'tangential': 0.0, 'uses-tools': 1.0}¶
Graded relevance gains per level — fed straight to
ef.evaluation’s graded nDCG.uses-toolsis a weak positive (a package that merely uses the relevant tooling);tangential/noneare non-relevant (gain0). These gains are the knob that lets nDCG rank a core-first ordering strictly above a uses-tools-first one at identical recall.
- class ir.eval.MinScoreCalibration(corpus_name, mode, k, min_score, sensitivity, specificity, youden_j, balanced_accuracy, separable, sensitivity_weight, n_positive, n_abstention, n_retrieval_miss, positive_scores, abstention_scores, grid, reason, embedder_id=None, strategy='conservative', vd_degraded=False, rerank=None)[source]¶
Bases:
objectA calibrated absolute abstention floor for one
(corpus, mode).Produced by
calibrate_min_score().min_scoreis the floor to pass toir.select.select()/ir.discover()(discover(min_score="auto")loads it): the conservative selector abstains when even the top hit scores below it.Nonemeans no floor could be calibrated (one of the two query classes was missing — seereason); the selector then never abstains by absolute score, exactly as before.The quality numbers treat
top_score ≥ min_score → commitas a binary classifier over in-scope (label commit) vs out-of-scope (label abstain) queries:- min_score¶
the calibrated floor (
Noneif not calibratable).
- sensitivity¶
TPR — fraction of in-scope queries kept (
top ≥ floor).
- specificity¶
TNR — fraction of out-of-scope queries abstained (
top < floor).
- youden_j¶
sensitivity + specificity − 1(0 = no separation, 1 = perfect).
- balanced_accuracy¶
(sensitivity + specificity) / 2.
- separable¶
whether the two score distributions are perfectly separable (some floor reaches
youden_j == 1).
- sensitivity_weight¶
the
wused to pick the floor (seeDFLT_CALIB_SENSITIVITY_WEIGHT).
- n_positive¶
in-scope cases used (gold-bearing and gold-retrieved).
- n_abstention¶
out-of-scope (empty-gold) cases used.
- n_retrieval_miss¶
gold-bearing cases whose gold never reached the
kcandidates — excluded (a retrieval failure, not an abstention signal), mirroringevaluate_selection()’s conditioning.
- positive_scores / abstention_scores
{min, median, max}summaries of each class’s top-score distribution (the auditable evidence the floor sits between them).
- grid¶
every evaluated floor,
{min_score, sensitivity, specificity, youden_j, objective}— the full sweep behind the choice.
- reason¶
why
min_scoreis what it is ("calibrated","no_positive_cases","no_abstention_cases","no_cases").
- corpus_name / mode / k / embedder_id
the configuration scored — and the
embedder_idstamp lets a consumer detect a stale floor after a rebuild with a different embedder.
- vd_degraded¶
True when
modewaslexical/hybridbutvdwas unavailable, so ranking fell back to dense and the floor is on the dense scale, not the requested mode’s — recalibrate oncevdis installed.
- classmethod from_dict(d)[source]¶
Inverse of
to_dict()(tolerant of a persisted record’s defaults).- Return type:
- save(corpus)[source]¶
Persist this calibration on
corpus(keyed by mode); returns self.Stored in the corpus’s
calibrationview (not its buildconfig), so it survives reopen and is loaded bydiscover(min_score="auto"). The floor is machine-local (derived from a live corpus + embedder) and is never committed to source control.- Return type:
- class ir.eval.NamedSetReport(theme, k, fp_rate, hard_positive_recall, distractors_seen=(), hard_positives_missed=())[source]¶
Bases:
objectThe outcome of
evaluate_named_sets()for one theme.- theme / k
the theme and the rank cutoff scored.
- fp_rate¶
distractor false-positive rate at
k(lower is better).
- hard_positive_recall¶
recall over the hard-positive set at
k(higher is better).
- distractors_seen¶
the distractor ids actually in the top-
k— so thefp_rateis auditable back to specific packages.
- hard_positives_missed¶
the hard-positive ids NOT in the top-
k.
- class ir.eval.NamedSets(theme, distractors=(), hard_positives=())[source]¶
Bases:
objectThe two named diagnostic sets for one theme.
- theme¶
the theme these sets diagnose.
- distractors¶
ids that are NOT relevant (
none/tangential) yet rank prominently — every appearance in the top-kis a false positive.
- hard_positives¶
ids that ARE relevant (
core/strong) but hard to rank (e.g. thin-description packages) — recall on these is the headline gap a fix must close.
- class ir.eval.PackageRelevanceCase(artifact_id, labels=<factory>, evidence=<factory>, observed=<factory>, thin_description=False, is_distractor=<factory>, metadata=<factory>)[source]¶
Bases:
objectOne artifact’s graded relevance to one or more themes.
Unlike
DiscoveryCase(one query -> gold ids, flat-binary gold), a case here is one artifact carrying its graded label per theme. The per-theme probe text (the query each theme is scored with) is not on the case — it lives in the JSONL__meta__header (seesave_package_cases()), alongside the corpus signature (ir.eval_gen.corpus_signature()), so a frozen label set pins to the corpus snapshot it was judged against.- artifact_id¶
the package id (matches the corpus’
artifact_id).
- labels¶
{theme: level}with each level inRELEVANCE_LEVELS.
- evidence¶
{theme: short reason}— the human-auditable why.
- observed¶
{theme: score}the ranking gave this artifact in the frozen run (optional; used to derive the named distractor set).
- thin_description¶
True when the package had an empty/near-empty pyproject description (used to derive the hard-positive set).
- is_distractor¶
optional cached
{theme: bool}; when absent it is derived (seederive_named_sets()).
- metadata¶
free-form per-case metadata.
- classmethod from_dict(d)[source]¶
Inverse of
to_dict(); rejects any level outsideRELEVANCE_LEVELS.- Return type:
- gain(theme, *, gains={'core': 3.0, 'none': 0.0, 'strong': 2.0, 'tangential': 0.0, 'uses-tools': 1.0})[source]¶
The graded gain for
themeundergains(defaultLEVEL_GAINS).A level missing from
gains(e.g. a partial caller-supplied mapping) defaults to0.0— the non-relevant convention — rather than raising.- Return type:
- ir.eval.RELEVANCE_LEVELS = ('none', 'tangential', 'uses-tools', 'strong', 'core')¶
Graded relevance levels for the package-relevance harness, weakest -> strongest. The SSOT consumed by
LEVEL_GAINS,PackageRelevanceCase, andto_graded_qrels().
- ir.eval.SWEEP_METRICS = ('selection_f1', 'selection_precision', 'selection_recall', 'conditional_commit_rate', 'abstention_accuracy', 'mean_selected_size')¶
The scalar selection metrics a sweep can optimize / rank by.
- class ir.eval.SelectionGridPoint(max_k, rel, min_score, report)[source]¶
Bases:
objectOne cell of a selector sweep: a parameter setting and the report it scored.
- max_k / rel / min_score
the swept selection parameters at this cell.
- report¶
the
SelectionReportfor those parameters.
- class ir.eval.SelectionReport(selection_precision, selection_recall, selection_f1, conditional_commit_rate, n_gold_retrieved, abstention_accuracy, mean_selected_size, n_cases, n_gold, n_abstention, strategy='conservative', mode='hybrid', k=10)[source]¶
Bases:
objectThe outcome of
evaluate_selection()— selection quality, isolated.Where
evaluate_discovery()scores the ranking, this scores the commit: given the samekcandidates, what subset did the selector keep? The headline isconditional_commit_rate— accuracy of the selection decision conditioned on retrieval having surfaced the gold — which is the one number that separates a selection failure from a retrieval failure.Every selection-quality metric shares that conditioning: precision, recall and F1 are computed only over the
n_gold_retrievedcases whose gold reached thekcandidates, so a retrieval miss is never charged to the selector (it isevaluate_discovery()’s to report).- selection_precision¶
mean over committed gold-retrieved cases of
|selected ∩ gold| / |selected|(Noneif nothing was ever committed) — how clean the committed sets are.
- selection_recall¶
mean over gold-retrieved cases of
|selected ∩ gold| / |gold ∩ retrieved|— of the gold the selector was actually shown, how much it kept (denominator is the retrievable gold, so retrieval misses do not depress it).
- selection_f1¶
mean per-case F1 over gold-retrieved cases (an empty commit scores 0).
- conditional_commit_rate¶
among gold cases whose gold was in the
kcandidates, the fraction where the selector committed to ≥1 gold — the selection decision isolated from retrieval recall (Noneif retrieval never surfaced any gold).
- n_gold_retrieved¶
the shared denominator of every selection-quality metric above (cases whose gold reached the
kcandidates).
- abstention_accuracy¶
fraction of abstention cases (empty gold) the selector correctly committed nothing to (
Noneif none).
- mean_selected_size¶
mean committed-set size over gold-retrieved cases.
- n_cases / n_gold / n_abstention
case counts.
- strategy / mode / k
the configuration scored (so a number is never mistaken for a different setup’s).
- class ir.eval.SelectionSweep(points, objective, strategy, mode, k, n_cases, n_gold, n_abstention, n_gold_retrieved)[source]¶
Bases:
objectThe outcome of
sweep_selector()— a grid of selector settings scored.Every point shares the same cases, retrieval
mode/ candidate depthkandstrategy; they differ only in the swept selection knobs (max_k,rel,min_score).best()picks a single setting by an objective;frontier()returns the precision/recall Pareto front for the honest trade-off view;table()renders the whole grid.- points¶
one
SelectionGridPointper grid cell (grid order).
- strategy / mode / k
the shared configuration the grid was scored under.
- n_cases / n_gold / n_abstention / n_gold_retrieved
case counts (echoed from the reports; selection metrics share the
n_gold_retrieveddenominator, so it is surfaced here too).
- best(metric=None, *, minimize=None)[source]¶
The grid point that optimizes
metric(defaultobjective).Ties are broken toward the cheaper, tighter commit — smaller
mean_selected_size, then smallermax_k, then largerrel— encoding the capability-discovery stance that fewer, higher-precision commits beat more (ir_01 §3).minimizeis inferred from the metric (onlymean_selected_sizeminimizes) but can be forced.- Raises:
ValueError – if the sweep is empty or
metricis unknown.- Return type:
- frontier(*, x='selection_recall', y='selection_precision')[source]¶
The Pareto-optimal points trading
xoff againsty(both maximized).A point is on the frontier when no other point is at least as good on both axes and strictly better on one.
mean_selected_sizeon an axis is read as “smaller is better” automatically. Returned sorted byxascending.- Return type:
- ir.eval.as_doc_retriever(corpus, *, mode='hybrid', surfaces=None, **search_kw)[source]¶
Adapt an
ircorpus toef’s retriever contract.Returns a callable
retriever(query, *, limit=10) -> [artifact_id, …]— bare doc-id strings, best-ranked first, one per artifact. That is exactly whatef.evaluation.evaluate_retrieval()consumes (it maps a bare string straight to a document id), so the returned callable can be handed toefwith no further adaptation.- Parameters:
- Return type:
- ir.eval.calibrate_min_score(corpus, cases, *, mode='hybrid', k=10, surfaces=None, sensitivity_weight=0.5, floor_grid=None, persist=False, **search_kw)[source]¶
Calibrate the absolute abstention
min_scorefloor for onemode.Retrieves
kcandidates per case once (reusingevaluate_selection()’s retrieval pass), then treats floor-picking as a 1-D separation between the in-scope top scores (gold-bearing cases whose gold reached the candidates) and the out-of-scope top scores (empty-gold abstention cases). The floor that best separates them — byw·sensitivity + (1−w)·specificity— is the calibratedmin_score.casesmust contain both gold-bearing and abstention cases; with only one class the floor is undefined (min_score=None,reasonrecords which class was missing). Generate abstention cases withir.eval_gen(itsabstentionslice) — this module does not synthesize them.- Parameters:
cases (
Sequence[DiscoveryCase]) – mixedDiscoveryCases (gold-bearing + abstention).mode (
str) – ranking mode to calibrate (a floor is mode-specific).k (
int) – candidate depth retrieved before selection (match yourdiscoverk).surfaces (
Iterable[str] |None) – restrict retrieval to these surface kinds.sensitivity_weight (
float) –win the pick objective (seeDFLT_CALIB_SENSITIVITY_WEIGHT);0.5is balanced.floor_grid (
Sequence[float] |None) – candidate floors to evaluate; default derives them from the observed top scores (midpoints between adjacent values + the commit-all / abstain-all extremes), which is exact for a 1-D split.persist (
bool) – when True,MinScoreCalibration.save()the result on the corpus sodiscover(min_score="auto")will load it.**search_kw (
Any) – any otherir.retrieve.search()keyword.
- Return type:
- Returns:
- ir.eval.compare_indexings(corpora, cases, *, themes, probes, k=20, named_sets=None, mode='hybrid', rank_depth=1000, gains={'core': 3.0, 'none': 0.0, 'strong': 2.0, 'tangential': 0.0, 'uses-tools': 1.0}, surfaces=None, search_kw_by_label=None, **search_kw)[source]¶
A/B (or N-way) regression gate over indexing / embedder configurations.
corporamaps a label -> a built corpus (anefinstruction-tuned-embedder corpus or anirdeps-as-text corpus is just another entry — the harness is embedder-agnostic). For each(label, theme)it computes graded nDCG@k (viato_graded_qrels()+ef.evaluation.ndcg_at_k), the named-set FP-rate / hard-positive recall@k (whennamed_setsis given), and the rank+score of every named FP/FN id — so a change’s effect is quantified per package.probessupplies the per-theme query text (normally loaded from the JSONL__meta__viaread_package_meta()). The first label is the baseline thatComparisonReport.regressions()compares against. Each ranking is fetched once to depthrank_depth(deep enough that even a buried named id gets a real rank) and sliced atkfor the@kmetrics.search_kw_by_labeloverrides search settings per label, on top of the sharedmode/surfaces/**search_kw: map one corpus under several labels to compare search configurations rather than indexings, e.g.{"deps x2": {"surface_weights": {"deps": 2.0}}}. Anyir.retrieve.search()keyword exceptk(the depth isrank_depth) andper_artifact(rankings are per artifact).- Return type:
- ir.eval.derive_named_sets(cases, theme, *, observed_floor, gains={'core': 3.0, 'none': 0.0, 'strong': 2.0, 'tangential': 0.0, 'uses-tools': 1.0})[source]¶
Derive the
NamedSetsforthemedeterministically from labels.distractors= non-relevant (none/tangential) packages whose frozenobservedscore for the theme is at or aboveobserved_floor(they ranked high enough to pollute the top-k) — unless a case carries an explicit cachedis_distractor[theme], which then overrides the observed-floor rule.hard_positives= relevant (core/strong) packages flaggedthin_description(the ones a description+README index struggles to surface). Both lists are de-duplicated and sorted, so the result is a stable, committable artifact.- Return type:
- ir.eval.distractor_curve_from_cases(source, cases, **kwargs)[source]¶
Convenience: a distractor curve over a source’s scope from single-gold cases.
Probes are the single-gold cases (
(query, gold[0])); the source’sscopeis the distractor pool and itsindexing_strategyis reused unless astrategyis passed inkwargs.
- ir.eval.distractor_robustness_curve(scope, probes, *, strategy=None, embedder='light', mode='dense', sizes=(1, 4, 8, 16, 32, 64, 128), trials=20, seed=0, k=1)[source]¶
Retrieval accuracy as the catalog grows — the needle-in-a-haystack curve.
For each catalog size
Nand each(query, gold_id)probe, build manyN-artifact sub-corpora (the gold artifact plusN−1distractors sampled fromscope) and measure how often the gold artifact lands in the topk. The accuracy at eachNis averaged over the probes and overtrialsrandom distractor draws.N=1is the trivial anchor (gold alone) and is always 1.0; a steep decline asNgrows is the signature of retrieval that does not discriminate.Sub-corpora are built in-memory with the
"light"(numpy-only) embedder by default, so the curve is fast and offline; passembedder="default"(andmode="hybrid") to measure the production configuration.Sizes larger than the corpus are capped at
len(scope)(the largest catalog the pool can fill) and de-duplicated, so the returned keys are the catalog sizes actually built — the x-axis never claims anNlarger than the corpus could supply.- Parameters:
scope (
Mapping[str,Any]) – the full{artifact_id: raw}pool to draw the catalog from.probes (
Sequence[tuple[str,str]]) –(query, gold_id)pairs;gold_idmust be a key ofscope.strategy (
Any) – theIndexingStrategyto index with (defaultWholeText).embedder (
Any) – embedder spec for the sub-corpora.mode (
str) – ranking mode for the probe searches.sizes (
Sequence[int]) – the catalog sizes to sweep (capped atlen(scope), deduped).trials (
int) – random distractor draws averaged per size (a size that consumes the whole pool has no sampling freedom, so it runs a single trial).seed (
int) – base RNG seed (reproducible; varied per size).k (
int) – the top-kthe gold must reach to count as a hit.
- Return type:
- Returns:
{N: accuracy}for each achievableN(the requested sizes capped atlen(scope)and de-duplicated).
- ir.eval.evaluate_discovery(corpus, cases, *, k_values=(1, 5, 10), primary_k=10, metrics=('ndcg', 'recall', 'precision', 'mrr', 'map'), mode='hybrid', surfaces=None, abstain_threshold=None, **search_kw)[source]¶
Score a corpus against
cases— metrics, failure taxonomy, abstention.Runs retrieval once per case and, from each ranking, computes the
ef.evaluationmetric primitives (so the numbers match the pure-efpath) and the capability-discovery extras in one pass.- Parameters:
k_values (
Sequence[int]) – rank cutoffs to report (primary_kis always included).primary_k (
int) – the cutoff the headlineprimaryreports at.metrics (
Sequence[str]) – whichefmetrics to compute (ndcg/recall/precision/mrr/map).mode (
str) – ranking mode passed toir.retrieve.search().surfaces (
Iterable[str] |None) – restrict to these surface kinds.abstain_threshold (
float|None) – if given, an abstention case is scored correct when the top hit’s score is below this threshold (a retrieval-side proxy for “no artifact applies”); leaveNoneto only count abstention cases without scoring them.**search_kw (
Any) – any otherir.retrieve.search()keyword.
- Return type:
- Returns:
a
DiscoveryReport. If no case carries gold (an empty or all-abstention set) the retrieval metrics are omitted —report.primaryisNone— but the taxonomy and abstention counts are still returned (unlikeretrieval_report(), which raisesValueErroron that input).
- ir.eval.evaluate_named_sets(corpus, named_sets, theme=None, *, probe, mode='hybrid', k=10, surfaces=None, **search_kw)[source]¶
Score one theme’s named distractor / hard-positive sets with a single probe.
Runs the theme
probeonce, takes the per-artifact ranking, and reports the distractor false-positive rate and hard-positive recall atk— plus the distractor ids actually seen in the top-kand the hard-positives missed, so a number is always auditable back to specific packages.themedefaults tonamed_sets.theme(the only thing it labels); pass it only to override.- Return type:
- ir.eval.evaluate_selection(corpus, cases, *, strategy='conservative', mode='hybrid', k=10, surfaces=None, max_k=3, rel=0.9, gap_ratio=0.5, min_score=None, **search_kw)[source]¶
Score a selector against
cases— selection quality, isolated.Retrieves
kcandidates per case (the window the selector sees), commits withir.select.select(), and scores the commit. The key number,SelectionReport.conditional_commit_rate, conditions on retrieval having surfaced the gold among thosek— so a low value means the selector dropped a gold it was shown, not that retrieval missed it.kis the candidate window; hold it equal to thekused withevaluate_discovery()to compare the two stages on the same footing.- Parameters:
cases (
Sequence[DiscoveryCase]) – theDiscoveryCases (gold-bearing and/or abstention).strategy (
str) – selection strategy (seeir.select.select()).mode (
str) – ranking mode for retrieval.k (
int) – candidate depth retrieved before selection.surfaces (
Iterable[str] |None) – restrict retrieval to these surface kinds.max_k (
int) – selection parameters.rel (
float) – selection parameters.gap_ratio (
float) – selection parameters.**search_kw (
Any) – any otherir.retrieve.search()keyword.
- Return type:
- Returns:
- ir.eval.fp_rate_on_distractors(ranking, distractor_ids, *, k)[source]¶
Fraction of named distractors that appear in the top-
kofranking.A distractor surfacing in the committed top-
kis a false positive; this is the rate over the named set (0.0when the set is empty). Duplicate ids are counted once.- Return type:
- ir.eval.level_histogram(cases, theme)[source]¶
Count of artifacts at each level for
theme(every level represented).
- ir.eval.load_calibration(corpus, mode, *, rerank=None, rerank_text='surface', rerank_max_chars=2000)[source]¶
Load a persisted
MinScoreCalibrationfor(corpus, mode).rerank/rerank_textselect the floor calibrated for a reranked search (same values asir.search()); the default is the first-stage floor.Returns
Nonewhen no calibration has been stored for that mode. Does not validate the embedder stamp — that staleness check lives at thediscover(min_score="auto")boundary, which knows the live corpus’s embedder id.- Return type:
- ir.eval.load_cases(path)[source]¶
Read
DiscoveryCases from a JSONL file (skips a__meta__header).- Return type:
- ir.eval.load_package_cases(path)[source]¶
Read
PackageRelevanceCases from JSONL (skips a__meta__header).- Return type:
- ir.eval.read_package_meta(path)[source]¶
Return the
__meta__header dict from a package-cases JSONL ({}if absent).The header carries the per-theme probe text (
probes) and the corpus signature the labels were frozen against — everythingcompare_indexings()needs that is not on a per-artifact case.
- ir.eval.recall_on_hard_positives(ranking, hard_positive_ids, *, k)[source]¶
Fraction of named hard-positives in the top-
k(duplicate ids counted once).- Return type:
- ir.eval.retrieval_report(corpus, cases, *, k_values=(1, 5, 10), metrics=('ndcg', 'recall', 'precision', 'mrr', 'map'), mode='hybrid', surfaces=None, limit=None, **search_kw)[source]¶
Score a corpus’s retrieval against the cases — the pure-
efpath.A thin wrapper that builds the retriever adapter and hands it, with the cases’
(queries, qrels), toef.evaluation.evaluate_retrieval(). Returns itsef.evaluation.RetrievalEvalReport(.primaryis mean NDCG@10). Use this to A/B configurations — dense vs hybrid, with vs without a reranker — on the standard BEIR/MTEB metrics.For the richer report (taxonomy + abstention) use
evaluate_discovery().- Raises:
ValueError – if no case carries gold (
efrequires at least one positively-judged query).evaluate_discovery()instead tolerates an all-abstention set — the two paths differ only on that degenerate input.- Return type:
- ir.eval.save_cases(cases, path, *, meta=None)[source]¶
Write
casesto a JSONL file (one case per line).An optional
metamapping is written as a leading{"__meta__": …}line — the natural home for a corpus-version anchor that pins the cases to the corpus snapshot they were generated against.- Return type:
- ir.eval.save_package_cases(cases, path, *, meta=None)[source]¶
Write
PackageRelevanceCases to JSONL (mirrorssave_cases()).metais written as a leading{"__meta__": …}line — the home for the per-theme probe text (probes) and the corpus signature (ir.eval_gen.corpus_signature()) that pin the labels to a corpus snapshot.- Return type:
- ir.eval.sweep_selector(corpus, cases, *, strategy='conservative', mode='hybrid', k=10, surfaces=None, max_k_grid=(1, 2, 3, 5, 8), rel_grid=(0.4, 0.5, 0.6, 0.7, 0.8, 0.9), min_score_grid=(None,), gap_ratio=0.5, objective='selection_f1', **search_kw)[source]¶
Tune the selector: score a grid of commit knobs against the cases.
The selection defaults (
DFLT_MAX_K,DFLT_REL_THRESHOLD) were tuned on real corpora (see ir_06), not guessed. This sweepsmax_k × rel × min_scoreand scores each cell withevaluate_selection()’s exact metric, so the right defaults for a given corpus can be read off empirically (precision vs recall vs commit size) rather than assumed.Retrieval is run once per case (selector parameters do not change the ranking) and the cached candidates are reused across the whole grid, so a 6×5 grid costs one retrieval pass, not thirty.
- Parameters:
cases (
Sequence[DiscoveryCase]) – theDiscoveryCases to score against.strategy (
str) – the selection strategy held fixed across the grid (the swept knobs areconservative’s; a non-conservativestrategy that ignores some knobs simply yields duplicate rows).mode (
str) – ranking mode for retrieval.k (
int) – candidate depth retrieved before selection (the selector’s window).surfaces (
Iterable[str] |None) – restrict retrieval to these surface kinds.min_score_grid (
Sequence[float|None]) – the axes of the sweep.gap_ratio (
float) – held fixed (used only by thescore_gapstrategy).objective (
str) – the default metricSelectionSweep.best()optimizes.**search_kw (
Any) – any otherir.retrieve.search()keyword.
- Return type:
- Returns:
a
SelectionSweep(.best()/.frontier()/.table()/.to_dict()).
- ir.eval.to_graded_qrels(cases, theme, *, probe=None, gains={'core': 3.0, 'none': 0.0, 'strong': 2.0, 'tangential': 0.0, 'uses-tools': 1.0}, query_id=None)[source]¶
Build
ef’s graded(queries, qrels)for onetheme.Unlike
to_qrels()(which hardcodes grade1), every positive artifact is judged at its gradedLEVEL_GAINSvalue, so graded gains reachef.evaluation’s nDCG with zeroefchange. There is exactly one query per theme (the theme probe);qrelsmaps it to{artifact_id: gain}for every artifact with a positive gain. Non-positive (none/tangential) artifacts are omitted, matchingef’s judged-positive qrels convention.
- ir.eval.to_qrels(cases, *, grade=1.0)[source]¶
Build
ef’s(queries, qrels)from the gold-bearing cases.Abstention cases (empty
gold) are skipped —ef’s retrieval metrics require at least one positively-judged document per query. Query ids are derived from each case’s position so they stay stable across the gap left by skipped abstention cases.
- ir.eval.validate_cases(corpus, cases)[source]¶
Gold ids a case references that are absent from the corpus.
Returns
{case_index: [missing_artifact_id, …]}for every case whose gold has drifted out of the corpus (an empty dict means the cases are aligned). The eval corpora are live and machine-specific, so a committed case file can quietly fall out of sync; run this before trusting a report.