Testland
Browse all skills & agents

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.

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."

Engine routing

This skill is the single home for term-based engine relevance testing. The core _rank_eval workflow below is authored against Elasticsearch; the per-engine deltas live in references:

EngineWhereDelta
Elasticsearchthis SKILL.mdBuilt-in _rank_eval; the canonical workflow
OpenSearchreferences/opensearch.md_rank_eval-compatible fork; Search Relevance Workbench, neural query DSL, hybrid BM25 + neural pipelines, ES-to-OS migration parity
Apache Solrreferences/solr.mdNo _rank_eval; debugQuery explain, LTR feature/model store REST, eDisMax tuning, external nDCG harness

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

OpenSearch delta - Workbench, neural, and hybrid relevance tests

View source (opens in new window)

OpenSearch delta - Workbench, neural, and hybrid relevance tests

Per the OpenSearch search-relevance docs (opens in new window), _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 (opens in new window). 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 (opens in new window), 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 (opens in new window) for new fields.
  • Hybrid pipeline normalization techniques (min_max, l2) affect scores significantly; pin in CI.

References

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.

Apache Solr delta - LTR, debugQuery, and external nDCG

View source (opens in new window)

Apache Solr delta - LTR, debugQuery, and external nDCG

Apache Solr is the primary Elasticsearch/OpenSearch alternative in enterprise search. Unlike the ES/OS _rank_eval endpoint, Solr has no single built-in IR-metrics endpoint: relevance testing is assembled from debugQuery score explain, the Learning To Rank (LTR) contrib module, eDisMax tuning, and a test harness that computes nDCG externally. This reference covers that assembly.

Engine differentiation within this skill:

EngineWhereDifferentiation axis
Elasticsearchmain SKILL.mdBuilt-in _rank_eval endpoint; no LTR store API
OpenSearchopensearch.md (opens in new window)ES-fork + neural search; different LTR surface
Apache Solrthis referencedebugQuery explain, LTR feature/model store REST, eDisMax qf/pf/mm tuning

When this delta applies

  • Production stack runs Apache Solr (standalone or SolrCloud) and you need a pre-deploy gate before changing query config, schema, or analyzers.
  • A trained LTR model (LambdaMART, LinearModel, NeuralNetwork) must be verified to improve or preserve nDCG before promotion.
  • eDisMax field weights (qf) or phrase boosts (pf) were edited and you need to confirm no per-query regression.
  • Score explain output shows an unexpected ranking and you need to reproduce
    • assert it in a test.

Step 1 - Start a test core

Per the Solr CLI reference (opens in new window):

bin/solr start -p 8983
bin/solr create -c test_products -d _default

Index a snapshot of your production corpus (or a representative subset). Freeze the index before running any judgment-driven tests - new documents shift relevance baselines silently.

Step 2 - Build the judgment list

Same CSV format and 4-point scale as the main skill's Step 1 (0 = irrelevant, 1 = somewhat, 2 = relevant, 3 = highly relevant). Collect judgments via query logs + click data, domain SME review, or Quepid (opens in new window) (open source judgment UI with Solr support).

Step 3 - Query and collect ranked results

Solr has no _rank_eval equivalent. Call the query endpoint and collect ranked doc IDs per query:

import requests

SOLR = "http://localhost:8983/solr/test_products"

def ranked_ids(query: str, rows: int = 10) -> list[str]:
    r = requests.get(f"{SOLR}/select", params={
        "q": query,
        "defType": "edismax",
        "qf": "title^5.0 description^1.0",
        "rows": rows,
        "fl": "id,score",
    })
    return [doc["id"] for doc in r.json()["response"]["docs"]]

Step 4 - Compute nDCG externally

import csv, math

def load_judgments(path: str) -> dict[str, dict[str, int]]:
    j: dict[str, dict[str, int]] = {}
    with open(path) as f:
        for row in csv.DictReader(f):
            j.setdefault(row["query"], {})[row["doc_id"]] = int(row["rating"])
    return j

def dcg(ratings: list[int]) -> float:
    return sum(r / math.log2(i + 2) for i, r in enumerate(ratings))

