Troubleshoot search relevance
Use this page when your search returns results but they're in the wrong order, irrelevant, or missing expected matches. These symptoms point to a relevance problem rather than a technical error.
Use the explain API to see exactly how Elasticsearch calculated a document's score, or to find out why a document didn't match. For a visual representation, use the Search Profiler in Kibana.
When a document ranks higher or lower than you expect, run the explain API against it with the same query that returned the unexpected result. Replace 2 with the _id of the document you want to inspect, and replace the example match query with your own:
GET /my-index-000001/_explain/2
{
"query": {
"match": {
"title": "elasticsearch tutorial"
}
}
}
The response shows whether the document matched and breaks the score down by term and field:
{
"_index": "my-index-000001",
"_id": "2",
"matched": true,
"explanation": {
"value": 2.1845279,
"description": "sum of:",
"details": [
{
"value": 1.0922639,
"description": "weight(title:elasticsearch in 1) [PerFieldSimilarity], result of:",
"details": [
{
"value": 1.0922639,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{ "value": 2.2, "description": "boost" },
{ "value": 1.2039728, "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5))" },
{ "value": 0.4123711, "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl))" }
]
}
]
},
{
"value": 1.0922639,
"description": "weight(title:tutorial in 1) [PerFieldSimilarity], result of: ..."
}
]
}
}
- Confirms the document matched the query.
- The
boostvalue (here2.2) is the effective boost passed to the BM25 scorer. For an unmodified field, this equals1.0 × (1 + k1) = 2.2by default. A higher value, for example6.6for a^3field boost, indicates a user-specified boost was applied. - Each entry in
detailsshows one term's contribution.boost,idf(how rare the term is), andtf(how often it appears) multiply together to produce the term score.
Use the breakdown to find what's driving the ranking:
- If the
boostvalue is higher or lower than you expect, a field or query boost is changing the score. Refer to Fix boosting not working as expected. - If a term you expect is missing from
details, the query isn't searching the field you intended. Refer to Fix the wrong fields being searched. - If a term has a low
idf, it appears in many documents and contributes little to the score. Rank is driven by the rarer terms, so consider whether the query terms are specific enough.
When a document you expect in the results is missing, run the explain API against it with the same query that failed to return it. Replace 4 with the _id of the document you want to inspect, and replace the example match query with your own:
GET /my-index-000001/_explain/4
{
"query": {
"match": {
"title": "elasticsearch tutorial"
}
}
}
{
"_index": "my-index-000001",
"_id": "4",
"matched": false,
"explanation": {
"value": 0.0,
"description": "No matching clauses",
"details": [
{ "value": 0.0, "description": "no match on optional clause (title:elasticsearch)" },
{ "value": 0.0, "description": "no match on optional clause (title:tutorial)" }
]
}
}
"matched": false with "No matching clauses" means none of the query's tokens appear in the indexed field. Use this to decide where to go next:
- If the document contains the words you searched for, the index and query are probably tokenizing text differently. Refer to Fix token mismatches between index and query.
- If you expect a synonym to bridge the gap, refer to Fix synonyms not applying.
- If the words appear in a different field from the one you queried, refer to Fix the wrong fields being searched.
When a query fails to match an expected document, the index and query might be tokenizing text differently. Use the analyze API to inspect what tokens Elasticsearch produces for a given field and text.
In the following examples, the title field uses the english analyzer. First, check how the indexed field tokenizes the text:
GET /my-index-000001/_analyze
{
"field": "title",
"text": "Getting started with Elasticsearch"
}
{
"tokens": [
{ "token": "get", "position": 0 },
{ "token": "start", "position": 1 },
{ "token": "elasticsearch", "position": 3 }
]
}
The english analyzer removes the stop word with and stems getting to get and started to start. Removing with leaves a gap, so elasticsearch keeps position 3.
Then run the same text through the analyzer your query uses. By default, a match query uses the analyzer defined on the field, so the tokens only differ if the field has a different search_analyzer or the query overrides the analyzer. For example, if your title field uses the english analyzer but the query sets "analyzer": "standard", the tokens differ:
GET /my-index-000001/_analyze
{
"analyzer": "standard",
"text": "Getting started with Elasticsearch"
}
{
"tokens": [
{ "token": "getting", "position": 0 },
{ "token": "started", "position": 1 },
{ "token": "with", "position": 2 },
{ "token": "elasticsearch", "position": 3 }
]
}
The index contains the stemmed tokens get and start, but the query produces getting and started. These tokens don't match, so the document won't appear in results.
To reproduce this with a search, run a match query that overrides the field's analyzer. The query term started is analyzed as started, which doesn't match the indexed token start:
GET /my-index-000001/_search
{
"query": {
"match": {
"title": {
"query": "started",
"analyzer": "standard"
}
}
}
}
{
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}
Without the analyzer override, the same query uses the field's english analyzer, analyzes started as start, and matches the document.
To fix this, remove the analyzer override from the query so it uses the field's analyzer, or change the field's search_analyzer to match its index analyzer.
Refer to Test an analyzer for a detailed walkthrough.
Synonyms are usually applied at search time, which is the recommended approach because you can update synonym sets without reindexing. Synonym sets managed through the synonyms API or Kibana work only in search analyzers. Index-time synonyms are also supported with the synonym filter (not synonym_graph), but changing them requires a reindex.
Serverless supports only synonym sets managed through the synonyms API or Kibana, not index-time or file-based synonyms.
If synonyms aren't matching, check the following. These checks are independent, so start with whichever fits your situation:
The synonym filter is part of the analyzer. Use the analyze API with the analyzer that applies your synonyms, usually your index's search analyzer, to confirm the synonym expansion is happening:
GET /my-index-000001/_analyze{ "analyzer": "my_search_analyzer", "text": "car" }If synonyms are working, the response includes extra tokens with
"type": "SYNONYM":{ "tokens": [ { "token": "car", "position": 0, "type": "<ALPHANUM>" }, { "token": "automobile", "position": 0, "type": "SYNONYM" }, { "token": "vehicle", "position": 0, "type": "SYNONYM" } ] }If you only see the original token and no
SYNONYMentries, the synonym filter is not part of that analyzer's chain.Your recent synonym update took effect. If you updated the synonym set, confirm whether you need to reload or reindex:
- Synonym sets managed through the synonyms API reload automatically when you update the set. You don't need to reload manually or reindex, as long as the synonym filter sets
"updateable": true. -
Custom synonym files configured as index-time analyzers require a full reindex to take effect on already-indexed documents. If the file is configured as a search-time analyzer with "updateable": true, you can reload it without reindexing by calling the reload search analyzers API after you update the file on every node.
- Synonym sets managed through the synonyms API reload automatically when you update the set. You don't need to reload manually or reindex, as long as the synonym filter sets
Refer to Search with synonyms for setup and reload guidance.
If boosted fields or documents aren't ranking as expected, use the explain API to confirm the boost applies. Common causes:
- Field boosts in
multi_match: verify the^syntax is correct and the field exists in the mapping. function_score: check that thefilteron each function matches the documents you expect. Also check thatscore_mode(how the function scores combine) andboost_mode(how the result combines with the query score) are set the way you intend. Refer to Function score query.script_score: confirm the script returns the value you intend. The final score can't be negative, and a script that returnsNaN, a negative value, or infinity can give documents a score of0. Refer to Script score query and Debug script score calculation errors in Painless.
Run your query with "explain": true in the request body to see per-result score breakdowns inline:
GET /my-index-000001/_search
{
"explain": true,
"query": {
"multi_match": {
"query": "elasticsearch tutorial",
"fields": ["title^3", "body"]
}
}
}
Each result in the response includes an _explanation block showing how the boost affected the score:
{
"hits": {
"hits": [
{
"_id": "2",
"_score": 6.553584,
"_explanation": {
"value": 6.553584,
"description": "max of:",
"details": [
{
"value": 6.553584,
"description": "sum of:",
"details": [
{
"value": 3.276792,
"description": "weight(title:elasticsearch in 1) [PerFieldSimilarity], result of:",
"details": [
{
"value": 3.276792,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{ "value": 6.6000004, "description": "boost" },
{ "value": 1.2039728, "description": "idf" },
{ "value": 0.4123711, "description": "tf" }
]
}
]
}
]
}
]
}
}
]
}
}
The boost value here is 6.6 (the BM25 default 2.2 multiplied by the ^3 field boost). If you don't see the boost you configured reflected in this value, verify the field name in the fields array matches the mapping exactly.
When a multi_match query returns irrelevant results, the field list might be too broad or misconfigured. Check:
- The
fieldsarray in yourmulti_match: remove low-signal fields or add explicit boosts to prioritize the right ones. Refer to Multi-match query. - Whether
copy_tois pulling unrelated content into a combined field. Use the get mapping API to inspect which fields copy into your target field. Refer tocopy_to. - The
typeparameter:best_fieldsranks by the single best matching field, whilemost_fieldssums scores across fields. Switch between them to see which fits your use case. Refer to Multi-match query types.
If semantic search returns results that are off-topic or miss obvious matches, the most common cause is an embedding model mismatch: documents and queries were embedded by incompatible models or at different times.
Check the following:
- Verify that the indexing and query-time embeddings come from compatible models. The
inference_idparameter sets the endpoint used at index time, andsearch_inference_id, if set, sets the endpoint used at query time. The two endpoints can differ, but they must produce compatible embeddings. Refer to thesemantic_textfield type reference for details. - If you changed the model or endpoint, reindex the affected documents so their embeddings match the current model.
- Use the
_explainAPI to verify that the vector similarity scores are non-zero for documents you expect to match. - If embeddings are missing or zero-length, the inference pipeline might have failed silently during indexing.