← Kursa Dön
📄 Text · 30 min

Mapping — Şema Tanımlama ve Field Tipleri

Şema Tanımlama, Field Tipleri, Dynamic Mapping

Bir evin planı olmadan inşaata başlarsan ne olur? Belki ilk birkaç duvarı dikersin ama sonra kapı yeri yanlış kalır, elektrik tesisatı çekilemez, su boruları çatışır. Mapping, Elasticsearch'teki evin planıdır. Verinin nasıl saklanacağını, nasıl aranacağını, nasıl analiz edileceğini belirler.

Mapping yanlışsa, ileride düzeltmek çok zor — genellikle reindex gerektirir. Bu yüzden mapping'i ilk günden doğru yapmak kritik.


Mapping Nedir?

Mapping, bir index'teki document'ların yapısını tanımlayan şemadır. Her field'ın:

  • Tipi: text mi, keyword mı, integer mı, date mi?

  • Nasıl analiz edileceği: Hangi analyzer kullanılacak?

  • Nasıl saklanacağı: Index'lenecek mi, sadece _source'ta mı duracak?

// Mapping tanımlı bir index oluştur
PUT /products
{
  "mappings": {
    "properties": {
      "name":         { "type": "text" },
      "sku":          { "type": "keyword" },
      "price":        { "type": "float" },
      "description":  { "type": "text", "analyzer": "turkish" },
      "category":     { "type": "keyword" },
      "in_stock":     { "type": "boolean" },
      "rating":       { "type": "half_float" },
      "created_at":   { "type": "date" },
      "tags":         { "type": "keyword" },
      "location":     { "type": "geo_point" }
    }
  }
}

Mevcut Mapping'i Görme

// Index mapping'ini kontrol et
GET /products/_mapping

// Yanıt:
{
  "products": {
    "mappings": {
      "properties": {
        "name": { "type": "text" },
        "sku": { "type": "keyword" },
        "price": { "type": "float" },
        // ...
      }
    }
  }
}

// Belirli bir field'ın mapping'ini gör
GET /products/_mapping/field/name

Dynamic Mapping — Otomatik Şema Algılama

Mapping tanımlamadan veri gönderirsen Elasticsearch field tiplerini otomatik algılar:

// Mapping tanımlamadan doğrudan veri gönder
POST /auto-test/_doc
{
  "title": "Test Başlığı",
  "count": 42,
  "price": 99.99,
  "active": true,
  "created": "2025-01-15T10:00:00Z",
  "ip_address": "192.168.1.1"
}

// Elasticsearch'ün algıladığı mapping:
GET /auto-test/_mapping

Dynamic Mapping Algılama Kuralları

JSON TipiAlgılanan Elasticsearch Tipi
"hello" (string)text + keyword (multi-field)
42 (integer)long
99.99 (float)float
true/falseboolean
"2025-01-15" (tarih string)date
"2025-01-15T10:00:00Z"date
{ "a": 1 } (object)object
[1, 2, 3] (array)İlk elemanın tipine göre

Dynamic Mapping'in Problemleri

Problem 1: String'ler hem text hem keyword olur

// Sen bunu gönderdin:
POST /test/_doc
{
  "status": "active"
}

// Elasticsearch bunu oluşturdu:
// "status": {
//   "type": "text",
//   "fields": {
//     "keyword": { "type": "keyword", "ignore_above": 256 }
//   }
// }

"status" alanı sadece exact match için kullanılacak — keyword yeter. Ama dynamic mapping hem text hem keyword oluşturdu. Sonuç: ikili indexleme, gereksiz disk ve bellek kullanımı.

Problem 2: Sayısal string'ler text olarak algılanır

POST /test/_doc
{
  "zip_code": "34000"    // String olarak gönderdin
}
// Tip: text + keyword
// Ama "zip_code" üzerinde range sorgusu yapamazsın!

Problem 3: Bir kez mapping oluştuktan sonra değiştirilemez

// İlk document "price" alanını string gönderdi:
POST /test/_doc/1
{
  "price": "100 TL"     // text olarak algılandı
}

