Mapping: Dynamic vs Explicit

intermediate elasticsearch mapping schema

Mapping is ES’s word for schema. Which fields exist, what types they are, how they get analyzed. There are two flavors: dynamic (ES guesses) and explicit (we declare).

Dynamic mapping — the prototype mode

If we index a document into a non-existent index, ES will:

  1. Create the index.
  2. Look at each field in the JSON.
  3. Guess the type based on the value.
  4. Save that mapping forever.
POST /products/_doc
{
  "title": "Sony WH-1000XM5",
  "price": 399,
  "in_stock": true,
  "created_at": "2026-05-26T10:30:00Z",
  "tags": ["audio", "wireless"]
}

ES infers:

  • titletext (with a .keyword sub-field)
  • pricelong
  • in_stockboolean
  • created_atdate
  • tagstext (with .keyword)

Sounds convenient, right? Until things break.

Where dynamic mapping bites you
- First doc has price: 399 → mapped as long. Next doc has price: 399.99 → rejected.
- First doc has id: "abc123" → mapped as text. Now you can't sort or aggregate on it without pain.
- Field explosion: someone sends { "metadata": { "click_1": ..., "click_2": ... } } dynamically. Your mapping grows by one field per request.
- Dates: "2026-05-26" mapped as date. Then someone sends "May 26 2026" → rejected.

Explicit mapping — the production mode

For anything serious, declare your schema upfront:

PUT /products
{
  "mappings": {
    "properties": {
      "title":      { "type": "text" },
      "sku":        { "type": "keyword" },
      "price":      { "type": "scaled_float", "scaling_factor": 100 },
      "in_stock":   { "type": "boolean" },
      "created_at": { "type": "date", "format": "strict_date_optional_time" },
      "tags":       { "type": "keyword" }
    }
  }
}

Now we have predictable types, we can’t accidentally pollute the mapping, and bad data gets rejected early.

Adding new fields later

Mappings are mostly append-only. We can add a new field, but we can’t change an existing field’s type.

PUT /products/_mapping
{
  "properties": {
    "discount_pct": { "type": "float" }
  }
}

This works. But trying to change title from text to keyword? Nope — you’d have to reindex into a new index with the corrected mapping.

Controlling dynamic behavior

We don’t have to choose all-or-nothing. The dynamic setting has three values:

  • true (default) — new fields are added to the mapping automatically
  • false — new fields are stored in _source but NOT indexed (invisible to search)
  • strict — new fields cause the document to be rejected
PUT /products
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "title": { "type": "text" },
      "sku":   { "type": "keyword" }
    }
  }
}

// This now FAILS:
POST /products/_doc
{ "title": "Sony XM5", "sku": "SONY-001", "random_field": "oops" }
// → strict_dynamic_mapping_exception

Strict mapping is the safest default for production. It forces you to be intentional.

Dynamic templates — the middle ground

Sometimes we want some flexibility — e.g. “any field that ends in _id should be a keyword.” That’s what dynamic templates are for:

PUT /events
{
  "mappings": {
    "dynamic_templates": [
      {
        "ids_as_keyword": {
          "match": "*_id",
          "mapping": { "type": "keyword" }
        }
      },
      {
        "strings_as_keywords": {
          "match_mapping_type": "string",
          "mapping": { "type": "keyword", "ignore_above": 1024 }
        }
      }
    ]
  }
}

Now user_id, session_id, product_id all become keywords automatically, and any other string becomes a keyword too (not text). Super useful for log indices where the shape varies.

The TL;DR

  • Prototyping locally? Dynamic mapping is fine.
  • Production? Explicit mapping. Maybe dynamic: strict to catch typos.
  • Log/event data with unknown shape? Dynamic templates with sensible defaults.
  • Need to change a field’s type? Reindex into a new index. There’s no in-place ALTER COLUMN.