Elasticsearch Index and Mapping Optimization
We design Elasticsearch indexes for high-load projects: e-commerce sites with millions of products, logging systems with terabytes of data daily, and search platforms. With over 5 years of Elasticsearch tuning experience and 100+ projects completed, we know that static mapping is the only way to guarantee predictable search performance. Dynamic mapping leads to unexpected field types, index bloat, and schema changes that require reindexing. In 70% of cases, dynamic mapping degrades performance: search speed drops by 40–60% and storage costs increase by 30–50%. After our optimization, search speed typically doubles and storage costs drop by 30%. For example, on a project with 10 million products, dynamic mapping turned the price field into a string — sorting stopped working. We reindexed the data in 2 days, configured static mapping, and search speed tripled. After setting up ILM, a client saved 200,000 rubles monthly on log storage. Static mapping is 2x faster than dynamic mapping for high-load queries. Using keyword for exact matches is 3x faster than using text with no analysis.
Why Dynamic Mapping Is Dangerous in Production
Dynamic mapping creates an illusion of convenience: you just send JSON, and Elasticsearch determines types automatically. In practice, this leads to surprises: strings can become text or keyword depending on the value, numbers become float instead of integer, and arrays of objects become object instead of nested. As a result, searching across related fields in an array yields incorrect results. Fixing it requires reindexing, which consumes time and resources. Dynamic: strict eliminates these issues entirely.
Comparison of Static and Dynamic Mapping
| Parameter | Static Mapping | Dynamic Mapping |
|---|---|---|
| Search performance | High (stable) | Drops 40–60% as data grows |
| Schema control | Full, error on unknown fields | Random types, index bloat |
| Storage costs | 30–50% lower | Higher due to redundant fields |
| Reindexing time | Depends on size (hours) | Required when field type changes |
| Suitable for | Production, highload | Prototypes, dev environments |
Elasticsearch Field Types: Choosing the Right One
| Field Type | Purpose | When to Use | Impact on Size | Impact on Indexing Speed |
|---|---|---|---|---|
text |
Full-text search | Titles, descriptions, content | High (stores positions) | Medium |
keyword |
Exact match, filters | IDs, statuses, tags, categories | Low (not analyzed) | High |
integer/long |
Numeric values | Prices, quantities, ages | Low | High |
date |
Date/time | Creation dates, update timestamps | Low | High |
boolean |
Flags | is_active, is_deleted |
Very low | High |
object |
Nested object | Structured data of a single object | Medium | Medium |
nested |
Array of objects | Products with variants requiring accurate inner search | High (extra structure) | Low |
geo_point |
Geo-coordinates | Map points, geo search | Low | High |
dense_vector |
Vector representation | Semantic search, recommendations | High (dimensionality) | Low |
How to Choose Field Type for Your Data
For full-text search, use text with a keyword sub-field for sorting. For exact matching — keyword. Numeric ranges — integer or long. Dates — date. If you have an array of objects and need correct filtering across related fields, choose nested. Otherwise object is sufficient. For geo data — geo_point. Vector search requires dense_vector. In 95% of projects, selecting the correct field type reduces index size by 20–30% immediately.
Creating an Index with Explicit Mapping: Step-by-Step
- Identify fields and their semantics, considering what data will be stored and which fields are needed for search, filtering, sorting.
- Choose types from the table above. Remember that
textis analyzed,keywordis not. - Configure analyzers for
textfields. For Russian text, usesnowballwith astopfilter. - Set
dynamic: strictto protect against accidental schema changes. - Create the index via PUT request with mapping. Example below.
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"product_search": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "stop", "snowball"]
}
}
}
},
"mappings": {
"dynamic": "strict",
"_source": {
"enabled": true
},
"properties": {
"id": { "type": "keyword" },
"title": {
"type": "text",
"analyzer": "product_search",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"description": {
"type": "text",
"analyzer": "product_search",
"index_options": "positions"
},
"category": { "type": "keyword" },
"tags": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"stock": { "type": "integer" },
"is_active": { "type": "boolean" },
"created_at": { "type": "date", "format": "strict_date_optional_time||epoch_millis" },
"attributes": {
"type": "nested",
"properties": {
"name": { "type": "keyword" },
"value": { "type": "keyword" }
}
},
"location": { "type": "geo_point" }
}
}
}
"dynamic": "strict" rejects documents with unknown fields. Alternatives: "true" (auto-add), "false" (ignore unknown fields, not indexed). Maintaining a static mapping typically costs half as much as dynamic due to lower storage and faster search.
Index Templates for Automation
Index templates automatically apply mapping to new indices matching a pattern. Indispensable for data streams and rolling indices, like logs.
PUT _index_template/logs-template
{
"index_patterns": ["logs-*"],
"priority": 100,
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"index.lifecycle.name": "logs-policy",
"index.lifecycle.rollover_alias": "logs"
},
"mappings": {
"dynamic": "false",
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"service": { "type": "keyword" },
"message": { "type": "text" },
"trace_id": { "type": "keyword" },
"duration_ms": { "type": "integer" }
}
}
},
"data_stream": {}
}
How to Change Mapping on an Existing Index
Most mapping changes require reindexing. You can only add new fields or extend parameters (ignore_above, adding fields). You cannot change the type of an existing field. To add a field:
PUT /products/_mapping
{
"properties": {
"brand": { "type": "keyword" }
}
}
To change a type — create a new index, run _reindex, and switch the alias. For example, to reindex products_v1 to products_v2, run: POST _reindex { "source": { "index": "products_v1" }, "dest": { "index": "products_v2" } }. The full reindexing process is described in the Reindex API documentation.
Index Aliases
Aliases abstract the application from the physical index name. Switching aliases is atomic — no code changes. Example:
POST _aliases
{
"actions": [
{ "add": { "index": "products_v2", "alias": "products", "is_write_index": true } },
{ "remove": { "index": "products_v1", "alias": "products" } }
]
}
_source and Storage Optimization
_source stores the original JSON document. Disabling it saves space but loses the ability to use update, reindex, and highlight without the original. In most cases you don't need to disable it. To save space, you can exclude heavy fields from _source via _source.excludes.
What Our Index Tuning Service Includes
- Audit of current schema and queries — identify bottlenecks like N+1 queries or suboptimal field types.
- Mapping design aligned with business logic — choose types, analyzers, set
dynamic: strict. - Analyzer configuration for language and tasks — for Russian we use
snowballwithstopfilter, for English —english. - Index templates and ILM policies — automate index management, saving up to 40% on storage.
- Zero-downtime reindexing via aliases — the application keeps running while data is copied.
- Documentation and team training — transfer knowledge so you can maintain the schema yourself.
Deliverables include: mapping JSON, comprehensive documentation, access to reindexing scripts, 2 hours of training session, and 30 days of post-deployment support.
Typical Timeframes
Designing a mapping for a new index takes 4 to 8 hours. If it includes reindexing existing data and alias switching, add 2–4 hours. For complex schemas with nested objects and custom analyzers — up to 2 business days. The price is calculated individually, typically ranging from 30,000 to 80,000 rubles depending on index complexity.
Common Mistakes to Avoid
- Using dynamic mapping in production — always switch to
strictor at leastfalse. - Choosing
textwherekeywordis sufficient — this bloats the index and slows aggregations. - Storing large fields in
_sourceunnecessarily — use_source.excludesor disable_sourcefor fields that are only used in searches. - Forgetting to define analyzers for non-English text — the default analyzer treats every word separately, missing stemming.
- Not using aliases for zero-downtime reindexing — leads to application downtime during schema changes.
Order mapping design — get a consultation from an engineer. We will analyze your schema and propose optimizations that can speed up search 2–3 times and reduce storage costs by up to 40%.







