judgment-list-author
Bootstraps human-relevance judgment lists (query sets, grading scales, rater guidelines, inter-rater agreement, Quepid tooling, TREC-style pooling, and refresh cadence) that serve as ground truth for search-relevance test suites. Use when a team needs to create or refresh the judgment corpus before running NDCG / MRR / Recall@k evaluations.
Install with skills.sh (any agent)
npx skills add testland/qa --skill judgment-list-authorjudgment-list-author
Related skills (elasticsearch-relevance-tests, opensearch-relevance-tests, vector-search-recall-tests) all require a judgment list - a set of (query, document_id, grade) triples that define what "relevant" means for your product. No automated metric creates that corpus. This skill does.
Per TREC's pooling methodology (opens in new window), "NIST pools the individual results, judges the retrieved documents for correctness, and evaluates the results" - the judgment list is the non-automated step that all automated metrics depend on.
When to use
Step 1 - Select the query set (head / torso / tail sampling)
Not all queries deserve equal judgment effort. Sample across three tiers:
| Tier | Definition | Suggested count | Priority |
|---|---|---|---|
| Head | Top ~100-500 by traffic volume (covers ~80% of impressions) | 50-100 queries | Highest |
| Torso | Queries ranked ~500-5000 (navigational, faceted) | 100-200 queries | Medium |
| Tail | Low-frequency, long-tail queries | 50-100 queries | Lower |
Pull the sample from query logs, not from intuition. Use a 90-day window to avoid seasonal skew. Keep the raw log row for each sampled query - you will need it to assign traffic weight when computing weighted NDCG.
A starting corpus of roughly 200-400 queries across tiers gives sufficient coverage for NDCG-based gates. Per Quepid docs (opens in new window), "100 judgments (10 pages of 10 search results) is a good starting point and likely sufficient for most cases".
Step 2 - Choose the grading scale
Two options are widely used:
Binary (TREC standard)
Per TREC qrels format (opens in new window), the classic qrels file uses two values: 0 (not relevant) and 1 (relevant). Simple to collect; sufficient for Precision@k and Recall@k. Use binary when raters have low domain expertise or when the query intent is unambiguous (navigational queries).
Qrels file format (4 columns, per TREC qrels format (opens in new window)):
<topic_id> <iteration> <doc_id> <relevance>
1 0 doc-abc-123 0
1 0 doc-xyz-456 1The iteration column is "almost always zero and not used" per TREC. Unjudged documents are assumed irrelevant in evaluation.
Graded 0-3 (Quepid default)
Per Quepid judgment rating best practices (opens in new window), the 0-3 scale maps to these grades and customer-feeling cues (the source frames them for e-commerce; restate them in your own domain's terms):
| Grade | Label | Rater cue |
|---|---|---|
| 3 | Perfect | "This is what I am looking for! I'm going to buy one." |
| 2 | Good | "I like these! Now I can look more and decide which one to buy." |
| 1 | Fair | "These results aren't what I'm looking for." |
| 0 | Poor | "These results are terrible! Maybe I'll look somewhere else." |
Use graded when you need NDCG or DCG (metrics that reward highly relevant results at higher ranks). Required by elasticsearch-relevance-tests Step 1 which uses a 4-point (0-3) scale.
Step 3 - Write grading guidelines for raters
Grading guidelines prevent scale drift between raters and across time. A minimal guidelines document covers:
Store the guidelines in version control alongside the judgment file. When guidelines change, treat it as a new judgment round - old and new grades are not comparable.
Step 4 - Set up Quepid for rater workflow
Quepid (opens in new window) is the standard open-source tool for collaborative judgment authoring. It runs as a self-hosted Rails app and connects to Elasticsearch, OpenSearch, Solr, Algolia, and other backends.
Setup flow:
For teams without Quepid, a spreadsheet works for small sets (under 500 judgments): columns query_id, query_text, doc_id, grade, rater_id, notes. Convert to qrels format before feeding _rank_eval.
Step 5 - Measure inter-rater agreement (Cohen's kappa)
Have at least two independent raters judge the same 10-20% overlap set. Compute Cohen's kappa to verify the scale is being applied consistently.
Formula per Cohen's kappa, Wikipedia (opens in new window):
kappa = (p_o - p_e) / (1 - p_e)where p_o is observed agreement and p_e is expected chance agreement.
Interpretation thresholds (Landis and Koch, 1977, as cited in Cohen's kappa, Wikipedia (opens in new window)):
| Kappa | Agreement |
|---|---|
| < 0.20 | Slight - raters are guessing; revise guidelines |
| 0.21-0.40 | Fair - guidelines unclear; calibrate with examples |
| 0.41-0.60 | Moderate - acceptable for exploratory work |
| 0.61-0.80 | Substantial - good for production gates |
| 0.81-1.00 | Almost perfect - target for high-stakes domains |
For binary judgments, kappa < 0.60 means your guidelines are ambiguous. Rewrite the edge-case section, run a calibration session with raters, and re-judge the overlap set before proceeding.
from sklearn.metrics import cohen_kappa_score
rater_a = [3, 2, 1, 0, 3, 2, 2, 1, 0, 3]
rater_b = [3, 2, 0, 0, 3, 1, 2, 1, 0, 2]
kappa = cohen_kappa_score(rater_a, rater_b)
print(f"Cohen's kappa: {kappa:.3f}")
# 0.61-0.80 = substantial agreement; proceed to full rating roundWhen multiple raters cover non-overlapping document sets (common for large judgment pools), use Krippendorff's alpha instead - it handles missing data across raters.
Step 6 - Pool from multiple retrieval systems
When you have results from more than one system (e.g. current BM25 + candidate neural re-ranker), use TREC-style depth-k pooling to maximize judgment coverage.
Per TREC pooling, Wikipedia (opens in new window), in pooling "the top-ranked n documents from each contributing run are aggregated, and the resulting document set is judged completely."
Practical pooling:
def pool_results(system_results: dict[str, list[str]], depth: int = 10) -> set[str]:
"""
system_results: { system_name: [doc_id, ...] } top-depth per query
Returns the union of all doc IDs in the pool.
"""
pool = set()
for docs in system_results.values():
pool.update(docs[:depth])
return poolPool depth = 10 is standard for small collections (< 100k docs). Increase to 20-50 when you have > 3 candidate systems to avoid missing relevant documents that only appear in one system's lower ranks.
Documents outside the pool are unjudged. Per TREC qrels format (opens in new window), documents "not occurring in the qrels file were not judged by the human assessor and are assumed to be irrelevant in the evaluations used in TREC." This is conservative but consistent across systems.
Step 7 - Establish refresh cadence
Judgment lists go stale when the document corpus changes substantially. Define explicit refresh triggers:
| Trigger | Action |
|---|---|
| Index schema change (new field, new analyzer) | Full re-pool + partial re-judge (re-rate 20% overlap) |
| Embedding model upgrade | Full re-pool for all affected query tiers |
| Corpus grows > 20% | Re-pool; re-judge new documents only |
| A relevance-regression check flags > 30% unrated | Partial re-judge: new docs in the unrated set |
| New product category / language added | Add new query stratum; judge from scratch for that stratum |
As a minimum, run a lightweight staleness check monthly: for each query, count the unrated_docs fraction in _rank_eval results. If the average exceeds 20%, schedule a re-judging session.
Output format
The judgment list consumed by elasticsearch-relevance-tests and opensearch-relevance-tests is a JSON array:
[
{
"query_id": "q001",
"query": "running shoes",
"ratings": [
{ "doc_id": "doc-abc", "rating": 3 },
{ "doc_id": "doc-xyz", "rating": 1 },
{ "doc_id": "doc-mno", "rating": 0 }
]
}
]Or equivalently as a TREC qrels file (4 columns) for binary cases, which tools like trec_eval and the Elasticsearch _rank_eval API both accept after mapping doc_id to the index's _id field.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Judge only the current system's top-10 | Biases the corpus toward the incumbent; new systems retrieve different docs | Pool from all candidate systems (Step 6) |
| One rater, no kappa check | Silent scale drift; metrics become meaningless over time | Require 10-20% overlap + kappa >= 0.60 (Step 5) |
| Reuse judgments after embedding model upgrade | Vector space changed; doc rankings shift entirely | Re-pool and re-judge after any embedding change |
| Judge head queries only | Tail queries drive long-tail revenue; regressions go undetected | Sample across head/torso/tail (Step 1) |
| Treat unjudged docs as relevant | Inflates recall metrics artificially | Default unjudged to irrelevant per TREC convention |
| No version control on guidelines | Raters from two periods use different scales; grades are incompatible | Store guidelines in git; treat guideline changes as a new round |
Limitations
References
Related skills
elasticsearch-relevance-tests
Author Elasticsearch relevance regression tests using the Ranking Evaluation API (`POST {index}/_rank_eval`) - judgment lists (query + expected docs at ranks), per-query metrics (Precision@K, Recall@K, MRR, DCG, ERR), reproducible test corpora; pair with Quepid + Splainer for interactive judgment authoring. Use before changing analyzers, synonyms, boosts, or query templates on an Elasticsearch index that serves user-facing search, so the NDCG / MRR baseline is captured first.
hybrid-search-eval-author
Evaluates hybrid retrieval pipelines (BM25 + vector + reranker) end-to-end: authors ground-truth judgment sets, computes nDCG@k and MRR over fused results, measures the lift from Reciprocal Rank Fusion vs weighted fusion vs single-stage retrieval, and quantifies reranker (cross-encoder/Cohere/bge) impact. Use when a production system combines lexical and semantic retrieval and you need a numeric relevance baseline, fusion-strategy comparison, or evidence that a reranker is earning its latency cost.
opensearch-relevance-tests
Author OpenSearch relevance tests with Search Relevance Workbench (judgment lists, query sets, experiments), `_rank_eval` API (Elasticsearch-fork-compatible), and hybrid BM25 + neural ranking eval. Reuse Elasticsearch judgment list format; document the differences (neural search query DSL, hybrid weighting via `neural_query_enricher`). Use when an OpenSearch index turns on neural or hybrid search, or when a move off Elasticsearch has to prove relevance parity between the two clusters.
solr-relevance-tests
Tests Apache Solr search relevance by querying a test core, asserting ranking and score expectations, uploading LTR feature stores and models via the `/schema/feature-store` and `/schema/model-store` REST APIs, using `debugQuery` for per-document score explain, tuning eDisMax parameters (`qf`, `pf`, `mm`, `bq`), and computing judgment-driven nDCG checks against pinned corpora. Use when the search stack runs Apache Solr (enterprise, SolrCloud, or embedded) and you need a pre-deploy relevance gate or LTR model verification.
vector-search-recall-tests
Vector search benchmarking - recall@k vs latency tradeoffs, ground-truth construction via brute-force, HNSW tuning (M / ef_construct / ef per Qdrant docs), embedding-model-upgrade drift detection. Use ANN-Benchmarks framework for cross-engine comparison; per-engine clients (Qdrant, Weaviate, pgvector, Pinecone, Elasticsearch k-NN, Milvus) for in-product tests. Use when HNSW / IVF parameters are being tuned or an embedding model is swapped, and recall@k on the existing corpus has never been measured against brute-force ground truth.