Elasticsearch index templates are the closest thing you’ll get to schema-on-write for a schemaless database, and they’re absolutely critical for predictable query performance.
Let’s watch an index template in action. Imagine we’re storing user activity logs. We want to ensure that user_id is always a keyword type for fast aggregations and timestamp is always a date type for time-based queries.
PUT _template/user_activity_template
{
"index_patterns": ["user_activity-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"user_id": {
"type": "keyword"
},
"timestamp": {
"type": "date",
"format": "epoch_millis||yyyy-MM-dd HH:mm:ss.SSS"
},
"event_type": {
"type": "keyword"
},
"details": {
"type": "object",
"enabled": false
}
}
}
}
}
When we create an index that matches the index_patterns (like user_activity-2023-10-27), this template is automatically applied.
PUT user_activity-2023-10-27
Now, if we send a document to this index:
POST user_activity-2023-10-27/_doc
{
"user_id": "user-123",
"timestamp": 1698393600000,
"event_type": "login",
"details": {
"ip_address": "192.168.1.1"
}
}
Elasticsearch will use the template’s mapping. user_id will be indexed as a keyword. The timestamp will be parsed correctly because we provided multiple formats. event_type is also a keyword. Notice details is marked as enabled: false. This means the details object’s fields won’t be indexed individually, saving space and indexing time if we never need to query within details.
The core problem index templates solve is the drift in mappings that inevitably happens in a dynamic environment. Without them, if you index a document with a new field, Elasticsearch might infer a text type for a field you intended to be a keyword (for aggregations). Or, it might infer a long for a field that sometimes contains floating-point numbers, leading to NumberFormatException errors later. Templates preempt this by defining the desired structure before any data is indexed.
The index_patterns are glob-style patterns that determine which indices the template applies to. A common pattern is my_index_name-* to apply to all indices starting with my_index_name_. You can also use multiple patterns with a comma-separated list.
Inside the template object, you define settings (like shard and replica counts) and mappings. The mappings section is where you specify field types. For text fields, keyword is generally preferred for exact matches, filtering, and aggregations, while text is for full-text search and requires analysis (tokenization, stemming, etc.). Date fields are crucial for time-series data, and specifying format allows Elasticsearch to parse various string representations. For nested objects, object is the default, but you can disable dynamic mapping of its sub-fields with enabled: false if you only want to store them as a JSON blob.
The order of index templates matters. If multiple templates match an index, the one with the most specific index_patterns takes precedence. If specificity is equal, the template with the higher order value (a number you can specify, default is 0) wins. This allows you to have a general template for all indices and more specific templates for particular index types.
When you update an index template, it does not retroactively change existing indices. It only affects new indices created after the template update and documents indexed into existing indices that don’t have explicit mappings for those fields. To update mappings on existing data, you typically need to reindex the data into a new index with the correct mappings.
The dynamic mapping parameter within mappings (or at the field level) controls how Elasticsearch handles fields not explicitly defined in the template. dynamic: true (the default) means new fields are added to the mapping. dynamic: false means new fields are ignored. dynamic: strict means new fields will cause an error. Index templates are the best place to set this, often on a parent object to control the dynamism of its children.
Understanding how Elasticsearch merges mappings when multiple templates apply is key to advanced configuration. When multiple templates match, Elasticsearch merges their mappings. If a field is defined in multiple templates, the definition from the template with the higher order value (or more specific index_patterns) will be used. For settings like number_of_shards, these are typically not merged; the values from the most specific or highest-ordered template usually win.
The next hurdle you’ll face is managing index lifecycle policies (ILM) to automatically handle index creation, rollover, and deletion based on age or size.