Mapping is ES’s word for schema. Which fields exist, what types they are, how they get analyzed. There are two flavors: dynamic (ES guesses) and explicit (we declare).
Dynamic mapping — the prototype mode
If we index a document into a non-existent index, ES will:
- Create the index.
- Look at each field in the JSON.
- Guess the type based on the value.
- Save that mapping forever.
POST /products/_doc
{
"title": "Sony WH-1000XM5",
"price": 399,
"in_stock": true,
"created_at": "2026-05-26T10:30:00Z",
"tags": ["audio", "wireless"]
}
ES infers:
title→text(with a.keywordsub-field)price→longin_stock→booleancreated_at→datetags→text(with.keyword)
Sounds convenient, right? Until things break.
price: 399 → mapped as long. Next doc has price: 399.99 → rejected.- First doc has
id: "abc123" → mapped as text. Now you can't sort or aggregate on it without pain.- Field explosion: someone sends
{ "metadata": { "click_1": ..., "click_2": ... } } dynamically. Your mapping grows by one field per request.- Dates:
"2026-05-26" mapped as date. Then someone sends "May 26 2026" → rejected.
Explicit mapping — the production mode
For anything serious, declare your schema upfront:
PUT /products
{
"mappings": {
"properties": {
"title": { "type": "text" },
"sku": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"in_stock": { "type": "boolean" },
"created_at": { "type": "date", "format": "strict_date_optional_time" },
"tags": { "type": "keyword" }
}
}
}
Now we have predictable types, we can’t accidentally pollute the mapping, and bad data gets rejected early.
Adding new fields later
Mappings are mostly append-only. We can add a new field, but we can’t change an existing field’s type.
PUT /products/_mapping
{
"properties": {
"discount_pct": { "type": "float" }
}
}
This works. But trying to change title from text to keyword? Nope — you’d have to reindex into a new index with the corrected mapping.
Controlling dynamic behavior
We don’t have to choose all-or-nothing. The dynamic setting has three values:
true(default) — new fields are added to the mapping automaticallyfalse— new fields are stored in_sourcebut NOT indexed (invisible to search)strict— new fields cause the document to be rejected
PUT /products
{
"mappings": {
"dynamic": "strict",
"properties": {
"title": { "type": "text" },
"sku": { "type": "keyword" }
}
}
}
// This now FAILS:
POST /products/_doc
{ "title": "Sony XM5", "sku": "SONY-001", "random_field": "oops" }
// → strict_dynamic_mapping_exception
Strict mapping is the safest default for production. It forces you to be intentional.
Dynamic templates — the middle ground
Sometimes we want some flexibility — e.g. “any field that ends in _id should be a keyword.” That’s what dynamic templates are for:
PUT /events
{
"mappings": {
"dynamic_templates": [
{
"ids_as_keyword": {
"match": "*_id",
"mapping": { "type": "keyword" }
}
},
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": { "type": "keyword", "ignore_above": 1024 }
}
}
]
}
}
Now user_id, session_id, product_id all become keywords automatically, and any other string becomes a keyword too (not text). Super useful for log indices where the shape varies.
The TL;DR
- Prototyping locally? Dynamic mapping is fine.
- Production? Explicit mapping. Maybe
dynamic: strictto catch typos. - Log/event data with unknown shape? Dynamic templates with sensible defaults.
- Need to change a field’s type? Reindex into a new index. There’s no in-place ALTER COLUMN.