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/nameDynamic 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/_mappingDynamic Mapping Algılama Kuralları
| JSON Tipi | Algılanan Elasticsearch Tipi |
|---|---|
"hello" (string) | text + keyword (multi-field) |
42 (integer) | long |
99.99 (float) | float |
true/false | boolean |
"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 alloweddynamic Değeri | Bilinmeyen Field | Index'lenir mi? | Mapping'e Eklenir mi? |
|---|---|---|---|
true (varsayılan) | Kabul edilir | ✅ Evet | ✅ Evet |
runtime | Kabul edilir | ❌ Hayır | ✅ Runtime field olarak |
false | Kabul edilir | ❌ Hayır | ❌ Hayır (ama _source'ta saklanır) |
strict | Reddedilir | ❌ 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" → falseGeo 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_exceptionNeden? Çü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-v1Bu 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.autocompleteGerç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 engellescaled_float→ Fiyat için bellek dostuhalf_float→ Rating için yeterli hassasiyetkeyword + text multi-field→ Hem arama hem filtrelemeindex: false→ images URL'leri aranmaz, sadece saklanırnull_value→ Rating null gelirse 0.0 olarak index'lecompletion→ 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_abovegibi parametreleri ihtiyaca göre kullan
Bir sonraki derste Shards ve Replicas konusuna geçeceğiz — Elasticsearch'ün dağıtık mimarisinin temellerini öğreneceğiz.
AI Asistan
Sorularını yanıtlamaya hazır