Elasticsearch’te Mapping ve Şema Yönetimi
Yıllarca Elasticsearch ile uğraşan biri olarak şunu rahatlıkla söyleyebilirim: mapping yönetimi, ELK Stack kurulumlarının en çok göz ardı edilen ama en kritik parçasıdır. İnsanlar Kibana’da güzel dashboard’lar kuruyor, Logstash pipeline’larını yazıyor, ama mapping konusuna “Elasticsearch zaten otomatik anlıyor” diyerek geçiştiriyor. Sonra da altı ay sonra index’leri şişiyor, sorgular yavaşlıyor, aggregation’lar çalışmıyor diye şikayet ediyorlar. Bu yazıda mapping’in ne olduğunu, neden önemsendiğini ve gerçek dünya senaryolarında nasıl yönetildiğini elimden geldiğince aktaracağım.
Mapping Nedir ve Neden Önemlidir?
Elasticsearch, temelde bir JSON döküman deposudur. Her dökümanı index’e yazarken o dökümanın alanlarını nasıl saklayacağını, nasıl analiz edeceğini ve nasıl sorgulayabileceğini bilmesi gerekir. İşte bu bilginin tamamına mapping diyoruz. İlişkisel veritabanlarındaki schema kavramına benzetebilirsiniz ama çok daha esnek ve bir o kadar da tehlikeli.
Elasticsearch’in “dynamic mapping” özelliği var. Yani siz hiçbir şey tanımlamadan veri gönderseniz bile, Elasticsearch gelen JSON’ı analiz edip alan tiplerini otomatik belirliyor. İlk başta kulağa harika geliyor. Ama üretim ortamında bu otomatikliği kontrolsüz bırakmak, zamanla aşağıdaki sorunlara yol açıyor:
- Tip çakışmaları: Bir alana ilk önce string, sonra integer veri gelirse Elasticsearch mapping exception fırlatır ve veri kaybedersiniz.
- Gereksiz alan patlaması: Log’larınız tutarsızsa Elasticsearch her yeni alanı ayrı bir mapping entry’si olarak kaydeder. Binlerce unique alan içeren index’ler hem disk hem de bellek açısından felakettir.
- Yanlış analiz: URL’leri metin olarak analiz etmesi gereken bir alan, Elasticsearch tarafından keyword olarak saklanırsa full-text arama çalışmaz. Ya da tam tersi, sayısal bir alan text olarak map edilirse aggregation yapamazsınız.
Dynamic Mapping’in Tuzakları
Şöyle bir senaryo düşünün: Uygulama ekibiniz yeni bir servis deploy etti. Bu servisin logları Logstash üzerinden Elasticsearch’e akmaya başladı. Her şey güzel görünüyor, ama bir hafta sonra fark ediyorsunuz ki response_time alanı bazen "250ms" string formatında, bazen de 250 integer formatında geliyor. İlk gelen hangi formattaysa o mapping’e yazılıyor, sonrakiler ise mapping conflict’e düşüyor ve o dökümanlar index’e yazılamıyor.
Mevcut bir index’in mapping’ini kontrol etmek için:
curl -X GET "localhost:9200/nginx-logs-2024.01/_mapping?pretty"
Hangi alanların nasıl tanımlandığını gördükten sonra, sorunlu alanları tespit edebilirsiniz. Ama dikkat: mevcut bir index’in mapping’ini değiştiremezsiniz. Elasticsearch, var olan bir alanın tipini değiştirmenize izin vermez. Bunun için ya yeni bir index oluşturup reindex yapmanız, ya da o alan için farklı bir strateji belirlemeniz gerekir.
Explicit Mapping ile Şema Tanımlamak
Üretim ortamlarında her zaman explicit mapping kullanın. Veri gelmeden önce hangi alanın ne tipte olacağını tanımlayın. Bu hem performans hem de veri tutarlılığı açısından kritik önem taşır.
Nginx log’ları için basit bir explicit mapping örneği:
curl -X PUT "localhost:9200/nginx-logs" -H 'Content-Type: application/json' -d'
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"client_ip": {
"type": "ip"
},
"request_method": {
"type": "keyword"
},
"request_url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 512
}
}
},
"status_code": {
"type": "short"
},
"response_time_ms": {
"type": "integer"
},
"bytes_sent": {
"type": "long"
},
"user_agent": {
"type": "text",
"index": false
}
}
}
}'
Burada birkaç önemli detaya dikkat çekelim:
iptipi: IP adresleri için özel bir tip. CIDR notasyonu ile range sorguları yapabiliyorsunuz.client_ip:[192.168.1.0/24 TO *]gibi.keywordtipi: Tam eşleşme aramaları ve aggregation’lar için. HTTP method gibi değerleri her zaman keyword yapın.- Multi-field tanımı:
request_urlhem text hem de keyword olarak tanımlandı. URL’de full-text arama yaparken text field’ı, exact match veya aggregation için keyword field’ı kullanırsınız. index: false:user_agentalanı aranmayacak, sadece görüntüleme amaçlı tutulacak. Bu şekilde index boyutunu küçültüyoruz.
Index Template ile Merkezi Mapping Yönetimi
Gerçek dünyada tek bir index yoktur. Log’larınız genellikle günlük, haftalık ya da aylık rotation ile yeni index’lere yazılır. nginx-logs-2024.01.01, nginx-logs-2024.01.02 gibi. Her seferinde elle mapping tanımlamak hem hatalı hem de sürdürülemez.
Burada Index Template devreye giriyor. Bir template tanımlıyorsunuz, belirli bir pattern’e uyan her yeni index bu template’i otomatik olarak kullanıyor.
curl -X PUT "localhost:9200/_index_template/nginx-logs-template"
-H 'Content-Type: application/json' -d'
{
"index_patterns": ["nginx-logs-*"],
"priority": 100,
"template": {
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1,
"refresh_interval": "5s",
"index.mapping.total_fields.limit": 500
},
"mappings": {
"dynamic": "strict",
"properties": {
"@timestamp": { "type": "date" },
"client_ip": { "type": "ip" },
"status_code": { "type": "short" },
"response_time_ms": { "type": "integer" },
"request_method": { "type": "keyword" },
"request_url": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 512 }
}
},
"environment": { "type": "keyword" },
"service_name": { "type": "keyword" }
}
}
}
}'
"dynamic": "strict" ayarı kritik. Bu sayede mapping’de tanımlanmamış bir alan geldiğinde Elasticsearch hata fırlatır ve o dökümanı kabul etmez. Sıkı bir ortamda bu istediğiniz davranış olabilir. Ama bazen daha esnek olmak istersiniz, o zaman "dynamic": false kullanabilirsiniz. Bu durumda bilinmeyen alanlar index’e yazılır ama aranabilir olmaz.
Mevcut template’leri listelemek için:
curl -X GET "localhost:9200/_index_template/nginx-logs-template?pretty"
Dynamic Template: En İyi İki Dünyanın Birleşimi
Strict dynamic mapping bazen çok kısıtlayıcı olabilir. Özellikle log formatı uygulamadan uygulamaya değişiyorsa. Tam burada dynamic template kavramı işe yarıyor. Belirli kurallara uyan alanları otomatik olarak belirli tiplere atayabiliyorsunuz.
Örnek senaryo: Tüm _count ile biten alanların integer, _time ile biten alanların float, is_ ile başlayan alanların boolean olmasını istiyoruz:
curl -X PUT "localhost:9200/_index_template/app-logs-template"
-H 'Content-Type: application/json' -d'
{
"index_patterns": ["app-logs-*"],
"priority": 90,
"template": {
"mappings": {
"dynamic": true,
"dynamic_templates": [
{
"count_fields_as_integer": {
"match_pattern": "regex",
"match": ".*_count$",
"mapping": {
"type": "integer"
}
}
},
{
"time_fields_as_float": {
"match_pattern": "regex",
"match": ".*_time$",
"mapping": {
"type": "float"
}
}
},
{
"boolean_fields": {
"match": "is_*",
"mapping": {
"type": "boolean"
}
}
},
{
"string_as_keyword": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
}'
Son template olan string_as_keyword özellikle önemli. Elasticsearch varsayılan olarak string alanları hem text hem keyword olarak (multi-field) saklar. Ama log senaryolarında çoğu zaman full-text aramaya ihtiyacınız yoktur. Tüm string’leri keyword yaparak hem disk alanından hem de index time’ından tasarruf edebilirsiniz.
Reindex: Mevcut Mapping Sorunlarını Çözmek
Daha önce bahsettiğimiz üzere, mevcut bir index’in field tipini değiştiremezsiniz. Ama reindex API’si ile eski index’teki verileri yeni bir mapping ile oluşturulmuş index’e taşıyabilirsiniz.
Diyelim ki nginx-logs-old index’indeki response_time alanı yanlışlıkla keyword olarak map edildi, onu integer yapmanız gerekiyor:
# Önce doğru mapping ile yeni index oluştur
curl -X PUT "localhost:9200/nginx-logs-new" -H 'Content-Type: application/json' -d'
{
"mappings": {
"properties": {
"response_time": { "type": "integer" },
"@timestamp": { "type": "date" }
}
}
}'
# Reindex işlemini başlat
curl -X POST "localhost:9200/_reindex?wait_for_completion=false"
-H 'Content-Type: application/json' -d'
{
"source": {
"index": "nginx-logs-old"
},
"dest": {
"index": "nginx-logs-new"
},
"script": {
"source": "if (ctx._source.response_time != null) { ctx._source.response_time = Integer.parseInt(ctx._source.response_time.toString().replaceAll("ms", "").trim()) }",
"lang": "painless"
}
}'
wait_for_completion=false parametresi ile işlemi arka planda başlatıyoruz. Büyük index’ler için bu şart. Task ID döner, onunla progress’i takip edersiniz:
curl -X GET "localhost:9200/_tasks/TASK_ID_HERE?pretty"
Reindex tamamlandıktan sonra alias’ı güncelleyerek uygulamalarınızın yeni index’i kullanmasını sağlayabilirsiniz, hiç downtime olmadan.
Alias ile Şeffaf Geçişler
Alias, Elasticsearch’te bir veya birden fazla index’e verilen takma isimdir. Uygulamalarınız alias üzerinden sorgulama yaparsa, arka planda hangi index’i kullandıklarını bilmek zorunda kalmazlar. Bu da reindex gibi operasyonları çok daha az riskli hale getirir.
# Eski index'e read alias, yeni index'e hem read hem write alias ekle
curl -X POST "localhost:9200/_aliases" -H 'Content-Type: application/json' -d'
{
"actions": [
{
"remove": {
"index": "nginx-logs-old",
"alias": "nginx-logs"
}
},
{
"add": {
"index": "nginx-logs-new",
"alias": "nginx-logs"
}
}
]
}'
Bu işlem atomik olarak gerçekleşir. Bir an bile tutarsızlık olmaz.
Alan Limitlerini ve Mapping Explosion’ı Yönetmek
Gerçek hayatta karşılaştığım en ciddi sorunlardan biri mapping explosion. Özellikle JSON formatında log atan uygulamalarda, her farklı hata tipi için farklı alanlar oluşturulabiliyor. Ya da key-value pair içeren dynamic JSON’lar geliyor ve Elasticsearch her key’i ayrı bir field olarak kaydediyor. Yüzlerce, binlerce field olan index’ler cluster’ı adeta felç ediyor.
Bunu önlemenin birkaç yolu var:
İlk olarak index.mapping.total_fields.limit ayarını template’inizde makul bir değere çekin (varsayılan 1000, üretimde 200-500 arası genellikle yeterli).
İkinci olarak, dynamic JSON verilerini düz field’lara açmak yerine flattened tipini kullanabilirsiniz:
curl -X PUT "localhost:9200/app-events" -H 'Content-Type: application/json' -d'
{
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"event_type": { "type": "keyword" },
"metadata": {
"type": "flattened"
}
}
}
}'
flattened tipi ile metadata altındaki tüm nested JSON, tek bir field olarak saklanır. İçindeki değerlere keyword araması yapabilirsiniz ama her bir alt alan ayrı bir mapping entry’si oluşturmaz. Mapping explosion sorununu çözmenin en pratik yollarından biri bu.
Gerçek Dünya Senaryosu: Çoklu Servis Log’larını Yönetmek
Bir mikro servis mimarisinde çalışıyorsanız, her servisin farklı log formatı olabilir. Ama bazı alanlar ortaktır: @timestamp, service_name, environment, log_level, trace_id gibi. Bu durumda component template kullanımı hayat kurtarır.
# Ortak alanlar için component template
curl -X PUT "localhost:9200/_component_template/common-fields"
-H 'Content-Type: application/json' -d'
{
"template": {
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"service_name": { "type": "keyword" },
"environment": { "type": "keyword" },
"log_level": { "type": "keyword" },
"trace_id": { "type": "keyword" },
"host": {
"properties": {
"name": { "type": "keyword" },
"ip": { "type": "ip" }
}
}
}
}
}
}'
# Servis özelindeki index template, component template'i kullanıyor
curl -X PUT "localhost:9200/_index_template/payment-service-template"
-H 'Content-Type: application/json' -d'
{
"index_patterns": ["payment-service-*"],
"priority": 200,
"composed_of": ["common-fields"],
"template": {
"mappings": {
"properties": {
"transaction_id": { "type": "keyword" },
"amount": { "type": "scaled_float", "scaling_factor": 100 },
"currency": { "type": "keyword" },
"payment_status": { "type": "keyword" },
"error_code": { "type": "keyword" }
}
}
}
}'
scaled_float tipi para miktarları için idealdir. scaling_factor: 100 ile 2 ondalık basamak hassasiyeti elde ediyorsunuz ve değerler integer olarak saklandığından float’a göre daha az disk alanı kullanıyor.
Mapping Değişikliklerine Versiyonlama Yaklaşımı
Mapping yönetimi sadece teknik değil, aynı zamanda bir süreç meselesi. Özellikle büyük ekiplerde kimse “bu template’i ne zaman kim değiştirdi” bilmiyor. Bunun için birkaç pratik öneri:
- Template tanımlarınızı Git’te tutun. Elasticsearch konfigürasyonunu kod olarak yönetin (GitOps yaklaşımı).
- Template’lerinize
_metaalanı ekleyerek versiyon ve açıklama bilgisi gömün. - CI/CD pipeline’larınıza template deployment adımı ekleyin. Elle yapılan değişikliklere izin vermeyin.
curl -X PUT "localhost:9200/_index_template/nginx-logs-template"
-H 'Content-Type: application/json' -d'
{
"index_patterns": ["nginx-logs-*"],
"priority": 100,
"_meta": {
"version": "2.3.1",
"description": "Nginx access log template",
"owner": "platform-team",
"last_modified": "2024-01-15",
"changelog": "Added response_time_ms field, removed deprecated upstream_time"
},
"template": {
"mappings": {
"properties": {
"@timestamp": { "type": "date" }
}
}
}
}'
Mapping Sorunlarını Tespit Etmek
Mapping sorunlarını proaktif olarak tespit etmek için birkaç Elasticsearch API’sini düzenli olarak kullanmanızı öneririm. Field istatistiklerini görmek, hangi alanların gerçekten kullanıldığını anlamak için:
# Index'teki field kapasitesi ve kullanımı
curl -X GET "localhost:9200/nginx-logs-*/_field_caps?fields=*&pretty" |
python3 -c "import sys,json; data=json.load(sys.stdin); print(len(data['fields']), 'total fields')"
# Mapping'deki toplam field sayısını kontrol et
curl -X GET "localhost:9200/nginx-logs-2024.01/_mapping?pretty" |
grep '"type"' | wc -l
Cluster genelinde mapping durumunu takip etmek için bu tür kontrolleri cron job veya monitoring sistemlerinize ekleyin. Field sayısı belirlediğiniz eşiği geçtiğinde alert alın.
Sonuç
Elasticsearch mapping yönetimi, “bir kere kur unut” değil, sürekli bakım gerektiren bir disiplindir. Bu yazıda anlattıklarımı özetlersek:
- Dynamic mapping’e körce güvenmeyin. Üretim ortamlarında mutlaka explicit mapping veya dynamic template kullanın.
- Index template’leri ve component template’leri benimseyin. Mapping tanımlarınızı merkezi ve tekrar kullanılabilir hale getirin.
dynamic: strictile başlayın, sonra ihtiyaca göre esnekliği artırın.- Flattened tipi, mapping explosion sorununa pratik bir çözüm sunar.
- Reindex + alias kombinasyonu, çalışan sistemde mapping değişikliğini mümkün kılar.
- Template’lerinizi versiyonlayın ve kod olarak yönetin.
- Field limitlerini izleyin ve alerting kurun.
Mapping sorunlarının büyük çoğunluğu veri üretime geçmeden önce iyi bir şema tasarımıyla önlenebilir. Ne kadar erken düşünürseniz, sonradan o kadar az “bu alan neden aggregation’da çıkmıyor” sorusunu yanıtlamak zorunda kalırsınız.