// Şimdi düzeltmeye çalışsan:
PUT /test/_mapping
{
  "properties": {
    "price": { "type": "float" }
  }
}
// ❌ HATA! Mevcut field'ın tipini değiştiremezsin!

Dynamic Mapping Kontrolleri

Dynamic mapping'in davranışını kontrol edebilirsin:

// Dynamic mapping tamamen kapalı — bilinmeyen field'lar reddedilir
PUT /strict-index
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "name": { "type": "text" },
      "price": { "type": "float" }
    }
  }
}

// Tanımlanmamış field göndermek hata verir
POST /strict-index/_doc
{
  "name": "Test",
  "price": 100,
  "color": "red"     // ❌ strict mode — "color" mapping'de yok!
}
// 400 Bad Request: mapping set to strict, dynamic introduction of [color] is not allowed
dynamic DeğeriBilinmeyen FieldIndex'lenir mi?Mapping'e Eklenir mi?
true (varsayılan)Kabul edilir✅ Evet✅ Evet
runtimeKabul edilir❌ Hayır✅ Runtime field olarak
falseKabul edilir❌ Hayır❌ Hayır (ama _source'ta saklanır)
strictReddedilir❌ Hayır❌ Hayır

Tavsiye: Production'da dynamic: strict veya dynamic: false kullan. Kontrolsüz field eklenmesini engelle.


Explicit Mapping — Bilinçli Şema Tanımlama

Production'da her zaman explicit mapping kullan:

Tüm Field Tipleri — Detaylı Referans

String Tipleri

PUT /my-index
{
  "mappings": {
    "properties": {
      // TEXT — Full-text search için
      // Analiz edilir (tokenize + normalize)
      "title": {
        "type": "text",
        "analyzer": "standard"     // Varsayılan analyzer
      },

      // KEYWORD — Exact match, aggregation, sorting için
      // Analiz edilmez, aynen saklanır
      "status": {
        "type": "keyword"
      },

      // TEXT + KEYWORD (Multi-field) — Her ikisi için
      "category": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },

      // WILDCARD — Wildcard sorguları için optimize edilmiş
      "log_message": {
        "type": "wildcard"
      },

      // CONSTANT_KEYWORD — Tüm dokümanlar aynı değere sahipse
      "environment": {
        "type": "constant_keyword",
        "value": "production"
      }
    }
  }
}

Sayısal Tipleri

{
  "mappings": {
    "properties": {
      "quantity":    { "type": "integer" },     // -2^31 ile 2^31-1
      "total_sold":  { "type": "long" },         // -2^63 ile 2^63-1
      "small_num":   { "type": "short" },        // -32768 ile 32767
      "tiny_num":    { "type": "byte" },         // -128 ile 127
      "price":       { "type": "float" },        // 32-bit IEEE 754
      "precise_val": { "type": "double" },       // 64-bit IEEE 754
      "rating":      { "type": "half_float" },   // 16-bit IEEE 754
      "percentage":  { "type": "scaled_float",   // Ölçeklenmiş float
                       "scaling_factor": 100 }    // 99.99 → 9999 olarak saklanır
    }
  }
}

💡 İpucu: scaled_float fiyat için ideal. "scaling_factor": 100 ile 99.99 TL → 9999 kuruş olarak saklanır. Disk ve bellek açısından daha verimli.

Tarih Tipleri

{
  "mappings": {
    "properties": {
      // Varsayılan date — çoğu format otomatik algılanır
      "created_at": {
        "type": "date"
      },
      
      // Özel format belirle
      "birth_date": {
        "type": "date",
        "format": "yyyy-MM-dd"
      },
      
      // Birden fazla format kabul et
      "event_date": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      },
      
      // Nanosaniye hassasiyeti
      "precise_time": {
        "type": "date_nanos"
      }
    }
  }
}

Elasticsearch'ün varsayılan olarak algıladığı tarih formatları:

"2025-01-15"                    → ✅ date
"2025-01-15T10:00:00Z"          → ✅ date
"2025-01-15T10:00:00.000+03:00" → ✅ date
"2025/01/15"                    → ❌ algılanmaz (text olur)
"15-01-2025"                    → ❌ algılanmaz
"Jan 15, 2025"                  → ❌ algılanmaz
1705312800000                   → ❌ algılanmaz (epoch_millis açıkça belirtilmeli)

