CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/elasticsearch-relevance-tests

Author search-engine relevance regression tests for Elasticsearch, OpenSearch, and Apache Solr. Core workflow on the Elasticsearch 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. Per-engine references cover the OpenSearch delta (Search Relevance Workbench, neural query DSL, hybrid BM25 + neural pipelines, ES-to-OS migration parity) and the Apache Solr delta (no _rank_eval: debugQuery score explain, LTR feature/model store REST, eDisMax qf/pf/mm tuning, external nDCG harness). Use before changing analyzers, synonyms, boosts, or query templates on an Elasticsearch, OpenSearch, or Solr index that serves user-facing search, so the NDCG / MRR baseline is captured first.

74

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

opensearch.mdreferences/

OpenSearch delta - Workbench, neural, and hybrid relevance tests

Per the OpenSearch search-relevance docs, _rank_eval is Elasticsearch-fork-compatible. The OpenSearch-specific surfaces worth testing: neural search, hybrid query, and the Search Relevance Workbench UI.

When this delta applies

  • Team standardized on OpenSearch (often AWS shops, often migrated from Elasticsearch ≤ 7.10).
  • Adopting OpenSearch's neural search or hybrid search features.
  • Migration test between Elasticsearch and OpenSearch - relevance parity must hold.

Step 1 - Reuse the judgment list format and _rank_eval

OpenSearch's _rank_eval accepts the same JSON as Elasticsearch's - endpoint + metrics identical per the OpenSearch search-relevance docs. The main skill's Step 1 judgment list (CSV (query, doc_id, rating) on the 4-point scale) and Step 3 request body are reusable verbatim against an OpenSearch cluster. One extra sourcing option exists here: the Search Relevance Workbench's pairwise judgment UI + bulk import (Step 3 below).

Step 2 - Search Relevance Workbench

Per the OpenSearch search-relevance docs, the Search Relevance Workbench plugin (UI in OpenSearch Dashboards) provides:

  • Query Set Management - group queries logically (e.g., "head queries", "long-tail queries").
  • Judgment management - pairwise UI for judges + bulk import (an extra judgment source beyond the main skill's Step 1 table).
  • Experiments - run query-template A/B against the same judgment list; compare metric scores side-by-side.

Workbench experiments are the easiest pre-tuning baseline-and-compare workflow.

Step 3 - Neural search query

OpenSearch supports k-NN vector search natively. Test setup:

PUT my_index
{
  "settings": { "index.knn": true },
  "mappings": {
    "properties": {
      "embedding": {
        "type": "knn_vector",
        "dimension": 768,
        "method": { "name": "hnsw", "engine": "lucene" }
      },
      "title": { "type": "text" }
    }
  }
}

Query:

POST my_index/_search
{
  "query": {
    "neural": {
      "embedding": {
        "query_text": "running shoes for marathon",
        "model_id": "<sentence-transformer-model>",
        "k": 10
      }
    }
  }
}

Test that neural results meet a recall@10 target against a held-out ground truth set:

def test_neural_recall_at_10():
    ground_truth = load_ground_truth("tests/marathon_queries.json")
    for query in ground_truth["queries"]:
        results = neural_search(query["text"], k=10)
        retrieved_ids = {r["_id"] for r in results}
        relevant_ids = set(query["relevant_ids"])
        recall = len(retrieved_ids & relevant_ids) / len(relevant_ids)
        assert recall >= 0.85, f"Recall {recall:.2f} below 0.85 for query: {query['text']}"

Pair with vector-search-recall-tests for HNSW parameter tuning.

Step 4 - Hybrid (BM25 + neural)

POST my_index/_search?search_pipeline=hybrid_pipeline
{
  "query": {
    "hybrid": {
      "queries": [
        { "match": { "title": "running shoes" } },
        { "neural": { "embedding": { "query_text": "running shoes", "k": 10 } } }
      ]
    }
  }
}

Hybrid weighting set up via search pipeline:

PUT _search/pipeline/hybrid_pipeline
{
  "phase_results_processors": [
    {
      "normalization-processor": {
        "normalization": { "technique": "min_max" },
        "combination": {
          "technique": "arithmetic_mean",
          "parameters": { "weights": [0.3, 0.7] }
        }
      }
    }
  ]
}

Test that hybrid weights matter:

def test_hybrid_weight_change_shifts_results():
    bm25_heavy_results = search_with_pipeline("hybrid_pipeline_03_07")  # 0.3 BM25 / 0.7 neural
    neural_heavy_results = search_with_pipeline("hybrid_pipeline_07_03")
    assert bm25_heavy_results != neural_heavy_results

Step 5 - Per-query metric regression (same as ES)

Identical to the main skill's Step 5 - run it against the OpenSearch cluster with its own pinned baseline file (e.g. tests/baseline-os.json).

Step 6 - ES → OS migration parity test

Run the same judgment list against both clusters; metric scores should be within ε:

def test_es_os_parity():
    es_score = rank_eval_against("http://es:9200/products", judgments)
    os_score = rank_eval_against("http://os:9200/products", judgments)
    delta = abs(es_score - os_score)
    assert delta < 0.05, f"ES vs OS NDCG diff {delta:.2f} > 0.05"

If the index settings (analyzers, mappings) are identical, scores should match. Differences point to subtle config drift.

Anti-patterns

Anti-patternWhy it failsFix
Test only BM25 path when neural enabledNeural regression slips silentlyStep 3 + Step 4
Use neural without warm-up for testsCold cache → flaky latency testsWarm before measuring
Set hybrid weights without testing both extremesSubtle BM25/neural balance change shipsStep 4
Skip migration parity testOS deviation from ES surfaces in prodStep 6
Trust default analyzers across ES/OSSubtle stemmer differencesPin analyzer config

Limitations

  • Workbench UI is OpenSearch-Dashboards-only; for pure-CLI workflows, drive judgments + experiments via API.
  • OpenSearch's neural search requires model deployment via the ML Commons plugin; setup steps differ from raw _rank_eval.
  • API surface evolves; verify per the current OpenSearch search-relevance docs for new fields.
  • Hybrid pipeline normalization techniques (min_max, l2) affect scores significantly; pin in CI.

References

  • OpenSearch search-relevance docs - workbench, neural, hybrid
  • The main elasticsearch-relevance-tests SKILL.md - compatible Rank Eval API + judgment format
  • vector-search-recall-tests - vector search precision/recall tooling

SKILL.md

tile.json