The 30 second answer
A text field is analyzed at index time, so "Red Dress" is stored as the tokens red and dress. A term query is not analyzed, so it looks for the exact string Red Dress and finds nothing. Use match for full text, and add a keyword sub-field through multi-fields for exact filters, sorting and facets. That gives one source value indexed two ways, each for its own job.
This is one of the most common Elasticsearch mistakes, and it shows up again in the facet question below, because facets need the same exact values.
Why it misses. The official docs warn: "Avoid using the term query for text fields." A text field goes through an analyzer at index time. The default standard analyzer removes most punctuation, splits on word boundaries and lowercases, so the brand value "Tommy Hilfiger" is indexed as the two terms tommy and hilfiger. A term query is not analyzed. Searching term: { brand: "Tommy Hilfiger" } looks for one term with a capital T and a space in it, which does not exist in the index, so it returns nothing. term: { brand: "tommy" } would match, which is even more confusing in a code review.
Match versus term. A match query runs the query text through the field's search analyzer first, so match: { brand: "Tommy Hilfiger" } produces the same tokens and matches. Use match (and its relatives such as multi_match) for what a shopper types, and term, terms or range for exact, structured values such as an id, a size code or a flag.
Multi-fields. Most product attributes need both behaviours: brand must be searchable as words and also filterable, sortable and aggregatable as an exact value. Multi-fields index the same source value more than once:
"brand": {
"type": "text",
"fields": {
"raw": { "type": "keyword" }
}
}Queries use brand for full text and brand.raw for term filters, sorting and terms aggregations. A keyword field is indexed as one unanalyzed term and has doc values, which is what sorting and aggregations read. If case differences in the source are a problem, a normalizer on the keyword field lowercases it without splitting it.
Why the dynamic default matters. With dynamic mapping, Elasticsearch typically maps a new string as text with a keyword sub-field, which is why field.keyword appears in so many examples. In a production catalog you want explicit mappings instead, so analyzers per language and keyword sizes are deliberate and a stray field cannot cause a mapping explosion.
How to debug it live. Run the _analyze API with the field's analyzer on the stored value and on the query text, then compare tokens. If the tokens differ, the query type or the mapping is wrong.
Alternate interview scenarios
"A filter on colour works for most values but not for 'Light Blue'. Why?"
Most colours are single words, so the analyzed tokens happen to equal the lowercased value and a term filter on lowercase input appears to work. "Light Blue" becomes two tokens, so an exact match on the whole value can never succeed against the text field. The fix is to filter on a keyword sub-field, with a normalizer if the data has inconsistent casing.
"Can you aggregate on a text field?"
Not by default, because text fields do not have doc values; aggregations and sorting need a keyword (or numeric) field. Enabling fielddata on a text field makes it possible but loads the analyzed terms into heap, which is expensive and aggregates on tokens rather than whole values. Use a keyword sub-field instead.
Sources (3)
- Elasticsearch: Term queryelastic.co
- Elasticsearch: Multi-fieldselastic.co
- Elasticsearch: Keyword field typeelastic.co