Boolean

{
  "mappings": {
    "properties": {
      "active": { "type": "boolean" }
    }
  }
}

// Kabul edilen değerler:
// true, "true", "yes", "on", "1" → true
// false, "false", "no", "off", "0" → false

Geo Tipleri

{
  "mappings": {
    "properties": {
      // Tek nokta (enlem/boylam)
      "location": { "type": "geo_point" },
      
      // Şekil (poligon, daire, çizgi)
      "coverage_area": { "type": "geo_shape" }
    }
  }
}

// geo_point değer formatları:
// Object: { "lat": 41.01, "lon": 28.97 }
// String: "41.01,28.97"
// Array: [28.97, 41.01]  ← DİKKAT: GeoJSON sırası [lon, lat]
// Geohash: "sxk9g4"

Özel Tipleri

{
  "mappings": {
    "properties": {
      // IP adresi
      "client_ip": { "type": "ip" },
      
      // Otomatik tamamlama
      "suggest": { "type": "completion" },
      
      // Sayı aralığı
      "age_range": { "type": "integer_range" },
      "event_period": { "type": "date_range" },
      
      // Token sayısı
      "tag_count": {
        "type": "token_count",
        "analyzer": "standard"
      },
      
      // Binary (base64)
      "thumbnail": {
        "type": "binary"     // Index'lenmez, sadece _source'ta saklanır
      }
    }
  }
}

Field Mapping Parametreleri

Her field tipinin ek parametreleri var:

index — Field'ı index'le veya index'leme

{
  "properties": {
    // Aranabilir (varsayılan)
    "name": { "type": "text", "index": true },
    
    // Aranamaz — sadece _source'ta saklanır
    "internal_note": { "type": "text", "index": false }
  }
}

index: false olan alanlar aranmaz, filtrelenmez, sıralanamaz. Ama _source'ta görünür. Disk ve bellek tasarrufu sağlar.

doc_values — Sorting ve Aggregation İçin

{
  "properties": {
    // doc_values aktif (keyword, numeric, date için varsayılan true)
    "category": { "type": "keyword" },    // sorting + aggregation ✅
    
    // doc_values kapalı — sorting ve aggregation yapılamaz
    "log_id": { "type": "keyword", "doc_values": false }
  }
}

store — _source Dışında Ayrı Saklama

{
  "properties": {
    "title": { 
      "type": "text",
      "store": true    // _source'tan bağımsız olarak ayrıca sakla
    }
  }
}

// store: true olan alanı ayrıca çekebilirsin
GET /my-index/_search
{
  "stored_fields": ["title"],
  "query": { "match_all": {} }
}

Genellikle gerek yok — _source yeterli. Ama _source çok büyükse ve sadece belirli alanları hızlıca almak istiyorsan kullanılır.

null_value — null Değer Yerine Koyma

{
  "properties": {
    "status": {
      "type": "keyword",
      "null_value": "UNKNOWN"    // null → "UNKNOWN" olarak index'lenir
    }
  }
}

// Normalde null değerler index'lenmez ve aranamaz
// null_value ile aranabilir hale gelir
GET /my-index/_search
{
  "query": {
    "term": { "status": "UNKNOWN" }
  }
}

copy_to — Alanları Birleştir

PUT /products
{
  "mappings": {
    "properties": {
      "first_name": { "type": "text", "copy_to": "full_name" },
      "last_name":  { "type": "text", "copy_to": "full_name" },
      "full_name":  { "type": "text" }    // Gizli alan — _source'ta görünmez
    }
  }
}

// first_name ve last_name değerleri otomatik olarak full_name'e kopyalanır
// Tek sorguda ikisinde birden arayabilirsin
GET /products/_search
{
  "query": {
    "match": { "full_name": "Ahmet Yılmaz" }
  }
}

ignore_above — Uzun Keyword Değerleri

{
  "properties": {
    "tag": {
      "type": "keyword",
      "ignore_above": 256    // 256 karakterden uzun değerler index'lenmez
    }
  }
}

coerce — Tip Zorlama

