Introduction
Elasticsearch's dynamic mapping feels like a feature designed to save time—tell it nothing about your schema and it'll figure it out automatically. In practice, this convenience becomes a liability. The consequences range from silent data loss to cluster bloat that brings search latency grinding to a halt. This guide walks through the most common dynamic mapping pitfalls and how we've solved them in production systems handling millions of events per day.
Numbers stored as strings
One of the most insidious problems we encounter is Elasticsearch inferring the wrong type for numeric data. When a field arrives as a string in JSON, dynamic mapping will index it as text by default. This means you lose numeric operations—range queries, aggregations, and sorting all become either impossible or produce nonsensical results.
Consider a real scenario: your payment service sends transaction amounts as strings (common when parsing from APIs or legacy systems). Elasticsearch sees "amount": "99.50" and treats it as text. Later, a business analyst tries to query transactions over $100 and gets zero results, or queries return data in lexicographic order rather than numeric order.
Here's how dynamic mapping misinterprets the data:
{
"transaction_id": "txn_12345",
"amount": "99.50",
"currency": "USD",
"timestamp": "2025-01-07T14:30:00Z"
}Elasticsearch creates a mapping like this:
{
"properties": {
"amount": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}The fix is explicit mapping. Define your index with the correct types upfront, or use dynamic templates to handle numeric strings intelligently. We'll cover templates in the next section, but the safest approach is always to define critical fields explicitly:
{
"mappings": {
"properties": {
"amount": {
"type": "scaled_float",
"scaling_factor": 100
},
"currency": {
"type": "keyword"
},
"timestamp": {
"type": "date"
}
}
}
}If you can't control the source data format, parse it during ingestion—use Logstash, a Kafka processor, or your application layer to convert strings to numbers before they reach Elasticsearch.
Mapping explosions from user keys
Imagine you're building a SaaS platform where customers can log custom metadata with their events. Each customer can add arbitrary key-value pairs. Without guardrails, Elasticsearch's dynamic mapping will create a new field in your mapping for every unique key a customer sends. We've seen this escalate from a clean mapping with 50 fields to one with 50,000 fields within a few weeks of production traffic.
This is a mapping explosion. Each field consumes memory in the cluster, slows down indexing, and bloats shard metadata. Queries become slower because Elasticsearch must consider more fields during analysis.
A concrete example: a customer sends this event:
{
"event_type": "page_view",
"user_id": "user_789",
"custom_metadata": {
"utm_source": "google",
"utm_medium": "cpc",
"page_load_ms": "1250"
}
}Then another customer sends:
{
"event_type": "page_view",
"user_id": "user_456",
"custom_metadata": {
"experiment_variant": "control",
"session_id": "sess_999",
"page_load_ms": "890"
}
}With dynamic mapping enabled, your mapping now includes four separate fields under custom_metadata. Multiply this across thousands of customers and millions of events, and you're suddenly managing hundreds of thousands of fields.
The solution is to disable dynamic mapping for the problematic object and use a more constrained approach. You have several options:
- Flatten the custom data into a single field: Store custom metadata as a JSON string or use Elasticsearch's
flattenedtype, which treats all nested keys as a single searchable field without creating separate mappings. - Define allowed fields explicitly: If you can enumerate the custom fields your customers need, define them upfront in a dynamic template.
- Disable dynamic mapping for that object: Set
dynamic: falseon thecustom_metadataobject so new fields are ignored.
Here's a mapping using the flattened type:
{
"mappings": {
"properties": {
"event_type": {
"type": "keyword"
},
"user_id": {
"type": "keyword"
},
"custom_metadata": {
"type": "flattened"
}
}
}
}The flattened type lets you search and filter on any key without creating individual fields. Trade-off: you can't aggregate or sort on individual custom fields, but you avoid the mapping explosion entirely.
Set dynamic: strict for control
Once you've defined the fields your index should contain, enforce it. Setting dynamic: strict tells Elasticsearch to reject any document that introduces a field not in your mapping. This is a hard stop—the indexing request fails and you get an error back immediately.
This feels restrictive, but it's one of the best safeguards we've deployed in production. It catches data quality issues early and prevents silent mapping drift. If a new field suddenly appears in your data pipeline, you'll know about it the moment it reaches Elasticsearch, not after you've indexed millions of malformed documents.
Example mapping with dynamic: strict:
{
"mappings": {
"dynamic": "strict",
"properties": {
"event_id": {
"type": "keyword"
},
"event_type": {
"type": "keyword"
},
"timestamp": {
"type": "date"
},
"user_id": {
"type": "keyword"
}
}
}
}If a document arrives with an unexpected field like "tracking_id", indexing fails with a mapping exception. You then update the mapping to include the new field, and retry.
For development and testing, you might use dynamic: true (the default) to iterate quickly. But as soon as your index goes to production, switch to dynamic: strict. The minimal friction of updating mappings is worth the guarantee that you won't silently accept malformed data.
Use dynamic templates for patterns
Not all fields can be known in advance, but many follow predictable patterns. Dynamic templates let you define rules that apply automatically to fields matching a condition. A field is created according to the template rule before it becomes a free-form dynamic field.
Common patterns we template include:
- Fields ending in
_dateor_time→ map todatetype - Fields ending in
_countor_ms→ map tointegerorlongtype - Fields with names like
ip_addressorclient_ip→ map toiptype - Fields prefixed with
raw_→ map tokeyword(not analyzed)
Here's a practical example. Your logging pipeline sends various metrics and timestamps, and you want them typed correctly without defining every single field:
{
"mappings": {
"dynamic_templates": [
{
"dates": {
"match_pattern": "regex",
"match": ".*_date|.*_time",
"mapping": {
"type": "date"
}
}
},
{
"integers": {
"match_pattern": "regex",
"match": ".*_count|.*_ms|.*_seconds",
"mapping": {
"type": "long"
}
}
},
{
"keywords": {
"match_pattern": "regex",
"match": "raw_.*",
"mapping": {
"type": "keyword"
}
}
},
{
"strings": {
"match": "*",
"mapping": {
"type": "text"
}
}
}
],
"properties": {
"event_id": {
"type": "keyword"
}
}
}
}Now when a document with "created_date": "2025-01-07" arrives, the template matches the pattern and automatically maps it as a date field. A field like "response_ms": 250 becomes a long. This gives you flexibility for undeclared fields while maintaining type safety through pattern matching.
Templates are evaluated in order, so list them from most specific to least specific. The catch-all template at the end ensures any field that doesn't match earlier patterns still gets a sensible default type.
Common mistakes
Assuming dynamic mapping is "good enough" for production: It's not. Dynamic mapping optimizes for convenience, not correctness. Enable it only during development and testing, or pair it with strict enforcement and templates in production.
Ignoring mapping errors during indexing: If a field gets the wrong type, it's nearly impossible to fix without reindexing. Pay attention to errors and warnings during ingestion.
Over-using dynamic templates without testing: A template that matches too broadly can mask real issues. Test templates with sample data before deploying to production.
Forgetting to set dynamic: false on nested objects: You can disable dynamic mapping at the object level, which prevents mapping explosions in nested or dynamic sections of your documents while allowing controlled dynamic mapping elsewhere.
Key takeaways
- Define numeric fields explicitly in your mapping; never rely on dynamic inference for numeric data.
- Use
dynamic: strictin production indices to catch data quality issues early and prevent silent mapping drift. - For custom or user-supplied metadata, use the
flattenedtype or setdynamic: falseto avoid mapping explosions. - Leverage dynamic templates to enforce type rules on fields matching known patterns, giving you flexibility without sacrificing control.
- Test your mappings thoroughly with realistic data before going live; a poorly chosen mapping is expensive to fix at scale.






Comments
Loading comments…