Mapping & field types
|
This section documents the current Elasticsearch 9.x line (with 8.19 as the final 8.x release) as published at the Elasticsearch documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Kibana-only UIs, the ML/NLP model-management workflow, cross-cluster replication, and parts of the paid / serverless-only surface) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Elasticsearch iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
A mapping is the schema of an index: it lists every field, the type Elasticsearch stores it as, and the parameters that control how it is indexed and queried. Elasticsearch can infer a mapping from the first document it sees (dynamic mapping), but production indices almost always pin an explicit mapping so field types never depend on document order. This page covers both, the field types you will actually use, the per-field parameters, metadata fields, dynamic templates, and runtime fields.
Dynamic vs. explicit mapping
Create an index with an explicit mapping by passing mappings to the create-index call. Each field
names a type; sub-fields nest under properties.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html
PUT /articles
{
"mappings": {
"properties": {
"title": { "type": "text" },
"slug": { "type": "keyword" },
"word_count": { "type": "integer" },
"published_at":{ "type": "date" },
"draft": { "type": "boolean" }
}
}
}
View the mapping any time — the response also shows every field dynamic mapping added:
GET /articles/_mapping
GET /articles/_mapping/field/title // just one field
You can add new fields to an existing mapping, but you cannot change the type of an existing field
or its core parameters — doing so requires creating a new index with the corrected mapping and
reindexing into it (see
Indexing, CRUD & bulk). That
irreversibility is why an explicit mapping matters: if the first word_count Elasticsearch ever
sees is "1200" (a string) it maps the field as text, and every later numeric query on it is
wrong until you rebuild the index.
Controlling dynamic behaviour
The dynamic setting, set at the mapping root or on any object field, decides what happens when a
document contains a field the mapping does not mention. See
Dynamic mapping.
dynamic |
Effect on an unmapped field in an incoming document |
|---|---|
|
The field is added to the mapping, type guessed from the JSON value. |
|
The field is added as a runtime field — queryable, but not indexed, so it costs nothing at write time and does not grow the on-disk mapping. |
|
The field is ignored for indexing (still stored in |
|
The indexing request is rejected with an error. |
// Lock the mapping down: reject anything not declared.
PUT /events
{
"mappings": {
"dynamic": "strict",
"properties": {
"type": { "type": "keyword" },
"at": { "type": "date" },
"payload": { "type": "object", "dynamic": "true" } // but let payload.* grow freely
}
}
}
Field data types
Full list: Field data types. The ones below cover the large majority of real mappings.
text vs. keyword — the key distinction
This is the single most important choice in a mapping.
-
textruns the value through an analyzer at index time, breaking it into terms (lowercased, tokenised, often stemmed). Use it for full-text search —match,match_phrase. It has nodoc_values, so you cannot sort or aggregate on it, and an exact-matchtermquery against it usually fails because the stored terms are analyzed. -
keywordstores the value as a single, verbatim term. Use it for IDs, enums, tags, hostnames, status codes — anything you filter on exactly, sort by, or aggregate.matchon akeywordbehaves liketerm.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/text.html
// https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"raw": { "type": "keyword", "ignore_above": 256 }
}
}
}
}
}
The example above is the text + keyword multi-field idiom: index name as text for
relevance-ranked search, and get a name.raw sub-field, extracted from the same JSON value, as
keyword for exact filters, sorting, and terms aggregations. This is what dynamic mapping does
automatically for every string field — it creates <field> as text with a <field>.keyword
sub-field capped at ignore_above: 256.
GET /products/_search
{
"query": { "match": { "name": "wireless keyboard" } },
"sort": [ { "name.raw": "asc" } ],
"aggs": { "by_name": { "terms": { "field": "name.raw" } } }
}
match_only_text
A space-efficient variant of text: it drops the positional and scoring index structures, so it
uses far less disk, at the cost of slower phrase queries and no relevance scoring on that field.
Ideal for log message bodies and other high-volume text you search but rarely rank. See
match_only_text.
PUT /logs
{ "mappings": { "properties": { "message": { "type": "match_only_text" } } } }
Numeric types and scaled_float
long, integer, short, byte, double, float, half_float, and scaled_float. Pick the
smallest type that fits the range — it saves disk and memory. scaled_float stores a
floating-point value as a long multiplied by a fixed scaling_factor, which is both smaller and
more exact than float for fixed-precision data such as money. See
Numeric field types.
PUT /orders
{
"mappings": {
"properties": {
"quantity": { "type": "integer" },
"price": { "type": "scaled_float", "scaling_factor": 100 } // cents
}
}
}
date
Stored internally as a long of milliseconds since the epoch (UTC). Accepts an ISO-8601 string, an
epoch number, or any pattern listed in format (||-separated). See
date field type.
PUT /sensor
{
"mappings": {
"properties": {
"reading_at": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd"
}
}
}
}
Use date_nanos instead when you need nanosecond resolution (high-frequency tracing).
boolean
Accepts JSON true/false and the strings "true"/"false". See
boolean field type.
PUT /flags { "mappings": { "properties": { "active": { "type": "boolean" } } } }
object vs. nested
A plain JSON object is mapped as type object and flattened internally: author.first and
author.last become independent fields. That is fine until the object is inside an array, where
flattening loses the association between sibling values — a search for an actor named "John" in the
"lead" role would also match a document where "John" and "lead" came from two different array
entries.
nested fixes this by indexing each array object as a hidden separate document, so a nested query
keeps the fields of one object together. The cost: nested sub-documents count against the shard doc
limit and must be queried with a dedicated nested query/aggregation. See
nested field type and
object field type.
PUT /movies
{
"mappings": {
"properties": {
"cast": {
"type": "nested",
"properties": {
"name": { "type": "keyword" },
"role": { "type": "keyword" }
}
}
}
}
}
GET /movies/_search
{
"query": {
"nested": {
"path": "cast",
"query": {
"bool": { "must": [
{ "match": { "cast.name": "John" } },
{ "match": { "cast.role": "lead" } }
] }
}
}
}
}
flattened
Maps an entire object — with all of its nested keys — as a single keyword-like field, so
arbitrary, unpredictable key names never enlarge the mapping. Everything under it is exact-match
only (no full-text analysis, no numeric ranges). Good for bags of labels or third-party metadata.
See
flattened field type.
PUT /issues
{ "mappings": { "properties": { "labels": { "type": "flattened" } } } }
PUT /issues/_doc/1
{ "labels": { "priority": "high", "team": "search", "sprint": "42" } }
GET /issues/_search
{ "query": { "term": { "labels.priority": "high" } } }
ip and range
ip accepts IPv4 and IPv6 and supports CIDR queries. The *_range types (integer_range,
long_range, float_range, double_range, date_range, ip_range) store an interval in one
field and match with gt/gte/lt/lte or a relation of within/contains/intersects. See
ip field type and
Range field types.
PUT /leases
{
"mappings": {
"properties": {
"client_ip": { "type": "ip" },
"valid": { "type": "date_range" }
}
}
}
PUT /leases/_doc/1
{ "client_ip": "10.0.0.42", "valid": { "gte": "2026-01-01", "lte": "2026-12-31" } }
GET /leases/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "client_ip": "10.0.0.0/24" } },
{ "range": { "valid": { "gte": "2026-06-01", "lte": "2026-06-01", "relation": "contains" } } }
]
}
}
}
Specialised types
These have dedicated pages — the mapping is only the entry point:
-
geo_point(lat/lon) andgeo_shape(polygons, lines) — see Geospatial. -
dense_vectorandsparse_vectorfor kNN and semantic search — see Vector & semantic search. -
joinfor a parent/child relationship within one index (the modern replacement for the removed_parentfield) — see Joins & relationships. -
completionfor as-you-type autocomplete suggestions — see Search extras.
Mapping parameters
Parameters are set per field alongside type. Full list:
Mapping parameters.
fields-
Declares multi-fields — extra indexed views of the same source value under a different type or analyzer, as in the
name/name.rawidiom above. Adding a multi-field to an existing mapping is allowed (only already-indexed docs need a reindex to populate it). analyzer/search_analyzer-
The analyzer applied to a
textfield at index time, and (if different) at query time — e.g. index with an edge-ngram analyzer for autocomplete but search with a plainstandardanalyzer. See Text analysis. format-
Accepted date patterns for a
datefield (see above). index-
falseturns off the searchable index for a field while keeping it in_sourceand (for most types) indoc_values, so it can still be aggregated and sorted but not filtered byquery. doc_values-
The columnar on-disk structure that powers sorting, aggregations, and scripting. On by default for every type except
text; setfalseto save disk on a field you will never sort or aggregate. norms-
Per-field length normalisation factors used in relevance scoring. Disable (
"norms": false) ontextfields you only filter on, never rank, to reclaim space. copy_to-
Copies this field’s value into another (usually a catch-all
text) field at index time, so you canmatchacross several fields with one query withoutmulti_match. ignore_above-
For
keyword, values longer than this many characters are stored in_sourcebut not indexed — stops a stray huge string from bloating the terms dictionary. The dynamic-mapping default for.keywordsub-fields is256. null_value-
A substitute term indexed when the JSON value is
null(a realnullis otherwise unsearchable). Must be the same type as the field.
PUT /people
{
"mappings": {
"properties": {
"first_name": { "type": "text", "copy_to": "full_name" },
"last_name": { "type": "text", "copy_to": "full_name" },
"full_name": { "type": "text" },
"bio": { "type": "text", "norms": false },
"country": { "type": "keyword", "null_value": "UNKNOWN" },
"legacy_id": { "type": "keyword", "index": false, "doc_values": false }
}
}
}
Metadata fields
Every document carries built-in _-prefixed fields alongside its own. See
Metadata fields.
_source-
The original JSON, stored verbatim and returned in every hit. You can disable it or filter it, but disabling breaks reindex, update, the Update-by-query API, and highlighting on non-stored fields — prefer
includes/excludes, or synthetic_source, over turning it off. See_sourcefield.PUT /telemetry { "mappings": { "_source": { "excludes": [ "raw_frame" ] // keep it searchable, drop it from stored JSON }, "properties": { "raw_frame": { "type": "text" } } } } _routing-
The value that decides which shard a document lands on (defaults to
_id). Make it a required, explicit value to co-locate related documents on one shard; ajoinmapping makes it mandatory. See_routingfield.PUT /orders/_doc/1?routing=customer-42 { "customer": "customer-42", "total": 19.99 } GET /orders/_search?routing=customer-42 { "query": { "term": { "customer": "customer-42" } } } _meta-
An arbitrary object for application-level metadata about the mapping itself (schema version, owning team). Elasticsearch never interprets it, and it can be updated after index creation. See
_metafield.PUT /articles/_mapping { "_meta": { "schema_version": 3, "owner": "content-platform" } }
Dynamic templates
Dynamic templates let dynamic mapping follow your rules instead of Elasticsearch’s guesses: match
by the detected JSON type (match_mapping_type), by field-name glob (match / unmatch), or by
dotted path (path_match / path_unmatch), and apply a mapping to every field that matches. See
Dynamic templates.
PUT /catalog
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keyword": {
"match_mapping_type": "string",
"mapping": { "type": "keyword", "ignore_above": 1024 }
}
},
{
"metrics_as_double": {
"path_match": "metrics.*",
"mapping": { "type": "double" }
}
}
]
}
}
The first template overrides the built-in "string becomes text + keyword" behaviour with plain
keyword; the second maps everything under metrics as double no matter what the first
document’s values look like.
Runtime fields
A runtime field is defined in the mapping (or supplied per query) but evaluated by a Painless
script at query time from _source or from other fields — schema-on-read rather than
schema-on-write. It adds no storage and no indexing cost, it can be added or changed on a live index
without a reindex, but every query that touches it pays the script cost per matching document. See
Runtime fields.
// In the mapping: a field that does not exist on disk.
PUT /http_logs
{
"mappings": {
"runtime": {
"day_of_week": {
"type": "keyword",
"script": "emit(doc['timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
},
"properties": { "timestamp": { "type": "date" }, "status": { "type": "integer" } }
}
}
// At query time only, via runtime_mappings -- nothing added to the index:
GET /http_logs/_search
{
"runtime_mappings": {
"status_class": {
"type": "keyword",
"script": "emit((doc['status'].value / 100) + 'xx')"
}
},
"query": { "term": { "status_class": "5xx" } },
"fields": [ "status_class", "day_of_week" ],
"aggs": { "by_class": { "terms": { "field": "status_class" } } }
}
Retrieve runtime values with the fields parameter of the search request (they are absent from
_source). A common pattern is to explore a new field as a runtime field, then promote it to an
indexed field once the query shape is settled and the query volume justifies the disk.
Preventing mapping explosion
Uncontrolled dynamic mapping — especially over documents with unpredictable keys — can create thousands of fields, which inflates the cluster state and slows every operation. Guardrails:
-
index.mapping.total_fields.limit(default1000) caps the number of fields in the mapping. Raising it is a smell; fixing the data model is better. -
index.mapping.depth.limitcaps object nesting;index.mapping.nested_fields.limitcapsnestedfield count. -
Set
dynamictostrictorruntime, or use aflattenedfield, wherever keys are open-ended.
PUT /wide_index/_settings
{ "index.mapping.total_fields.limit": 2000 }
See Settings to prevent mapping explosion. This schema-on-write discipline is the opposite end of the spectrum from a document database that imposes no field limit at all — contrast MongoDB schema design, where flexible-schema collections push the same structural decisions entirely into the application.