Definition
- Dynamic mapping – Elasticsearch's behavior of automatically inferring a type for a field it has never seen, based on the value in the first document.
- text type – an analyzed field type that tokenizes and lowercases the value for full-text search; cannot be used for sorting or aggregation on the whole value.
- keyword type – an un-analyzed field type that stores the whole value verbatim for exact matching, sorting, and aggregations.
- Multi-field – the default dynamic mapping for strings: the field itself is
text, with a.keywordsub-field of typekeyword(ignore_above 256). - Explicit mapping – the production practice of defining field types up front instead of relying on dynamic inference.
Imagine National Bank of India files a customer's full address in two forms. One form is chopped into searchable words: street, mumbai, india, so a clerk can find anyone living in Mumbai; that is the text form. The other form is the whole address kept exactly as written on one line, used for sorting and grouping; that is the keyword form. When the bank first sees a new kind of field, it files in both forms just in case.
Understanding the Concepts
What happens when Elasticsearch sees a new string field?
Dynamic mapping is the behavior where, if you index a document containing a field not in the mapping, Elasticsearch infers a type from the value and adds it automatically. For a string value, the result is a multi-field: the field is mapped as text, and a sub-field named keyword is added.
# Index a document with a new field "description"
curl -X PUT "http://localhost:9200/transactions/_doc/1" \
-H "Content-Type: application/json" \
-d '{"description": "International wire transfer to HDFC savings"}'
# Check the auto-generated mapping
curl -X GET "http://localhost:9200/transactions/_mapping"
{
"transactions": {
"mappings": {
"properties": {
"description": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
How does the text type work?
The text type is for full-text search. When a value is indexed into a text field it passes through an analyzer that tokenizes and normalizes it. This lets a match query find the document when the user searches for any word, regardless of case.
# Full-text search on the text field
curl -X GET "http://localhost:9200/transactions/_search" \
-H "Content-Type: application/json" \
-d '{
"query": {
"match": {
"description": "wire transfer"
}
}
}'
# Matches because "International wire transfer..." contains
# the tokens "wire" and "transfer" after analysis
How does the keyword type work?
The keyword type stores the value verbatim and un-analyzed. It is what you use for structured identifiers and categorical data: account IDs, status codes, currency codes.
# Exact match on the keyword sub-field
curl -X GET "http://localhost:9200/transactions/_search" \
-H "Content-Type: application/json" \
-d '{
"query": {
"term": {
"status.keyword": "COMPLETED"
}
}
}'
# Aggregation on the keyword sub-field
curl -X GET "http://localhost:9200/transactions/_search" \
-H "Content-Type: application/json" \
-d '{
"size": 0,
"aggs": {
"by_status": {
"terms": {
"field": "status.keyword"
}
}
}
}'
{"aggregations":{"by_status":{"buckets":[{"key":"COMPLETED","doc_count":150},{"key":"FAILED","doc_count":12}]}}}
How is text different from keyword?
| Aspect | text | keyword |
|---|---|---|
| Analyzed | Yes (tokenized, lowercased) | No (stored verbatim) |
| Full-text search | Yes | No |
| Exact match | No (tokens, not whole value) | Yes |
| Sorting | Not efficient | Yes |
| Aggregation | Not meaningful | Yes |
| Use case | Descriptions, notes | IDs, status, codes |
What is date detection and why is it a gotcha?
Dynamic mapping also infers other types. A string that looks like a date can be mapped as a date if date detection is enabled, which is a common surprise.
# This string looks like a date -- ES may map it as date!
curl -X PUT "http://localhost:9200/accounts/_doc/1" \
-H "Content-Type: application/json" \
-d '{"referenceCode": "2025-01-15"}'
# Check -- surprise, it is a date!
curl -X GET "http://localhost:9200/accounts/_mapping"
"referenceCode": {"type": "date"}
# If the next document has referenceCode: "REF-ABC-123"
# it will FAIL because it cannot parse as a date!
# Disable date detection to prevent surprises
curl -X PUT "http://localhost:9200/accounts" \
-H "Content-Type: application/json" \
-d '{"mappings":{"date_detection":false}}'
What are the dynamic mapping modes?
Dynamic mapping has modes you can set per index to control how unknown fields are handled.
# strict mode: reject documents with unmapped fields
curl -X PUT "http://localhost:9200/transactions" \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"dynamic": "strict",
"properties": {
"amount": {"type": "long"},
"status": {"type": "keyword"},
"description": {"type": "text"}
}
}
}'
# This will be REJECTED because "newField" is not mapped
curl -X PUT "http://localhost:9200/transactions/_doc/1" \
-d '{"amount":50000,"status":"COMPLETED","newField":"surprise"}'
{"error":{"reason":"mapping set to strict, dynamic introduction of [newField] within [_doc] is not allowed"}}
| Mode | Behavior |
|---|---|
| true (default) | Auto-add new fields to mapping |
| runtime | Add as runtime fields (query-time only) |
| false | Store but ignore in queries |
| strict | Reject documents with unknown fields |
What are common mistakes?
Relying on dynamic mapping for production doubles index size by storing two representations of every string field.
# WRONG: dynamic mapping for production -- accountId gets text + keyword
curl -X PUT "http://localhost:9200/transactions/_doc/1" \
-d '{"accountId":"ACC001","status":"COMPLETED"}'
# accountId is text + keyword, but you only ever filter on it
# CORRECT: explicit mapping -- accountId is pure keyword
curl -X PUT "http://localhost:9200/transactions" \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"dynamic": "strict",
"properties": {
"accountId": {"type": "keyword"},
"status": {"type": "keyword"},
"description": {"type": "text"},
"timestamp": {"type": "date"}
}
}
}'
What should a senior engineer know?
A senior engineer defines explicit mappings for production indices. For a banking audit index at National Bank of India, the account ID and status are pure keyword, the description is text, and the timestamp is date. Dynamic is set to strict so unexpected fields are rejected rather than silently and wrongly mapped.
package com.bank.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "transactions")
public class TransactionDocument {
@Id
private String id;
@Field(type = FieldType.Keyword) // Exact match only
private String accountId;
@Field(type = FieldType.Keyword)
private String status;
@Field(type = FieldType.Text) // Full-text search
private String description;
@Field(type = FieldType.Date)
private String timestamp;
// Constructors, getters omitted for brevity
}
Visual Overview
New string field arrives in a document
│
▼
Dynamic mapping infers type
│
├──→ JSON string → text + keyword multi-field
│ text: ["international", "wire", "transfer"]
│ keyword: "International wire transfer" (verbatim)
│
├──→ JSON number → long or float
├──→ JSON boolean → boolean
└──→ Date-like string → date (if date_detection=true)
Query routing:
│
├──→ match query → uses text field (tokenized)
├──→ term query → uses field.keyword (exact)
├──→ sort → uses field.keyword (exact)
└──→ aggregation → uses field.keyword (exact)
Key Takeaway
The precise answer is that neither text nor keyword alone is the default for a string in Elasticsearch: the default is both at once. Dynamic mapping creates a multi-field: the top-level field as text for full-text search, and a keyword sub-field for exact matching, sorting, and aggregation. The text type tokenizes and lowercases; the keyword type stores verbatim. Elasticsearch hedges because it does not know your intent when it first sees a field.
The gotcha is date detection: a string like "2025-01-15" can be mapped as a date, and then a non-date value in the same field fails. Dynamic mapping modes let you control the behavior: true auto-adds, false ignores, and strict rejects unknown fields.
For production, explicit mappings are the professional choice. At National Bank of India, the transactions audit index maps account ID and status as keyword, description as text, and timestamp as date, with dynamic set to strict so nothing sneaks in wrongly mapped.
Remember one sentence: dynamic mapping gives you text plus keyword for every string, but production indices deserve explicit mappings that match your actual query patterns and avoid doubling the index size.
---