{
  "properties": {
    "price": {
      "type": "float",
      "coerce": true     // "100" string → 100.0 float'a çevrilir (varsayılan true)
    },
    "strict_price": {
      "type": "float",
      "coerce": false    // "100" string gönderirsen hata verir
    }
  }
}

Mapping Güncelleme

Yeni Field Ekleme — ✅ Mümkün

// Mevcut mapping'e yeni field ekle
PUT /products/_mapping
{
  "properties": {
    "color": { "type": "keyword" },
    "weight": { "type": "float" }
  }
}

Mevcut Field'ı Değiştirme — ❌ Mümkün Değil

// Bu ÇALIŞMAZ:
PUT /products/_mapping
{
  "properties": {
    "price": { "type": "integer" }    // float'tan integer'a değiştiremezsin!
  }
}
// ❌ mapper_parsing_exception

Neden? Çünkü mevcut veriler eski tip ile index'lenmiş. Tip değiştirmek, tüm verinin yeniden index'lenmesini gerektirir.

Çözüm: Reindex

// 1. Yeni mapping ile yeni index oluştur
PUT /products-v2
{
  "mappings": {
    "properties": {
      "name":  { "type": "text" },
      "price": { "type": "integer" }    // Artık integer
    }
  }
}

// 2. Eski index'ten yeni index'e veriyi kopyala
POST /_reindex
{
  "source": { "index": "products-v1" },
  "dest":   { "index": "products-v2" }
}

// 3. Alias'ı yeni index'e çevir
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products-v1", "alias": "products" } },
    { "add":    { "index": "products-v2", "alias": "products" } }
  ]
}

// 4. Eski index'i sil (opsiyonel)
DELETE /products-v1

Bu yüzden alias kullanmak çok önemli. Uygulama products alias'ını kullanıyorsa, arkadaki index v1'den v2'ye geçse bile uygulama etkilenmez.


Multi-Field Mapping

Aynı veriyi birden fazla şekilde index'lemek:

PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",            // Full-text search için
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword"      // Aggregation ve sorting için
          },
          "autocomplete": {
            "type": "text",
            "analyzer": "autocomplete_analyzer"  // Autocomplete için
          }
        }
      }
    }
  }
}

// Kullanım:
// Arama: name
// Sıralama: name.keyword
// Aggregation: name.keyword
// Autocomplete: name.autocomplete

Gerçek Dünya Mapping Örneği: E-Ticaret

PUT /products
{
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "turkish_custom": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "turkish_stemmer"]
        }
      },
      "filter": {
        "turkish_stemmer": {
          "type": "stemmer",
          "language": "turkish"
        }
      }
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "product_id":    { "type": "keyword" },
      "name": {
        "type": "text",
        "analyzer": "turkish_custom",
        "fields": {
          "keyword": { "type": "keyword" },
          "suggest": { "type": "completion" }
        }
      },
      "description": {
        "type": "text",
        "analyzer": "turkish_custom"
      },
      "sku":           { "type": "keyword" },
      "category": {
        "type": "keyword",
        "fields": {
          "text": { "type": "text" }
        }
      },
      "brand":         { "type": "keyword" },
      "price": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      "original_price": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      "currency":      { "type": "keyword" },
      "in_stock":      { "type": "boolean" },
      "stock_count":   { "type": "integer" },
      "rating": {
        "type": "half_float",
        "null_value": 0.0
      },
      "review_count":  { "type": "integer" },
      "tags":          { "type": "keyword" },
      "images":        { "type": "keyword", "index": false },
      "attributes": {
        "type": "object",
        "properties": {
          "color":    { "type": "keyword" },
          "size":     { "type": "keyword" },
          "material": { "type": "keyword" },
          "weight":   { "type": "float" }
        }
      },
      "seller": {
        "type": "object",
        "properties": {
          "id":     { "type": "keyword" },
          "name":   { "type": "keyword" },
          "rating": { "type": "half_float" }
        }
      },
      "location":      { "type": "geo_point" },
      "created_at":    { "type": "date" },
      "updated_at":    { "type": "date" }
    }
  }
}

