Choosing Between keyword and text Field Types in Elasticsearch
A decision guide for picking Elasticsearch's keyword vs text field types, with a comparison table, trade‑off analysis, and a ready‑to‑run multi‑field mapping plus validation queries.
10 Aug 2025, 06:24 UTC

Decision and Constraints
When you index a string field you must decide whether the field will be used for exact filtering, sorting, and aggregations (keyword) or for relevance‑based full‑text search (text). The choice is immutable for the life of the index; changing it later requires a full re‑index. Constraints to consider:
- Exact match, range filter, or terms aggregation needed →
keyword. - Free‑text search, phrase matching, or relevance scoring needed →
text. - Both use cases on the same logical property → use a multi‑field mapping.
- Field length > 256 bytes (default
ignore_above) →keywordwill not be indexed unless you raise the limit. - Enabling
fielddataon atextfield for aggregations can exhaust JVM heap.
Supported Options Compared
| Aspect | keyword | text | Multi‑field (both) |
|---|---|---|---|
| Indexing | Stored as a single token | Analyzed into terms | Both representations stored |
| Exact filter / term query | Works out of the box | Fails (tokens ≠ original) | Use .keyword sub‑field |
| Full‑text search / match query | Not supported | Works (analyzer‑driven) | Use root text field |
| Sorting | Supported | Disabled by default | Sort on .keyword |
| Aggregations (terms, significant_terms) | Supported | Blocked unless fielddata:true | Aggregate on .keyword |
| Disk overhead | Low | Low | ~2× for the field |
| Re‑index required to change | Yes | Yes | Yes (mapping immutable) |
Trade‑offs
Keyword only gives minimal storage and fast exact operations but cannot serve relevance search. Text only enables rich queries but blocks aggregations and exact filters unless you enable fielddata, which is risky on large data sets. Multi‑field gives the best of both worlds at the cost of roughly double the inverted index size for that field. Choose multi‑field when the same logical attribute participates in both dashboards (filters, Kibana visualizations) and search boxes.
Concrete Implementation
Create an index products where the brand field is indexed as both text (for search) and keyword (for filters and aggregations). Run the following curl command against a cluster where you have manage_index privileges (typically the admin or superuser role). Replace <ES_HOST> with your cluster endpoint.
curl -X PUT "https://<ES_HOST>/products" \
-H 'Content-Type: application/json' \
-d '{
"mappings": {
"properties": {
"brand": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"description": { "type": "text" }
}
}
}'
Expected response: {"acknowledged":true,"shards_acknowledged":true,"index":"products"}. If you receive a mapping conflict, the index already exists with a different mapping; you must delete and recreate or re‑index into a new index.
Validation Steps
- Exact filter – run a term query on the
.keywordsub‑field:
Verify that only documents with the exact brand "Acme" are returned.curl -X GET "https://<ES_HOST>/products/_search" \ -H 'Content-Type: application/json' \ -d '{ "query": { "term": { "brand.keyword": "Acme" } } }' - Full‑text search – run a match query on the root
textfield:
Expect hits containing "Acme Corp", "Acme Corporation", etc., ranked by relevance.curl -X GET "https://<ES_HOST>/products/_search" \ -H 'Content-Type: application/json' \ -d '{ "query": { "match": { "brand": "acme corp" } } }' - Aggregation on keyword – request a terms aggregation on
brand.keyword:
The response should contain buckets for each distinct brand.curl -X GET "https://<ES_HOST>/products/_search?size=0" \ -H 'Content-Type: application/json' \ -d '{ "aggs": { "brands": { "terms": { "field": "brand.keyword" } } } }' - Aggregation on text (blocked) – attempt the same aggregation on the root
brandfield:
Elasticsearch returns a 400 error:curl -X GET "https://<ES_HOST>/products/_search?size=0" \ -H 'Content-Type: application/json' \ -d '{ "aggs": { "brands_text": { "terms": { "field": "brand" } } } }'Fielddata is disabled on text fields by default.... This confirms the default protection against heap pressure.
Limitations and Practical Checks
- ignore_above: The example sets
ignore_above: 1024so brand values up to 1 KB are indexed as keywords. Values longer than that are silently ignored for the keyword sub‑field but still searchable via the text field. - Re‑index requirement: Any mapping change (e.g., adding a new sub‑field) forces a full re‑index. Plan index aliases to avoid downtime.
- Heap monitoring: If you ever enable
fielddata:trueon a text field, monitorjvm.mem.heap_used_percentandindices.fielddata.memory_sizevia the_nodes/statsAPI. - Verification checklist after deployment:
- Run the four validation queries above.
- Confirm term query on
.keywordreturns expected exact matches. - Confirm match query on
textreturns relevance‑ranked results. - Confirm aggregation on
.keywordsucceeds and ontextfails with the expected error.
Rollback Note
Creating the index is a state‑changing operation. If the mapping is wrong, delete the index (DELETE /products) and recreate with the corrected mapping. No partial rollback is possible because mappings are immutable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.