Testland
Browse all skills & agents

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill elasticsearch-relevance-tests
View source

elasticsearch-relevance-tests

Per the Elasticsearch Rank Eval API (opens in new window), the _rank_eval endpoint "evaluates search result quality across typical queries using relevance metrics."

When to use

  • Search-driven product (e-commerce, docs site, internal portal) where relevance regression directly affects business outcomes.
  • Pre-deploy gate before changing analyzers, synonyms, boosts, query templates.
  • A/B baseline: capture today's NDCG/MRR before tuning so you can prove improvement (or detect regression).

Step 1 - Build the judgment list

A judgment is (query, doc_id, rating). Ratings: 0 = irrelevant, 1 = somewhat, 2 = relevant, 3 = highly relevant (4-point scale). Build judgments via:

SourceMethod
Query logs + click dataClick model (clicked = ≥1, multi-click = ≥2)
Quepid (open source)Interactive UI for judges to rate per-query results
SplainerDiagnose why a doc ranked where it did
Domain SMEsHigh-stakes queries; manual rating

Judgment list format (CSV is common):

query,doc_id,rating
"running shoes",sku-1234,3
"running shoes",sku-5678,2
"running shoes",sku-9999,0
"red dress",sku-2222,3

Step 2 - Define metrics for your domain

Per the Elasticsearch Rank Eval API (opens in new window):

MetricWhen to use
Precision@Kflat top-K accuracy; no graded weighting
Recall@Kcompleteness of the relevant set within top K
MRRone good answer suffices (navigational, Q&A)
DCG / NDCGgraded relevance, rank-discounted; default for graded judgments
ERRuser-stops-at-first-relevant; rank-decay sensitive

For e-commerce with graded judgments → NDCG@10 + MRR. For Q&A → MRR

  • Precision@1.

Step 3 - Submit a rank_eval request

Per the Elasticsearch Rank Eval API (opens in new window):

POST products/_rank_eval
{
  "requests": [
    {
      "id": "running_shoes_query",
      "request": {
        "query": { "match": { "name": "running shoes" } }
      },
      "ratings": [
        { "_index": "products", "_id": "sku-1234", "rating": 3 },
        { "_index": "products", "_id": "sku-5678", "rating": 2 },
        { "_index": "products", "_id": "sku-9999", "rating": 0 }
      ]
    },
    {
      "id": "red_dress_query",
      "request": { "query": { "match": { "name": "red dress" } } },
      "ratings": [
        { "_index": "products", "_id": "sku-2222", "rating": 3 }
      ]
    }
  ],
  "metric": {
    "dcg": { "k": 10, "normalize": true }
  }
}

Response shape:

{
  "metric_score": 0.84,
  "details": {
    "running_shoes_query": { "metric_score": 0.91, "unrated_docs": [...] },
    "red_dress_query": { "metric_score": 0.77, "unrated_docs": [...] }
  }
}

Step 4 - Wrap as a test

import requests, csv

def load_judgments(path):
    by_query = {}
    with open(path) as f:
        for row in csv.DictReader(f):
            by_query.setdefault(row["query"], []).append({
                "_index": "products",
                "_id": row["doc_id"],
                "rating": int(row["rating"]),
            })
    return by_query

def test_search_relevance_baseline():
    judgments = load_judgments("tests/judgments.csv")
    requests_payload = [
        {
            "id": q.replace(" ", "_"),
            "request": { "query": { "match": { "name": q } } },
            "ratings": ratings,
        }
        for q, ratings in judgments.items()
    ]
    body = {
        "requests": requests_payload,
        "metric": { "dcg": { "k": 10, "normalize": true } },
    }
    r = requests.post("http://localhost:9200/products/_rank_eval", json=body)
    result = r.json()

    # Baseline NDCG must not regress vs known-good
    assert result["metric_score"] >= 0.80, f"NDCG@10 regressed: {result['metric_score']}"

Step 5 - Per-query regression detection

Aggregate metric only catches large shifts. Track per-query:

def test_no_query_drops_more_than_10_percent():
    current = run_rank_eval()
    baseline = json.loads(Path("tests/baseline.json").read_text())

    for query_id, baseline_score in baseline["details"].items():
        current_score = current["details"][query_id]["metric_score"]
        delta = current_score - baseline_score["metric_score"]
        assert delta >= -0.10, \
            f"Query {query_id} dropped {delta:.2f} (was {baseline_score['metric_score']:.2f}, now {current_score:.2f})"

Advanced topics

Binary-metric relevant_rating_threshold config, snapshotting a reproducible test corpus for CI, and Quepid + Splainer judgment authoring live in references/rank-eval-guide.md.

Anti-patterns

Anti-patternWhy it failsFix
Use binary judgments onlyLoses graded info; NDCG degrades to Precision4-point scale (Step 1)
Rebuild judgments per test runBias from current rankingPinned judgment list (Step 1)
Track only aggregate NDCGHides per-query regressionsPer-query tracking (Step 5)
Test against changing indexBaselines move under your feetSnapshot corpus (advanced guide)
100% click-derived judgmentsClick bias to top results, position biasMix click + SME judgments

Limitations

  • Judgments are expensive; budget hundreds-to-thousands of query-doc pairs for a meaningful test set.
  • Click-derived judgments have position bias; correct using click models (cascade, dynamic Bayesian).
  • Rank Eval API doesn't natively support relevance graded > 4 or pairwise comparisons.
  • Synonyms, language analyzers, custom scoring matter - pin in CI.

References

  • Elasticsearch Rank Eval API (opens in new window) - request/response schema, metrics
  • Quepid (judgment authoring UI) - github.com/o19s/quepid
  • Splainer (debug per-doc ranking) - github.com/o19s/splainer-search
  • opensearch-relevance-tests - sister skill (compatible API)
  • vector-search-recall-tests - vector search analogue

Rank Eval guide - binary thresholds, snapshot corpora, judgment tooling

View source (opens in new window)

Rank Eval guide - binary thresholds, snapshot corpora, judgment tooling

Supplementary detail for elasticsearch-relevance-tests. The core _rank_eval workflow (judgments, metrics, request, test wrapper, per-query regression) stays in SKILL.md; this file holds the deeper configuration and tooling.

relevant_rating_threshold for binary metrics

Per the Elasticsearch Rank Eval API (opens in new window): Precision/Recall/MRR accept relevant_rating_threshold (default 1). For graded judgments:

"metric": {
  "precision": {
    "k": 10,
    "relevant_rating_threshold": 2,
    "ignore_unlabeled": false
  }
}

Rating >= 2 counted as "relevant"; below counted as "not relevant". The ignore_unlabeled flag controls whether unrated docs in results count against precision.

Reproducible test corpus

Snapshot the index state used for tests:

PUT _snapshot/test_repo/baseline_2026_05_06
{
  "indices": "products",
  "include_global_state": false
}

Restore for each CI run:

- name: Restore index snapshot
  run: |
    curl -X POST localhost:9200/_snapshot/test_repo/baseline_2026_05_06/_restore

Otherwise document changes (new docs, re-indexes) silently shift relevance baselines.

Quepid + Splainer integration

Quepid (opens in new window) (open source from OpenSource Connections) provides:

  • Web UI for judges to rate per-query results
  • CSV export -> SKILL.md Step 1 judgment list
  • "Try" tab to test query template changes against current judgments

Splainer (opens in new window) explains why a doc ranked where it did - invaluable for debugging unexpected results.

Related skills

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.

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.

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.