Lesson 0003 · Relevance & Internals

Analysis & the term-mismatch trap

How text becomes terms — and the silent bug where a query matches nothing.

~10 minWarm-up + quiz + labCheat sheet: here

Warm-up — recall from 0002

BM25 sums a score over each query term. So answer from memory first: what actually decides what counts as a “term”? — Not the query. It's analysis: the pipeline that turns raw text into indexed tokens, run once at index time and again at search time. If those two runs disagree, the match silently fails. That failure is the payoff of this lesson.

The analyzer: three stages, in order

An analyzer transforms a text value into a stream of terms (tokens) through exactly three stages, always in this order: Analyzer reference

1 · char filtersRewrite the raw character stream before tokenizing — strip HTML, map &and. Zero or more.
2 · tokenizerSplit the stream into tokens. Exactly one. The standard tokenizer breaks on word boundaries.
3 · token filtersAdd, remove, or change tokens — lowercase, stop-words, stemming, synonyms. Zero or more.

The default standard analyzer is standard tokenizer + lowercase filter. So the field value "The Quick-Brown Fox" becomes three terms:

thequickbrownfox — lowercased, and the hyphen split into a boundary. Those are what land in the index. The original string is gone (kept only in _source, which is never searched).

Two runs of the pipeline: index time vs search time

Analysis happens twice, and this is the crux:

Matching is term-against-term. By default the same analyzer runs on both sides, so they line up. You can set a different search_analyzer (a real technique — e.g. index with an edge-ngram analyzer for autocomplete, search with a plain one). But if the two pipelines ever produce different tokens for the same word, lookups miss. Analyzer reference

text vs keyword: analyzed or not

Whether a field is analyzed at all depends on its mapping type — the single most common source of “why didn't my query match” confusion: Mapping reference

TypeAnalyzed?Stored term(s)Built for
textYes — full pipelinemany tokens, e.g. thequickfull-text search (match)
keywordNo — verbatimone exact term The Quick-Brown Foxexact match, sorting, aggregations

A keyword field stores the whole value as one untokenized term, byte-for-byte. No lowercasing, no splitting. That's why you sort and aggregate on keyword, never on text. A dynamically-mapped string becomes both: a text field plus a .keyword sub-field.

The term-mismatch trap

Now the interview-grade payoff. Two query types treat analysis oppositely:

The rule that explains the trap

match analyzes its input; term does not. A term query looks up your string exactly as written against the stored terms.

So on a text field holding the document "Elasticsearch" (indexed, after lowercasing, as the term elasticsearch):

GET /docs/_search
{ "query": { "term": { "title": "Elasticsearch" } } }   // zero hits!

Zero hits — because term searches for the literal Elasticsearch (capital E), but the index only contains elasticsearch. The query never got lowercased, because term skips analysis. Swap to match (which analyzes Elasticsearchelasticsearch) and it matches.

The mirror-image mistake: running match against a keyword field. There the stored term is the whole verbatim string, so a full-text match on a single word inside it also misses. The fix is always the same diagnostic: ask what terms actually exist.

Two flavours of the same bug

term on text → your query wasn't analyzed but the field was. · match on keyword → the field wasn't analyzed but your query was. Both come from a pipeline mismatch between the two sides. See Lesson 0001: term also runs in filter context (unscored), which is why exact-match filters use it.

Check yourself

From memory — effortful recall is the point. Feedback is immediate.

Which stage splits the text stream into individual tokens?

The single tokenizer does the splitting. Char filters rewrite characters before it; token filters transform tokens after it.

Compared to match, a term query:

term looks up the input verbatim against stored terms. match analyzes the query string first — the reason term on a text field so often misses.

A keyword field stores its value as:

keyword is never analyzed — the whole value becomes one exact term, which is why it powers sorting, aggregations, and exact match.

The term-mismatch trap fires when index and search analysis:

Matching is term-against-term. If the two runs of the pipeline emit different tokens for the same word, the lookup finds nothing — a silent zero-hit failure.

To see exactly which tokens a field will produce, call:

_analyze shows the token stream. _explain shows BM25 arithmetic (lesson 0002); _profile shows query timing.

Hands-on: make the trap happen, then read the tokens

You have a live local cluster. Run these and watch analysis with your own eyes.

1 · Watch the pipeline turn text into terms

POST /_analyze
{ "analyzer": "standard", "text": "The Quick-Brown Fox" }

The response lists each token with its start_offset/end_offset. Confirm you get thequickbrownfox — lowercased and split on the hyphen. This is the ground truth of what's in your index.

2 · Reproduce the zero-hit bug

PUT /lab_analysis/_doc/1
{ "title": "Elasticsearch" }                            // text field, dynamically mapped

GET /lab_analysis/_search
{ "query": { "term":  { "title": "Elasticsearch" } } }  // predict: hits?

GET /lab_analysis/_search
{ "query": { "match": { "title": "Elasticsearch" } } }  // predict: hits?

Predict each before you run it. The term query returns nothing; match returns doc 1. Explain the difference out loud in one sentence — that sentence is the interview answer.

3 · Query the auto-created .keyword sub-field

GET /lab_analysis/_search
{ "query": { "term": { "title.keyword": "Elasticsearch" } } }   // now it hits

Here term works: the .keyword sub-field stored the verbatim Elasticsearch, so the exact string matches. Then DELETE /lab_analysis to clean up. Bring any surprise to your teacher.

Primary source — read this next

Text analysis — Analyzer reference (Elastic Docs). The authoritative walkthrough of char filters → tokenizer → token filters, the built-in analyzers, and how to configure a custom one. Pair it with the _analyze API doc for hands-on probing.