def ndcg_at_k(query: str, doc_ids: list[str],
              judgments: dict[str, int], k: int = 10) -> float:
    ranked = [judgments.get(d, 0) for d in doc_ids[:k]]
    ideal = sorted(judgments.values(), reverse=True)[:k]
    idcg = dcg(ideal)
    return dcg(ranked) / idcg if idcg > 0 else 0.0

def test_ndcg_baseline():
    judgments = load_judgments("tests/judgments.csv")
    scores = {}
    for query, rels in judgments.items():
        ids = ranked_ids(query)
        scores[query] = ndcg_at_k(query, ids, rels)
    mean = sum(scores.values()) / len(scores)
    assert mean >= 0.75, f"Mean nDCG@10 regressed: {mean:.3f}"

Step 5 - Per-query regression guard

import json
from pathlib import Path

def test_no_query_drops_more_than_10_percent():
    baseline = json.loads(Path("tests/solr_baseline.json").read_text())
    judgments = load_judgments("tests/judgments.csv")
    for query, rels in judgments.items():
        current = ndcg_at_k(query, ranked_ids(query), rels)
        b = baseline[query]
        delta = current - b
        assert delta >= -0.10, (
            f"Query '{query}' dropped {delta:.3f} "
            f"(was {b:.3f}, now {current:.3f})"
        )

Save a new baseline after any intentional improvement:

python3 tests/capture_baseline.py > tests/solr_baseline.json

Step 6 - debugQuery for score explain

Per the Solr debugQuery reference (opens in new window), append debug=results&debug.explain.structured=true to receive a nested score breakdown per document:

def explain_top(query: str, rows: int = 5) -> dict:
    r = requests.get(f"{SOLR}/select", params={
        "q": query,
        "defType": "edismax",
        "qf": "title^5.0 description^1.0",
        "rows": rows,
        "debug": "results",
        "debug.explain.structured": "true",
    })
    return r.json()["debug"]["explain"]

def test_top_doc_score_above_threshold():
    explain = explain_top("running shoes")
    top_id = next(iter(explain))
    score = explain[top_id]["value"]
    assert score >= 5.0, f"Top document score {score:.2f} below expected floor"

Use explainOther (per the Solr debugQuery reference (opens in new window)) to compare scoring of an expected document against the actual top results:

?q=running+shoes&explainOther=id:SKU-1234&debug=results

This surfaces why SKU-1234 ranked lower than expected.

Step 7 - eDisMax tuning verification

Per the Solr eDisMax reference (opens in new window), the key parameters affecting relevance are:

ParameterEffect
qfField weights: title^5.0 description^1.0
pfPhrase proximity boost when all terms appear together
mmMinimum-should-match: 75% requires 3 of 4 terms
bqAdditive boost query: bq=category:shoes^2.0
bfAdditive function boost: bf=recip(rord(price),1,1000,1000)
tieTie-breaker across qf fields (default 0.0)
psPhrase slop: ps=3 allows 3 intervening words

Pin the eDisMax config in tests so a config file change is caught before deploy:

EDISMAX_PARAMS = {
    "defType": "edismax",
    "qf": "title^5.0 description^1.0 brand^3.0",
    "pf": "title^10.0",
    "mm": "75%",
    "tie": "0.1",
}

def test_edismax_params_unchanged():
    # Fails if the live handler returns different defaults
    r = requests.get(f"{SOLR}/config/requestHandler",
                     params={"componentName": "/select"})
    handler = r.json()["config"]["requestHandler"]["/select"]
    defaults = handler.get("defaults", {})
    for key, expected in EDISMAX_PARAMS.items():
        assert defaults.get(key) == expected, (
            f"eDisMax param '{key}' changed: expected {expected!r}, "
            f"got {defaults.get(key)!r}"
        )

Step 8 - LTR feature store upload and verification

Per the Solr LTR reference (opens in new window), the feature store REST API:

# Upload features
curl -XPUT 'http://localhost:8983/solr/test_products/schema/feature-store' \
  --data-binary "@tests/ltr/features.json" \
  -H 'Content-type:application/json'

# Verify store contents
curl 'http://localhost:8983/solr/test_products/schema/feature-store/_DEFAULT_'

Minimal features.json with a field-value feature and a recency function:

