If you’ve ever indexed a document into Elasticsearch without defining anything upfront, you’ve already used dynamic mapping — you just didn’t know about explicit mapping. Elasticsearch quietly inspected your data, made a series of judgment calls about field types, and built a schema on your behalf. Sometimes that works perfectly. Other times, it silently creates a text field where you needed a keyword, and you don’t discover the problem until a query returns nothing.
Table of Contents
What Is Mapping in Elasticsearch
Mapping is the schema definition for an Elasticsearch index. It tells Elasticsearch what fields a document contains, what data type each field holds, and how each field should be indexed and stored.
Every field in a document — a string, a number, a date, a nested object — has a mapping. That mapping controls everything: whether a string field is full-text searchable, whether a date can be used in range queries, whether a number can be aggregated, whether a field is stored at all.
Think of mapping as a contract
keyword is matched exactly. A field mapped as text is tokenized and analyzed. A field mapped as date supports gte and lte range syntax. The mapping is the contract between your data and your queries.There are two ways to establish that contract: let Elasticsearch figure it out automatically (dynamic mapping), or define it yourself (explicit mapping).
What Is Dynamic Mapping in Elasticsearch
Dynamic mapping means Elasticsearch automatically creates field mappings when it encounters new fields in a document — no schema definition required from you.
The first time you index a document with a field Elasticsearch hasn’t seen before, it inspects the value, infers a type, and adds the field to the index mapping. From that point on, the field is defined, and all future documents must be compatible with that definition.
Works
How Dynamic Mapping Works
When Elasticsearch encounters a new field, it applies type detection rules based on the JSON value type:
| JSON Value | Elasticsearch Type | Notes |
|---|---|---|
true / false | boolean | Straightforward. |
Whole number (e.g. 42) | long | Always long, never integer. |
Decimal number (e.g. 3.14) | float | Precision may surprise you. |
| String matching date format | date | ISO 8601 strings detected automatically. |
| String (everything else) | text + keyword sub-field | ⚠ Double-mapped — uses 2× storage. |
Object {} | object | Fields nested inside are also mapped dynamically. |
Array [] | Type of first element | Mixed-type arrays will fail on subsequent docs. |
text + keyword — even fields you’ll never do full-text search on. This silently doubles index size for every string field.Here’s what happens under the hood when you index a document for the first time:
PUT /orders/_doc/1
{
"customer": "Alice",
"total": 149.99,
"placed_at": "2024-03-15T10:30:00Z",
"shipped": false
}Elasticsearch creates the index and generates this mapping automatically:
{
"orders": {
"mappings": {
"properties": {
"customer": {
"type": "text", // ← also gets .keyword sub-field
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"total": { "type": "float" },
"placed_at": { "type": "date" },
"shipped": { "type": "boolean" }
}
}
}
}Elasticsearch made four decisions on your behalf. Three are probably fine. The fourth — mapping customer as text — might not be what you wanted if you only ever need exact matching or aggregations, where keyword alone would suffice.
Advantages of Dynamic Mapping
Pros
+ New fields added automatically as your data evolves
+ Perfect for prototyping and exploratory analysis
+ Good enough for log ingestion pipelines
+ No friction during early development
When to rely on it
− Ad-hoc data exploration
− Schemas that change frequently
− Early stage of a project
Disadvantages of Dynamic Mapping
Cons
− All strings are double-mapped (
text + keyword) by default, using 2× storage− Field types cannot be changed after creation — requires full reindex
− Uncontrolled data can cause mapping explosions with thousands of fields
You can’t change a field type after the fact.
long, you can’t re-map it as keyword on the same index. The only path is to create a new index with the correct mapping and reindex all your data. Discovering this in production is painful.What Is Explicit Mapping in Elasticsearch
Explicit mapping means you define the index schema before indexing any documents. You specify every field, its type, and any additional configuration — analyzers, index options, doc_values, and so on.
You create an explicit mapping using the mappings block when creating an index:
PUT /orders
{
"mappings": {
"properties": {
"customer": { "type": "keyword" }, // exact match only
"total": { "type": "double" }, // decimal precision
"placed_at": { "type": "date" },
"shipped": { "type": "boolean" }
}
}
}From this point, Elasticsearch knows exactly what to expect. Any document with a field not in this mapping will either be rejected or ignored, depending on your dynamic setting.
Advantages of Explicit Mapping
Pros
+ Queries work as expected from day one, no type surprises
+ Set
dynamic: "strict" to reject unexpected fields and enforce data quality+ Prevents mapping explosions — you control what gets indexed
+ Lower storage footprint — no accidental double-mapping
Dynamic vs Explicit Mapping in Elasticsearch
| Dimension | Dynamic Mapping | Explicit Mapping |
|---|---|---|
| Setup time | None | Required upfront |
| Schema flexibility | High — new fields auto-added | Low — requires mapping update |
| Type safety | Inference-based, can be wrong | Guaranteed correct |
| Best for | Prototyping, log ingestion, exploration | Production search, analytics, known schemas |
| Mapping explosion risk | High with uncontrolled data | Low — you control what’s accepted |
| Query reliability | Depends on inference accuracy | High — types match query expectations |
| Changing field types | Requires reindex if wrong | Rarely needed if designed correctly |
The short version
Dynamic Mapping Example
Let’s walk through a realistic scenario where dynamic mapping creates a silent, production-breaking problem.
The event_id type conflict
event_id that looks like "EVT-00123". Dynamic mapping creates this field as text + keyword. Fine — for now.Three months later, a new service starts sending
"event_id": 123 — a bare integer. Documents start failing to index. Payment failure events are silently lost. You don’t find out until someone notices the payment dashboard is missing data.Here’s the exact sequence that causes the problem:
PUT /logs/_doc/1
{
"event_id": "EVT-00123",
"level": "info",
"message": "User login successful"
}
// Dynamic mapping creates: event_id → text + keywordPUT /logs/_doc/2
{
"event_id": 123, // ← integer, not string
"level": "error",
"message": "Payment failed"
}{
"error": {
"type": "mapper_parsing_exception",
"reason": "failed to parse field [event_id] of type [text]"
}
}The second document is rejected entirely
event_id before any documents were indexed.Explicit Mapping Example
Here’s the same log index rebuilt with explicit mapping, plus a practical example of using dynamic: "strict" to enforce your schema.
PUT /logs
{
"mappings": {
"dynamic": "strict", // unknown fields → rejected
"properties": {
"event_id": { "type": "keyword" }, // exact lookups only
"level": { "type": "keyword" }, // "info", "error", "warn"
"message": { "type": "text" }, // full-text search
"timestamp": { "type": "date" },
"service": { "type": "keyword" }, // aggregations
"duration_ms": { "type": "long" } // percentile queries
}
}
}A few intentional decisions worth unpacking:
event_id → keywordYou’ll never do full-text search on it — only exact lookups and aggregations. No need for the double mapping.
level → keywordLog levels like
"info", "error", "warn" are always matched exactly. Full-text analysis would be wasted work.message → textThe only field where you actually want tokenization and analysis — searching for words inside log messages.
Now watch what happens when an unexpected field arrives:
PUT /logs/_doc/1
{
"event_id": "EVT-00123",
"level": "info",
"message": "User login successful",
"timestamp": "2024-03-15T10:30:00Z",
"service": "auth",
"duration_ms": 42,
"unexpected_field": "oops" // ← not in mapping
}{
"error": {
"type": "strict_dynamic_mapping_exception",
"reason": "mapping set to strict, dynamic introduction of
[unexpected_field] within [_doc] is not allowed"
}
}Clear, immediate, actionable
A valid document indexes cleanly, and your aggregation query works exactly as expected:
GET /logs/_search
{
"size": 0,
"aggs": {
"errors_by_service": {
"filter": { "term": { "level": "error" } },
"aggs": {
"by_service": {
"terms": { "field": "service" }
}
}
}
}
}{
"aggregations": {
"errors_by_service": {
"doc_count": 142,
"by_service": {
"buckets": [
{ "key": "payment", "doc_count": 87 },
{ "key": "auth", "doc_count": 34 },
{ "key": "search", "doc_count": 21 }
]
}
}
}
}This works reliably because level and service are both keyword fields — designed for exactly this kind of aggregation.
Best Practices for Elasticsearch Mapping
Start dynamic, finish explicit
Always inspect what dynamic mapping created
GET /your-index/_mapping and review every field. Look for strings that should be keyword only, or numbers mapped as long that should be double.Use KEYWORD for filter / aggregate / sort fields
text where you genuinely need full-text search. Status codes, identifiers, category names, country codes — these should be keyword, not the default double mapping.Use index: false for fields you store but don't search
"index": false. They’ll be stored in _source but won’t consume index resources.Set ignore_malformed: true for fault-tolerant pipelines
Use dynamic: "false" as a middle ground
"strict", unknown fields are rejected. With "false", they’re silently ignored — stored in _source but not indexed. Good for semi-structured data.Escape hatch pattern
dynamic: "strict" at the root and create a dedicated metadata object with dynamic: true for flexible fields. This gives you schema enforcement where it matters and flexibility where you need it."mappings": {
"dynamic": "strict", // enforce schema at root
"properties": {
"event_id": { "type": "keyword" },
"message": { "type": "text" },
"metadata": { // flexible escape hatch
"type": "object",
"dynamic": true // dynamic fields allowed here
}
}
}Common Mapping Mistakes in Elasticsearch
Treating the first document as representative
"price": "149.99" (a string), price is mapped as text. Every future document sending "price": 149.99 (a number) will fail. Always index a representative sample — or use explicit mapping — before going to production.Ignoring the text + keyword double mapping
text with a .keyword sub-field. A large index with thousands of string fields, each double-mapped, uses significantly more disk and heap. Audit your mappings for fields that don’t need both.Thinking you can change a field type by sending new data
Forgetting that arrays don't have a special type
"tags": ["search", "elk"], Elasticsearch maps tags as text + keyword. A later document sending "tags": [1, 2, 3] fails. Mixed-type arrays need explicit mapping — and probably a data quality fix upstream.Using dynamic: "strict" without an escape hatch
metadata object pattern above instead.Conclusion
Dynamic mapping makes Elasticsearch approachable. You can start indexing without writing a single schema definition, and for many use cases — log ingestion, exploratory analysis, local development — that’s genuinely useful. But every decision dynamic mapping makes on your behalf is a decision you didn’t make consciously. Those implicit decisions accumulate and, eventually, one of them breaks something in production.
Explicit mapping is the practice of making those decisions deliberately. You choose which strings are text and which are keyword. You choose what happens when an unexpected field arrives. You choose the exact numeric type your aggregations need. That upfront investment pays off every time a query works exactly as designed.
The practical approach: use dynamic mapping to learn your data, then write an explicit mapping that encodes what you’ve learned. The two aren’t mutually exclusive — they’re stages in the lifecycle of a well-designed Elasticsearch index.
