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

true (default)

The field is added to the mapping, type guessed from the JSON value.

runtime

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.

false

The field is ignored for indexing (still stored in _source, so it is returned in hits but cannot be searched or aggregated).

strict

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.

  • text runs 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 no doc_values, so you cannot sort or aggregate on it, and an exact-match term query against it usually fails because the stored terms are analyzed.

  • keyword stores 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. match on a keyword behaves like term.

// 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) and geo_shape (polygons, lines) — see Geospatial.

  • dense_vector and sparse_vector for kNN and semantic search — see Vector & semantic search.

  • join for a parent/child relationship within one index (the modern replacement for the removed _parent field) — see Joins & relationships.

  • completion for 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.raw idiom 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 text field at index time, and (if different) at query time — e.g. index with an edge-ngram analyzer for autocomplete but search with a plain standard analyzer. See Text analysis.

format

Accepted date patterns for a date field (see above).

index

false turns off the searchable index for a field while keeping it in _source and (for most types) in doc_values, so it can still be aggregated and sorted but not filtered by query.

doc_values

The columnar on-disk structure that powers sorting, aggregations, and scripting. On by default for every type except text; set false to save disk on a field you will never sort or aggregate.

norms

Per-field length normalisation factors used in relevance scoring. Disable ("norms": false) on text fields 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 can match across several fields with one query without multi_match.

ignore_above

For keyword, values longer than this many characters are stored in _source but not indexed — stops a stray huge string from bloating the terms dictionary. The dynamic-mapping default for .keyword sub-fields is 256.

null_value

A substitute term indexed when the JSON value is null (a real null is 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 _source field.

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; a join mapping makes it mandatory. See _routing field.

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 _meta field.

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 (default 1000) caps the number of fields in the mapping. Raising it is a smell; fixing the data model is better.

  • index.mapping.depth.limit caps object nesting; index.mapping.nested_fields.limit caps nested field count.

  • Set dynamic to strict or runtime, or use a flattened field, 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.