Introduction
You've probably heard the warning: dynamic mapping in Elasticsearch feels convenient until production goes down. Elasticsearch mapping explained: lessons from production isn't just about defining fields—it's about preventing silent failures, avoiding expensive reindexing operations, and designing search systems that scale. Mapping decisions made on day one shape query performance, aggregation capability, and operational headaches for years.
Over the past four days, we've built intuition around indexing and querying. Today we're shifting focus to the schema layer—how to declare fields explicitly, choose the right types, and plan for the inevitable changes that come later. We've learned these lessons the hard way across e-commerce platforms, CRM systems, and trading dashboards where a wrong mapping choice meant either performance cliffs or weekend reindexing marathons.
Explicit mappings — The foundation of reliability
Dynamic mapping is Elasticsearch's default behavior: send a document with a new field, and the cluster guesses the type and indexes it. In development, this feels magical. In production, it's a liability.
Here's why. A field typed as text when it should be keyword means you can't sort on it later. A numeric field indexed as a string breaks aggregations. Worse, once indexed, changing the type requires reindexing the entire shard. On a multi-terabyte index, that's hours of downtime or careful blue-green operations.
Explicit mapping gives you control. Before a single document lands in your index, you declare every field, its type, and how it should be indexed. This is non-negotiable in production.
Start with a mapping that covers your known schema. Here's a realistic example from an e-commerce product catalog:
PUT products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
"tags": {
"type": "keyword"
},
"description": {
"type": "text",
"analyzer": "standard"
},
"in_stock": {
"type": "boolean"
},
"created_at": {
"type": "date"
}
}
}
}
Notice we haven't left anything to chance. Each field has an explicit type. Each type serves a purpose. The mapping is a contract: "Here's what data we accept, and here's how we'll search it."
To prevent accidental field creation, add a strict policy at index creation. Set dynamic: false to reject documents with unknown fields, or dynamic: "strict" to throw an error immediately:
PUT products
{
"mappings": {
"dynamic": "strict",
"properties": {
"name": { "type": "text" },
"price": { "type": "scaled_float", "scaling_factor": 100 }
}
}
}
With dynamic: "strict", any document missing a field in your mapping will fail indexing. This forces discipline: update the mapping first, test it, then deploy your application. It hurts briefly. It prevents much larger hurts later.
Text vs keyword — The foundational choice
The single most important decision in your mapping is understanding the difference between text and keyword fields. Get this wrong, and you'll spend weeks debugging why full-text search works but sorting doesn't, or vice versa.
Text fields are for full-text search. Elasticsearch analyzes them: lowercasing, tokenizing, stemming, removing stop words. A product name "Red Running Shoes" becomes tokens like "red", "run", "shoe". Queries against text fields are forgiving and powerful—users search for "red shoes" and find "Red Running Shoes". But you cannot sort or aggregate on text fields.
Keyword fields are for exact matching, filtering, sorting, and aggregations. They're stored as-is, not analyzed. A tag "electronics" stays "electronics". Queries must match exactly or use term-level queries. But sorting and aggregations work perfectly.
Most fields need both. Use multi-fields (covered in the next section) to index the same value in both ways:
PUT products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"tags": {
"type": "keyword"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
Now name supports full-text queries like match, and name.keyword supports sorting and exact-match filters. The tags field is keyword-only because we always filter and aggregate on tags, never full-text search them.
For numeric fields like price, we use scaled_float instead of float to preserve precision. Floating-point arithmetic can introduce rounding errors; scaled_float with scaling_factor: 100 stores prices as integers (in cents) internally, eliminating precision issues on financial data.
Multi-fields — Index once, query many ways
Multi-fields let you index a single source field using different analyzers and types. This is powerful for flexibility without data duplication.
Think about a product title in an international e-commerce platform. You want to:
- Full-text search with stemming (match "running" to "runner").
- Autocomplete suggestions (prefix matching).
- Exact phrase matching without analysis.
- Sort results by title.
Multi-fields handle all of this from a single input:
PUT products
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": {
"type": "keyword"
},
"raw": {
"type": "keyword",
"index": false
},
"suggest": {
"type": "completion"
}
}
}
}
}
}
Now a single document field title: "Red Running Shoes" is indexed as:
title(text, analyzed): enables full-text search.title.keyword(keyword): enables sorting and filtering.title.suggest(completion): enables autocomplete.
In queries, you reference each version:
GET products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "running shoes" } }
],
"filter": [
{ "term": { "title.keyword": "Red Running Shoes" } }
]
}
},
"sort": [
{ "title.keyword": "asc" }
]
}
The storage overhead is minimal—Elasticsearch reuses the source data. The query flexibility is massive. You've solved sorting, aggregation, full-text search, and autocomplete with one well-planned mapping.
Mappings are hard to change — Plan ahead
Once data is in Elasticsearch, the mapping is mostly immutable. You can add new fields, but changing a field's type requires reindexing.
Let's say you indexed a SKU field as text and later realized you need it as keyword for filtering. You cannot simply update the mapping. You must:
- Create a new index with the corrected mapping.
- Reindex data from the old index to the new one.
- Update your application to point to the new index.
- Delete the old index.
On a billion-document index, this takes hours or days. On a real-time trading system or ride-hailing platform, this is operational hell.
The solution is planning. Spend time upfront understanding your query patterns, sort requirements, and filtering needs. Ask:
- Will we ever sort on this field?
- Will we aggregate or bucket by this?
- Is this for full-text search or exact matching?
- What's the cardinality (unique value count)?
For fields you're unsure about, use multi-fields. Index them both as text and keyword. The storage cost is small insurance against reindexing.
Here's a safe, general-purpose mapping for unstructured fields:
PUT documents
{
"mappings": {
"properties": {
"content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"tags": {
"type": "keyword"
},
"created_at": {
"type": "date"
}
}
}
}
The ignore_above: 256 on the keyword field prevents indexing extremely long values—a practical safeguard for keyword-indexed text fields that might occasionally exceed reasonable lengths.
If you do need to reindex, use the Reindex API:
POST _reindex
{
"source": {
"index": "products"
},
"dest": {
"index": "products_v2"
}
}
This copies all documents from the old index to the new one with the updated mapping. It's slow but reliable. Plan for it during maintenance windows.
Common mistakes
Relying on dynamic mapping in production. It works until it doesn't. The moment a field comes in with an unexpected value (a string when you expected a number), the type is decided forever. Use explicit mappings.
Using text for fields that need sorting. Text fields cannot be sorted. Every time you add a new field you think might be sorted, ask yourself: "Is this text for searching or data for filtering/sorting?" If it's the latter, use keyword or add a keyword sub-field.
Not using multi-fields. The storage overhead is negligible. The query flexibility is enormous. Make multi-fields your default for fields that might be queried multiple ways.
Over-analyzing text fields. The standard analyzer works for most use cases. But adding aggressive stemming or custom filters can make queries behave unexpectedly. Test analyzers thoroughly before production.
Forgetting about numeric precision. Use scaled_float for financial data, not float`. Floating-point errors compound in aggregations. This mistake has caused real money discrepancies in e-commerce systems.
Key takeaways
- Always use explicit mappings in production. Set
dynamic: "strict"to catch schema violations early. - Understand the text vs keyword distinction: text for full-text search, keyword for filtering, sorting, and aggregations.
- Use multi-fields to index values multiple ways. Index text fields as both text and keyword to get search and sort capability from one source field.
- Plan your mapping before indexing. Changing types requires reindexing, which is expensive and disruptive at scale.
- Use
scaled_floatfor financial data to avoid precision loss. - When in doubt, add a keyword sub-field. The storage cost is minimal; the operational flexibility is substantial.






Comments
Loading comments…