ELK Stack — Mappings

5 minute read

Elasticsearch Mappings

A mapping is the schema definition for an Elasticsearch index. It tells Elasticsearch what type each field is — text, keyword, integer, date, boolean, IP, geo_point, etc. — and how it should be indexed and searched.

Why Mappings Matter

Without explicit mappings, Elasticsearch guesses field types when it first sees a document. This dynamic mapping is convenient but often wrong:

// You index this document:
{ "status_code": "200", "response_time": "1.5" }

// Elasticsearch guesses:
// status_code → text (but you want integer!)
// response_time → text (but you want float!)

Now you can’t do status_code > 400 or aggregate on response_time because they’re stored as text, not numbers.

Rule: Always define explicit mappings for production indices.

Core Field Types

Type Description Example Values
text Full-text searchable (analyzed) “Hello world”, “Error in UserService”
keyword Exact value (not analyzed) “GET”, “error”, “192.168.1.50”
integer 32-bit integer 200, 404, 500
long 64-bit integer 1234567890
float 32-bit floating point 1.5, 99.99
double 64-bit floating point 3.141592653589793
boolean True or false true, false
date Date/time “2025-03-15T10:00:00Z”
ip IPv4 or IPv6 “192.168.1.50”
geo_point Latitude/Longitude {“lat”: 40.7, “lon”: -74.0}
object JSON object {“name”: “John”, “age”: 30}
nested Array of objects (preserves relationship) [{“name”: “A”}, {“name”: “B”}]

text vs keyword — The Key Difference

This is the most important distinction in Elasticsearch:

Aspect text keyword
Analyzed Yes (broken into tokens) No (stored as-is)
Use for Full-text search Filtering, sorting, aggregations
Example “Error in UserService” → [“error”, “in”, “userservice”] “GET” → “GET”
Search Match query (fuzzy, partial) Term query (exact match)

Example:

{
  "message": "Connection timeout in PaymentService",   // → text (for searching)
  "level": "ERROR",                                     // → keyword (for filtering)
  "status_code": 500,                                   // → integer
  "host": "web-server-01"                               // → keyword
}

You’d search message with: “connection timeout” (finds it via full-text search). You’d filter level with: “ERROR” (exact match only).

Create a Mapping

Define a mapping when creating an index:

$ curl -X PUT -u elastic:changeme123 \
  "http://localhost:9200/web-logs" \
  -H "Content-Type: application/json" \
  -d '{
    "mappings": {
      "properties": {
        "timestamp": { "type": "date" },
        "client_ip": { "type": "ip" },
        "method": { "type": "keyword" },
        "path": { "type": "keyword" },
        "status_code": { "type": "integer" },
        "bytes_sent": { "type": "long" },
        "response_time": { "type": "float" },
        "message": { "type": "text" },
        "user_agent": { "type": "text" },
        "host": { "type": "keyword" },
        "geo": {
          "properties": {
            "country": { "type": "keyword" },
            "city": { "type": "keyword" },
            "location": { "type": "geo_point" }
          }
        }
      }
    },
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  }'

Expected output:

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "web-logs"
}

View a Mapping

$ curl -u elastic:changeme123 \
  "http://localhost:9200/web-logs/_mapping?pretty"

Expected output:

{
  "web-logs": {
    "mappings": {
      "properties": {
        "bytes_sent": { "type": "long" },
        "client_ip": { "type": "ip" },
        "host": { "type": "keyword" },
        "message": { "type": "text" },
        "method": { "type": "keyword" },
        "path": { "type": "keyword" },
        "response_time": { "type": "float" },
        "status_code": { "type": "integer" },
        "timestamp": { "type": "date" },
        "user_agent": { "type": "text" }
      }
    }
  }
}

Dynamic Mapping

If you index a document without defining a mapping, Elasticsearch uses dynamic mapping to guess types:

JSON Value Elasticsearch Guesses
"hello" text + keyword (multi-field)
123 long
1.5 float
true boolean
"2025-03-15" date
"192.168.1.1" (when using ip type hints) text (ES doesn’t auto-detect IP)

Common mistake: Elasticsearch auto-maps string numbers like "200" as text, not integer. Always define explicit mappings.

Multi-Field Mappings

Sometimes you need a field to be both text (for search) and keyword (for aggregation):

{
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "fields": {
          "raw": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

Now you can:

  • Search with: message: "connection timeout" (full-text)
  • Aggregate with: message.raw (exact values)

Add a Field to an Existing Mapping

You can add new fields but cannot change existing field types (this is an Elasticsearch limitation):

$ curl -X PUT -u elastic:changeme123 \
  "http://localhost:9200/web-logs/_mapping" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "datacenter": { "type": "keyword" },
      "request_id": { "type": "keyword" }
    }
  }'

If you need to change a field type, you must:

  1. Create a new index with the correct mapping
  2. Reindex data from the old index to the new one
  3. Delete the old index
  4. (Optionally) create an alias pointing to the new index

Reindex to Fix a Mapping

# Step 1: Create new index with correct mapping
$ curl -X PUT -u elastic:changeme123 \
  "http://localhost:9200/web-logs-v2" \
  -H "Content-Type: application/json" \
  -d '{
    "mappings": {
      "properties": {
        "status_code": { "type": "integer" }
      }
    }
  }'

# Step 2: Reindex data
$ curl -X POST -u elastic:changeme123 \
  "http://localhost:9200/_reindex" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "index": "web-logs" },
    "dest": { "index": "web-logs-v2" }
  }'

# Step 3: Delete old index and create alias
$ curl -X DELETE -u elastic:changeme123 "http://localhost:9200/web-logs"
$ curl -X POST -u elastic:changeme123 \
  "http://localhost:9200/_aliases" \
  -H "Content-Type: application/json" \
  -d '{
    "actions": [
      { "add": { "index": "web-logs-v2", "alias": "web-logs" } }
    ]
  }'

Date Field Formats

The date field supports multiple formats:

{
  "timestamp": {
    "type": "date",
    "format": "yyyy-MM-dd'T'HH:mm:ss.SSSZ||yyyy-MM-dd||epoch_millis"
  }
}

Common date formats:

  • "2025-03-15T10:00:00.000Z" — ISO 8601
  • "2025-03-15" — Date only
  • 1710500400000 — Unix timestamp in milliseconds (epoch_millis)

Common Mapping Mistakes

Mistake Impact Fix
Not defining mappings ES guesses wrong types Always create explicit mappings
Using text for status codes Can’t filter or aggregate properly Use integer or keyword
Forgetting keyword for IPs Can’t do IP range queries Use ip type
Changing field types Elasticsearch rejects the change Reindex to a new index
Too many fields “Mapping explosion” — performance issues Set index.mapping.total_fields.limit

Quick Reference

Concept Key Point
Mapping Schema definition for an Elasticsearch index
text Full-text search (analyzed into tokens)
keyword Exact match, filtering, sorting, aggregations
date Date/time values with configurable formats
ip IP addresses with range query support
Dynamic mapping ES guesses types (often wrong for numbers)
Multi-field One field indexed as both text and keyword
Can’t change types Must reindex to fix mapping mistakes
View mapping GET /my-index/_mapping

Next Step

You understand how data is structured. Now let’s learn about shards and replicas — how Elasticsearch distributes and protects your data.