← Kursa Dön
📄 Text · 30 min

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:

RDBMSElasticsearchAçıklama
DatabaseClusterEn üst seviye gruplandırma
TableIndexBenzer dokümanların toplandığı yer
RowDocumentTek bir veri kaydı
ColumnFieldDoküman içindeki bir alan
SchemaMappingVeri yapısı tanımı
SQLQuery DSLSorgu dili
Primary Key_idBenzersiz 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-v2

Index 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-*?v

Index 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 → Yok

Index 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ır

Bu 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,tags

Document Metadata Alanları

Her document'ın otomatik oluşturulan metadata alanları vardır:

AlanAçıklamaDeğiştirilebilir mi?
_indexDocument'ın bulunduğu indexHayır (taşıma ile değişir)
_idBenzersiz tanımlayıcıEvet (sen belirlersin veya otomatik)
_sourceOrijinal JSON verisiDevre dışı bırakılabilir (önerilmez)
_versionVersiyon numarasıOtomatik artar
_seq_noSıra numarası (concurrency control)Otomatik
_primary_termPrimary shard term'iOtomatik
_routingShard routing değeriÖzelleştirilebilir
_scoreArama relevance skoruSadece 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?

DurumID Stratejisi
Veritabanından sync ediyorsunDB'deki primary key'i kullan
Log verisiOtomatik 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

TipAçıklamaÖrnekKullanım
textFull-text search"Merhaba dünya"Arama, analiz
keywordTam eşleşme"ACTIVE", "TR"Filtreleme, sıralama, aggregation
long/integer/short/byteTam sayı42, -10Sayısal filtreleme
float/double/half_floatOndalıklı sayı3.14, 99.99Fiyat, oran
booleanDoğru/Yanlıştrue, falseFiltre
dateTarih"2025-01-15"Zaman bazlı sorgular
objectİç içe JSON{"city": "İstanbul"}Yapısal veri
nestedBağımsız iç dokümanlar[{...}, {...}]İlişkili alt dokümanlar
geo_pointCoğrafi koordinat{"lat": 41.01, "lon": 28.97}Harita sorguları
ipIP adresi"192.168.1.1"Ağ analizi
completionOtomatik 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ışır

Kural: 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:

  1. Document, routing ile doğru primary shard'a yönlendirilir

  2. Primary shard document'ı yazar

  3. Replica shard'lara kopyalanır

  4. ~1 saniye sonra aranabilir hale gelir (refresh interval)

2. Reading (Okuma)

GET /products/_doc/1

Document, 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/1

Silme 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: 3

Optimistic 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ın

Bu, 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 geldi

Java 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ı kullanproducts, 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.