Bu mapping'deki tasarım kararları:

  • dynamic: strict → Tanımlanmamış field gelmesini engelle

  • scaled_float → Fiyat için bellek dostu

  • half_float → Rating için yeterli hassasiyet

  • keyword + text multi-field → Hem arama hem filtreleme

  • index: false → images URL'leri aranmaz, sadece saklanır

  • null_value → Rating null gelirse 0.0 olarak index'le

  • completion → Autocomplete/suggest için


Java ile Mapping Oluşturma

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;

class Main {
    public static void main(String[] args) throws Exception {
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200)
        ).build();
        ElasticsearchClient client = new ElasticsearchClient(
            new RestClientTransport(restClient, new JacksonJsonpMapper())
        );

        // Index + Mapping oluştur
        CreateIndexResponse response = client.indices().create(c -> c
            .index("products")
            .settings(s -> s
                .numberOfShards("1")
                .numberOfReplicas("0")
            )
            .mappings(m -> m
                .properties("name", p -> p.text(t -> t.analyzer("standard")))
                .properties("price", p -> p.float_(f -> f))
                .properties("category", p -> p.keyword(k -> k))
                .properties("in_stock", p -> p.boolean_(b -> b))
                .properties("created_at", p -> p.date(d -> d))
            )
        );

        System.out.println("Index oluşturuldu: " + response.acknowledged());

        // Mapping'i kontrol et
        GetMappingResponse mappingResponse = client.indices().getMapping(g -> g
            .index("products")
        );
        System.out.println("Mapping: " + mappingResponse.result());

        restClient.close();
    }
}

Best Practices

Explicit mapping tanımla — Dynamic mapping'e güvenme, her field'ın tipini belirle

`dynamic: strict` kullan — Beklenmedik field'ların girmesini engelle

`keyword` ve `text` ayrımını doğru yap — Filtreleme = keyword, arama = text

`scaled_float` kullan — Fiyat gibi alanlar için bellek dostu

`index: false` — Aranmayacak alanları (URL, image path) index'leme

Alias + reindex stratejisi — Mapping değişikliği gerektiğinde zero-downtime geçiş

Multi-field kullan — Aynı veri hem arama hem aggregation için gerekiyorsa


Yaygın Hatalar

❌ "Dynamic mapping yeterli"

Dynamic mapping production'da kabus. String alanlar ikili index'lenir, sayısal string'ler text olur, tarih formatı yanlış algılanır. Her zaman explicit mapping yaz.

❌ "Field tipini sonra değiştiririm"

Mevcut bir field'ın tipini değiştiremezsin. Reindex gerekir. İlk günden doğru tipi belirle.

❌ "Her şeyi text yapıyorum"

status, category, country_code gibi alanlar keyword olmalı. Text yaparsan aggregation ve exact match çalışmaz (veya beklenmedik sonuçlar verir).

❌ "null_value kullanmıyorum"

Null değerler varsayılan olarak index'lenmez. exists sorgusu ile bulamaz, aggregation'da sayılmaz. Gerekiyorsa null_value ayarla.

❌ "Mapping'de çok fazla field var"

Elasticsearch varsayılan olarak index başına 1000 field sınırı koyar. Dinamik olarak sürekli yeni field eklenmesi "mapping explosion" sorununa yol açar.

// Limit'i kontrol et ve gerekirse artır (ama dikkatli ol)
PUT /my-index/_settings
{
  "index.mapping.total_fields.limit": 2000
}

Özet

  • Mapping, Elasticsearch'teki şema tanımıdır — her field'ın tipi, analiz şekli ve saklama biçimi

  • Dynamic mapping otomatik tip algılar ama production'da sorun çıkarır — explicit mapping kullan

  • text analiz edilir (arama), keyword aynen saklanır (filtreleme/aggregation/sorting)

  • Mevcut field tipi değiştirilemez — reindex gerekir, bu yüzden ilk günden doğru belirle

  • Multi-field ile aynı veri hem text hem keyword olarak index'lenebilir

  • `dynamic: strict` ile beklenmedik field'ların girmesini engelle

  • null_value, copy_to, index: false, ignore_above gibi parametreleri ihtiyaca göre kullan

Bir sonraki derste Shards ve Replicas konusuna geçeceğiz — Elasticsearch'ün dağıtık mimarisinin temellerini öğreneceğiz.