Index, Document ve Field — Veri Modeli
Elasticsearch Veri Modeli
Bir Excel dosyası düşün. Dosyanın kendisi bir tablo (index), her satır bir kayıt (document), her sütun bir alan (field). Ama Elasticsearch'ün Excel'den farkı büyük: satırların birbirinden farklı sütunları olabilir, hiyerarşik veri tutabilirsin ve en önemlisi — milyarlarca satırda milisaniyede arama yapabilirsin.
Bu derste Elasticsearch'ün veri modelini temelinden öğreneceğiz. RDBMS alışkanlıklarını bir kenara bırak — burada her şey JSON, her şey doküman odaklı.
RDBMS vs Elasticsearch Terminolojisi
İlk önce zihinsel dönüşümü yapalım:
| RDBMS | Elasticsearch | Açıklama |
|---|---|---|
| Database | Cluster | En üst seviye gruplandırma |
| Table | Index | Benzer dokümanların toplandığı yer |
| Row | Document | Tek bir veri kaydı |
| Column | Field | Doküman içindeki bir alan |
| Schema | Mapping | Veri yapısı tanımı |
| SQL | Query DSL | Sorgu dili |
| Primary Key | _id | Benzersiz tanımlayıcı |
⚠️ Dikkat: Bu eşleme %100 doğru değil — sadece zihinsel geçiş için. Elasticsearch'ün veri modeli temelden farklıdır. İlişkisel değildir (no joins), denormalize veriyi sever.
Index — Verilerin Evi
Index, Elasticsearch'te benzer dokümanların toplandığı mantıksal birimdir. RDBMS'teki tabloya benzeterek başlayabilirsin ama farkları bilmek önemli.
Index Oluşturma
// Basit index oluştur
PUT /products
// Ayarlarla birlikte index oluştur
PUT /products
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
// Mapping ile birlikte oluştur (önerilen)
PUT /products
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"name": { "type": "text" },
"price": { "type": "float" },
"category": { "type": "keyword" },
"description": { "type": "text" },
"in_stock": { "type": "boolean" },
"created_at": { "type": "date" }
}
}
}Index Adlandırma Kuralları
✅ Geçerli index adları:
products
app-logs-2025-01
user_profiles
my.index.v2
❌ Geçersiz index adları:
Products → Büyük harf yasak
my index → Boşluk yasak
_internal → Alt çizgi ile başlayamaz (sistem index'leri için ayrılmış)
.hidden → Nokta ile başlayanlar gizli index (özel kullanım)
my/index → Slash yasak
my\index → Backslash yasak
my*index → Wildcard karakterler yasak
çok-uzun-bir-index-adı... → Max 255 byteİsimlendirme best practice:
// Sabit veri setleri
products
users
orders
// Zaman bazlı veri (log, metrik)
app-logs-2025.01.15
metrics-2025.01
nginx-access-2025-w03
// Versiyonlama
products-v1
products-v2Index Bilgisine Erişim
// Index detaylarını gör (settings + mappings + aliases)
GET /products
// Sadece settings
GET /products/_settings
// Sadece mapping
GET /products/_mapping
// Index istatistikleri
GET /products/_stats
// Döküman sayısı
GET /products/_count
// Tüm index'leri listele
GET /_cat/indices?v
// Belirli pattern'e uyan index'ler
GET /_cat/indices/app-logs-*?vIndex Yönetimi
// Index'i kapat (arama yapılamaz, kaynak tasarrufu)
POST /products/_close
// Index'i tekrar aç
POST /products/_open
// Index'i sil (DİKKAT: Geri alınamaz!)
DELETE /products
// Birden fazla index sil
DELETE /old-logs-*
// Index var mı kontrol et
HEAD /products
// 200 → Var, 404 → YokIndex Aliases — Esneklik Katmanı
Alias, bir index'e verilen takma addır. Uygulama alias üzerinden çalışır, arkadaki index değişse bile uygulamayı güncellemeye gerek kalmaz.
// Alias oluştur
POST /_aliases
{
"actions": [
{ "add": { "index": "products-v1", "alias": "products" } }
]
}
// Artık "products" dediğinde aslında "products-v1"e gidersin
GET /products/_search
{
"query": { "match_all": {} }
}
// Yeni versiyon index'e geçiş (zero-downtime)
POST /_aliases
{
"actions": [
{ "remove": { "index": "products-v1", "alias": "products" } },
{ "add": { "index": "products-v2", "alias": "products" } }
]
}
// Atomik operasyon — iki işlem aynı anda yapılırBu pattern production'da çok yaygındır. Mapping değişikliği gerektiğinde yeni index oluştur, veriyi reindex'le, alias'ı yeni index'e çevir.
Document — Veri Birimi
Document, Elasticsearch'teki en küçük veri birimidir. Her document bir JSON objesidir.
Document Yapısı
Elasticsearch'e bir document eklediğinde, dönen yanıtta metadata alanları görürsün:
// Document ekle
PUT /products/_doc/1
{
"name": "MacBook Pro 16",
"price": 75000,
"category": "Laptop",
"in_stock": true,
"tags": ["apple", "laptop", "premium"],
"specs": {
"cpu": "M3 Pro",
"ram": 18,
"storage": 512
}
}
// Yanıt:
{
"_index": "products", // Hangi index'te
"_id": "1", // Benzersiz ID
"_version": 1, // Versiyon (her update'te artar)
"result": "created", // İşlem: created veya updated
"_seq_no": 0, // Sıra numarası
"_primary_term": 1 // Primary term
}Document Okuma
// Tam document (metadata + _source)
GET /products/_doc/1
// Yanıt:
{
"_index": "products",
"_id": "1",
"_version": 1,
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": { // Orijinal JSON — senin gönderdiğin veri
"name": "MacBook Pro 16",
"price": 75000,
"category": "Laptop",
"in_stock": true,
"tags": ["apple", "laptop", "premium"],
"specs": {
"cpu": "M3 Pro",
"ram": 18,
"storage": 512
}
}
}
// Sadece _source (metadata olmadan)
GET /products/_source/1
// Belirli alanları getir
GET /products/_doc/1?_source_includes=name,price
// Bazı alanları hariç tut
GET /products/_doc/1?_source_excludes=specs,tagsDocument Metadata Alanları
Her document'ın otomatik oluşturulan metadata alanları vardır:
| Alan | Açıklama | Değiştirilebilir mi? |
|---|---|---|
_index | Document'ın bulunduğu index | Hayır (taşıma ile değişir) |
_id | Benzersiz tanımlayıcı | Evet (sen belirlersin veya otomatik) |
_source | Orijinal JSON verisi | Devre dışı bırakılabilir (önerilmez) |
_version | Versiyon numarası | Otomatik artar |
_seq_no | Sıra numarası (concurrency control) | Otomatik |
_primary_term | Primary shard term'i | Otomatik |
_routing | Shard routing değeri | Özelleştirilebilir |
_score | Arama relevance skoru | Sadece arama sonuçlarında |
_id — Benzersiz Tanımlayıcı
Her document'ın index içinde benzersiz bir _idsi olmalıdır.
// Yöntem 1: Sen belirle (PUT ile)
PUT /products/_doc/1
{
"name": "MacBook Pro"
}
// Yöntem 2: Elasticsearch otomatik oluşturur (POST ile)
POST /products/_doc
{
"name": "MacBook Air"
}
// Yanıt: "_id": "xYz123AbC456" (random 20 karakter)
// Yöntem 3: Kendi format'ınla (PUT ile)
PUT /products/_doc/PROD-2025-001
{
"name": "MacBook Air"
}Ne zaman hangisi?
| Durum | ID Stratejisi |
|---|---|
| Veritabanından sync ediyorsun | DB'deki primary key'i kullan |
| Log verisi | Otomatik ID (POST) |
| Upsert (varsa güncelle, yoksa ekle) | Kendi ID'n |
| Yüksek yazma performansı | Otomatik ID (POST) — daha hızlı |
💡 İpucu: Otomatik ID, Elasticsearch'ün PUT ile kontrol etmesi gereken "bu ID var mı?" sorgusunu atlar — bu yüzden POST ile otomatik ID, toplu yazmalarda daha hızlıdır.
Field — Dokümanın İçindeki Alanlar
Field, document'ın içindeki her bir veri parçasıdır. JSON'un key'leri.
Temel Field Tipleri
PUT /products/_doc/1
{
"name": "MacBook Pro 16", // text — full-text search için
"sku": "MBP-16-M3-2025", // keyword — exact match için
"price": 75000.00, // float
"quantity": 150, // integer
"in_stock": true, // boolean
"created_at": "2025-01-15T10:00:00Z", // date
"tags": ["apple", "laptop"], // array (keyword array)
"rating": 4.8, // float
"description": "Profesyonel laptop" // text
}Field Tipleri Detay
| Tip | Açıklama | Örnek | Kullanım |
|---|---|---|---|
| text | Full-text search | "Merhaba dünya" | Arama, analiz |
| keyword | Tam eşleşme | "ACTIVE", "TR" | Filtreleme, sıralama, aggregation |
| long/integer/short/byte | Tam sayı | 42, -10 | Sayısal filtreleme |
| float/double/half_float | Ondalıklı sayı | 3.14, 99.99 | Fiyat, oran |
| boolean | Doğru/Yanlış | true, false | Filtre |
| date | Tarih | "2025-01-15" | Zaman bazlı sorgular |
| object | İç içe JSON | {"city": "İstanbul"} | Yapısal veri |
| nested | Bağımsız iç dokümanlar | [{...}, {...}] | İlişkili alt dokümanlar |
| geo_point | Coğrafi koordinat | {"lat": 41.01, "lon": 28.97} | Harita sorguları |
| ip | IP adresi | "192.168.1.1" | Ağ analizi |
| completion | Otomatik tamamlama | "Elastic..." | Suggest |
text vs keyword — Kritik Fark
Bu Elasticsearch'teki en önemli ayrımlardan biri:
// text field → Analiz edilir → Full-text search için
// "Elasticsearch Öğreniyorum" → ["elasticsearch", "öğreniyorum"]
// match sorgusu ile aranır
// keyword field → Analiz edilmez → Tam eşleşme için
// "Elasticsearch Öğreniyorum" → "Elasticsearch Öğreniyorum" (aynen saklanır)
// term sorgusu ile aranır// Mapping'de ikisini birlikte kullan (multi-field)
PUT /products
{
"mappings": {
"properties": {
"category": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
// Arama yaparken:
// Full-text search → category alanını kullan
GET /products/_search
{
"query": { "match": { "category": "laptop bilgisayar" } }
}
// Exact match, aggregation, sorting → category.keyword kullan
GET /products/_search
{
"query": { "term": { "category.keyword": "Laptop" } }
}
GET /products/_search
{
"aggs": {
"by_category": {
"terms": { "field": "category.keyword" }
}
}
}Object Field — İç İçe JSON
PUT /products/_doc/1
{
"name": "MacBook Pro 16",
"specs": {
"cpu": "M3 Pro",
"ram": 18,
"storage": {
"type": "SSD",
"size": 512
}
}
}
// İç içe alanlara erişim (dot notation)
GET /products/_search
{
"query": {
"match": {
"specs.cpu": "M3"
}
}
}
GET /products/_search
{
"query": {
"term": {
"specs.storage.type": "SSD"
}
}
}⚠️ Dikkat: Object field'lar Elasticsearch tarafından flatten edilir (düzleştirilir). Bu, array of objects'te sorun yaratabilir:
// Bu dokümanı index'le:
PUT /orders/_doc/1
{
"products": [
{ "name": "Laptop", "quantity": 1 },
{ "name": "Mouse", "quantity": 5 }
]
}
// Elasticsearch bunu şu şekilde saklar (flatten):
// products.name: ["Laptop", "Mouse"]
// products.quantity: [1, 5]
// Bu sorgu YANLIŞ sonuç verir:
GET /orders/_search
{
"query": {
"bool": {
"must": [
{ "match": { "products.name": "Laptop" } },
{ "term": { "products.quantity": 5 } }
]
}
}
}
// "Laptop" + quantity 5 olan yok ama bu sorgu eşleşir!
// Çünkü name ve quantity arasındaki ilişki kayboldu.
// Çözüm: "nested" field tipi (Bölüm 6'da detaylı anlatılacak)Array Fields
Elasticsearch'te ayrı bir "array" tipi yoktur. Her field otomatik olarak array kabul eder:
// Tek değer
PUT /products/_doc/1
{
"tags": "laptop"
}
// Array değer — aynı field tipi
PUT /products/_doc/2
{
"tags": ["laptop", "apple", "premium"]
}
// İkisi de geçerli ve aynı mapping ile çalışırKural: Array içindeki tüm değerler aynı tipte olmalı. ["laptop", 42, true] şeklinde karışık tip olmaz.
Document Lifecycle — Yaşam Döngüsü
Bir document'ın Elasticsearch'teki yaşam döngüsünü anlamak önemli:
1. Indexing (Yazma)
PUT /products/_doc/1
{
"name": "MacBook Pro",
"price": 75000
}Perde arkasında:
Document, routing ile doğru primary shard'a yönlendirilir
Primary shard document'ı yazar
Replica shard'lara kopyalanır
~1 saniye sonra aranabilir hale gelir (refresh interval)
2. Reading (Okuma)
GET /products/_doc/1Document, herhangi bir shard'dan (primary veya replica) okunabilir. Bu, okuma kapasitesini artırır — daha fazla replica, daha fazla paralel okuma.
3. Updating (Güncelleme)
POST /products/_update/1
{
"doc": {
"price": 72000
}
}⚠️ Kritik bilgi: Elasticsearch'te güncelleme aslında sil ve yeniden yaz işlemidir. Document'lar immutable'dır (değiştirilemez). Eski document "deleted" olarak işaretlenir, yeni versiyonu yazılır. Eski versiyon merge sırasında temizlenir.
4. Deleting (Silme)
DELETE /products/_doc/1Silme de aslında "hemen silme" değildir. Document "deleted" olarak işaretlenir ve arka plan merge işlemlerinde fiziksel olarak temizlenir.
Versiyonlama
Her işlemde _version artar:
// İlk oluşturma
PUT /products/_doc/1
{ "name": "MacBook" }
// _version: 1
// Güncelleme
PUT /products/_doc/1
{ "name": "MacBook Pro" }
// _version: 2
// Tekrar güncelleme
POST /products/_update/1
{ "doc": { "price": 75000 } }
// _version: 3Optimistic Concurrency Control
Aynı document'ı iki process aynı anda güncellemeye çalışırsa ne olur? _seq_no ve _primary_term ile kontrol:
// Önce document'ı oku
GET /products/_doc/1
// "_seq_no": 5, "_primary_term": 1
// Güncelle — sadece seq_no ve primary_term eşleşirse
PUT /products/_doc/1?if_seq_no=5&if_primary_term=1
{
"name": "MacBook Pro",
"price": 72000
}
// Eğer başka biri arada güncelleme yaptıysa:
// 409 Conflict hatası alırsınBu, veritabanlarındaki "optimistic locking" mekanizmasının Elasticsearch karşılığıdır.
Birden Fazla Document ile Çalışma
Multi-Get (_mget)
Birden fazla document'ı tek seferde getir:
GET /_mget
{
"docs": [
{ "_index": "products", "_id": "1" },
{ "_index": "products", "_id": "2" },
{ "_index": "products", "_id": "3" }
]
}
// Aynı index'ten çekiyorsan kısa yol:
GET /products/_mget
{
"ids": ["1", "2", "3"]
}Bulk API
Birden fazla işlemi tek seferde yap:
POST /_bulk
{"index": {"_index": "products", "_id": "10"}}
{"name": "Keyboard", "price": 500}
{"index": {"_index": "products", "_id": "11"}}
{"name": "Mouse", "price": 300}
{"update": {"_index": "products", "_id": "10"}}
{"doc": {"price": 450}}
{"delete": {"_index": "products", "_id": "11"}}Bulk API, performans için kritiktir. 1000 document ekleyeceksen 1000 ayrı request yerine 1 bulk request gönder. Bölüm 3'te detaylı göreceğiz.
Index Templates
Aynı pattern'deki index'ler için otomatik ayar ve mapping tanımla:
// Index template oluştur
PUT /_index_template/logs-template
{
"index_patterns": ["app-logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"service": { "type": "keyword" }
}
}
},
"priority": 100
}
// Artık "app-logs-" ile başlayan her index otomatik bu ayarlarla oluşturulur
POST /app-logs-2025.01.15/_doc
{
"@timestamp": "2025-01-15T10:00:00Z",
"level": "ERROR",
"message": "Database connection failed",
"service": "payment-service"
}
// Index otomatik oluşturuldu, mapping template'ten geldiJava ile Document İşlemleri
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
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;
import java.util.List;
import java.util.Map;
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())
);
// Document oluştur
Map<String, Object> product = Map.of(
"name", "MacBook Pro 16",
"price", 75000,
"category", "Laptop",
"in_stock", true,
"tags", List.of("apple", "laptop", "premium")
);
IndexResponse response = client.index(i -> i
.index("products")
.id("1")
.document(product)
);
System.out.println("Result: " + response.result()); // Created
System.out.println("Version: " + response.version()); // 1
// Document oku
GetResponse<Map> getResponse = client.get(g -> g
.index("products")
.id("1"),
Map.class
);
System.out.println("Found: " + getResponse.found());
System.out.println("Source: " + getResponse.source());
// Document var mı kontrol et
boolean exists = client.exists(e -> e
.index("products")
.id("1")
).value();
System.out.println("Exists: " + exists);
restClient.close();
}
}Best Practices
✅ Index başına tek tip doküman — "products" index'ine hem ürün hem kullanıcı koyma
✅ Anlamlı index adları kullan — products, app-logs-2025.01, user-sessions
✅ Alias kullan — Uygulama doğrudan index adını bilmemeli, alias üzerinden çalışmalı
✅ ID stratejini belirle — DB sync → DB ID kullan; log → otomatik ID
✅ Mapping önceden tanımla — Dynamic mapping'e güvenme (sonraki ders)
✅ Denormalize et — SQL'deki gibi join yapmaya çalışma, veriyi document'ın içine göm
✅ Bulk API kullan — Toplu işlemlerde tek tek request atma
Yaygın Hatalar
❌ "Index'i veritabanı gibi kullanıyorum"
RDBMS'de products tablosu + categories tablosu + JOIN. Elasticsearch'te products index'inde her ürünün kendi kategori bilgisi var — denormalize.
❌ "Array of objects ile nested sorgu yapıyorum"
Object array'lerde alan ilişkileri kaybolur. İlişkiyi korumak istiyorsan nested field tipi kullan.
❌ "_source'u devre dışı bırakıyorum"
_source field'ını kapatmak disk tasarrufu sağlar ama update, reindex ve highlight yapamazsın. Özel durumlar dışında kapatma.
❌ "Dynamic mapping yeterli, explicit tanımlamaya gerek yok"
Dynamic mapping string'leri hem text hem keyword olarak ikili index'ler — disk israfı. Ayrıca sayısal string'leri text olarak algılayabilir. Explicit mapping tanımla.
❌ "Tek büyük index yapıyorum"
Zaman bazlı veri (loglar, metrikler) için günlük/haftalık/aylık index'ler oluştur. Eski veriyi silmek, compress etmek, taşımak kolaylaşır.
Özet
Index, benzer document'ların toplandığı mantıksal birimdir — RDBMS'teki tabloya benzer ama daha esnek
Document, Elasticsearch'teki en küçük veri birimi — JSON formatında, metadata alanlarıyla birlikte saklanır
Field, document içindeki her bir veri alanıdır — text, keyword, integer, date gibi tipleri vardır
text vs keyword en kritik ayrımdır: text analiz edilir (arama), keyword aynen saklanır (filtreleme/aggregation)
Alias kullanmak, index değişikliklerinde zero-downtime sağlar
Document'lar immutable'dır — güncelleme aslında sil + yeniden yaz demektir
Denormalize veri modeli Elasticsearch'ün doğasına uygundur — SQL join'lerinden kaçın
Bir sonraki derste Mapping konusuna dalacağız — field tipleri, dynamic mapping ve schema tanımlamanın inceliklerini öğreneceğiz.
AI Asistan
Sorularını yanıtlamaya hazır