[
  {
    "name": "titleMatch",
    "class": "org.apache.solr.ltr.feature.SolrFeature",
    "params": { "q": "title:(${query})" }
  },
  {
    "name": "recency",
    "class": "org.apache.solr.ltr.feature.FieldValueFeature",
    "params": { "field": "published_date" }
  }
]
def test_feature_store_uploaded():
    r = requests.get(
        f"{SOLR}/schema/feature-store/_DEFAULT_"
    )
    names = {f["name"] for f in r.json()["features"]}
    assert "titleMatch" in names
    assert "recency" in names

Step 9 - LTR model upload and re-ranking test

Per the Solr LTR reference (opens in new window), upload a MultipleAdditiveTreesModel (LambdaMART):

curl -XPUT 'http://localhost:8983/solr/test_products/schema/model-store' \
  --data-binary "@tests/ltr/lambdamart_v1.json" \
  -H 'Content-type:application/json'

Then assert the LTR re-ranked list improves nDCG vs the baseline BM25 list. Per the Solr LTR reference (opens in new window), the rq parameter with reRankDocs controls how many top BM25 candidates are re-scored:

def ranked_ids_ltr(query: str, rows: int = 10,
                   rerank_docs: int = 100) -> list[str]:
    r = requests.get(f"{SOLR}/select", params={
        "q": query,
        "defType": "edismax",
        "qf": "title^5.0 description^1.0",
        "rq": "{!ltr model=lambdamart_v1 reRankDocs=" + str(rerank_docs) + "}",
        "rows": rows,
        "fl": "id,score,[features]",
    })
    return [doc["id"] for doc in r.json()["response"]["docs"]]

def test_ltr_improves_ndcg():
    judgments = load_judgments("tests/judgments.csv")
    bm25_scores, ltr_scores = {}, {}
    for query, rels in judgments.items():
        bm25_scores[query] = ndcg_at_k(query, ranked_ids(query), rels)
        ltr_scores[query] = ndcg_at_k(query, ranked_ids_ltr(query), rels)
    bm25_mean = sum(bm25_scores.values()) / len(bm25_scores)
    ltr_mean = sum(ltr_scores.values()) / len(ltr_scores)
    assert ltr_mean >= bm25_mean, (
        f"LTR model did not improve nDCG: BM25={bm25_mean:.3f}, LTR={ltr_mean:.3f}"
    )

Step 10 - CI integration

# .github/workflows/solr-relevance.yml
jobs:
  relevance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Start Solr
        run: |
          bin/solr start -p 8983
          bin/solr create -c test_products -d _default
      - name: Index test corpus
        run: python3 tests/index_corpus.py
      - name: Upload LTR feature store
        run: |
          curl -XPUT 'http://localhost:8983/solr/test_products/schema/feature-store' \
            --data-binary "@tests/ltr/features.json" \
            -H 'Content-type:application/json'
      - name: Upload LTR model
        run: |
          curl -XPUT 'http://localhost:8983/solr/test_products/schema/model-store' \
            --data-binary "@tests/ltr/lambdamart_v1.json" \
            -H 'Content-type:application/json'
      - name: Run relevance tests
        run: pytest tests/ -v --tb=short

Anti-patterns

Anti-patternWhy it failsFix
Mutable test coreIndex changes shift baselines between runsSnapshot + restore before each CI run
Only asserting aggregate nDCGPer-query regressions hide in the meanPer-query guard (Step 5)
LTR model tested without BM25 baselineImprovement is unmeasurableCapture BM25 nDCG first, then compare (Step 9)
Fetching debugQuery output without debug.explain.structured=trueString parse is fragile across Solr versionsAlways use structured explain
Uploading model before feature storeModel references features that don't exist yetFeatures first, model second (Steps 8-9)
Hard-coded reRankDocs=10Candidate pool too small; LTR can't reorder enough docsSet reRankDocs to at least 3x the result page size

Limitations

  • Solr has no native IR-metrics API (_rank_eval equivalent); nDCG must be computed in the test harness. ES/OS teams may prefer the built-in API.
  • LTR requires the ltr contrib module enabled in solrconfig.xml and the featureVectorCache configured. Missing config silently disables re-ranking.
  • Large LTR models (deep tree ensembles) may exceed ZooKeeper's buffer limits in SolrCloud; use DefaultWrapperModel with an external resource reference (per the Solr LTR reference (opens in new window)).
  • Click-derived judgments carry position bias; correct using click models before using them as ground truth.

References