← Kursa Dön
📄 Text · 40 min

Elasticsearch İç Yapısı — Lucene, Segment, Translog

Giriş — Motorun Kaputunu Açıyoruz

Bir araba kullanırken motorun nasıl çalıştığını bilmenize gerek yoktur. Ama bir yarış pilotu iseniz — enjeksiyon sistemi, turbo basıncı, vites oranları hakkında derin bilgi sahibi olmanız gerekir. Çünkü limit durumlarda performansı bu bilgi belirler.

Elasticsearch'ü "kullanmak" için iç yapısını bilmenize gerek yok. Ama production ortamında performans sorunları yaşadığınızda, refresh interval'ı neden değiştirmeniz gerektiğini, merge policy'nin disk I/O'yu nasıl etkilediğini veya translog'un veri güvenliğiyle ilişkisini anlamanız şart olur. Bu ders, Elasticsearch'ün kaputunu açıyor.


1. Apache Lucene — Elasticsearch'ün Kalbi

1.1 Lucene Nedir?

Elasticsearch bir arama motoru değildir — bir dağıtık arama platformudur. Gerçek arama motoru, altında çalışan Apache Lucene'dir. Lucene, Doug Cutting tarafından 1999'da Java ile yazılmış, dünyanın en performanslı full-text search kütüphanesidir.

Elasticsearch Mimarisi:
┌─────────────────────────────────────────────┐
│            Elasticsearch                      │
│  ┌────────────────────────────────────────┐  │
│  │  Dağıtık Koordinasyon Katmanı          │  │
│  │  (Cluster, Shard Routing, Replication) │  │
│  └────────────────┬───────────────────────┘  │
│                   │                           │
│  ┌────────────────▼───────────────────────┐  │
│  │  REST API + Query DSL                   │  │
│  │  (HTTP Interface, JSON Processing)      │  │
│  └────────────────┬───────────────────────┘  │
│                   │                           │
│  ┌────────────────▼───────────────────────┐  │
│  │  Apache Lucene                          │  │
│  │  (Indexing, Searching, Scoring)         │  │
│  │  ┌──────────┐ ┌──────────┐ ┌────────┐  │  │
│  │  │ Segment 1│ │ Segment 2│ │Segment3│  │  │
│  │  └──────────┘ └──────────┘ └────────┘  │  │
│  └─────────────────────────────────────────┘  │
└───────────────────────────────────────────────┘

Elasticsearch'ün yaptığı şeyleri kategorize edelim:

Lucene'in işi:

  • Inverted index oluşturma ve arama

  • Token'ları segmentlere yazma

  • BM25 scoring

  • Segment merge

  • Stored fields, doc values

Elasticsearch'ün işi:

  • Dağıtık koordinasyon (cluster, shard routing)

  • REST API sağlama

  • JSON doküman yönetimi

  • Replica senkronizasyonu

  • Cluster health monitoring

  • Cross-shard aggregation

1.2 Lucene Index vs Elasticsearch Index

Bu terminoloji kafa karıştırıcı olabilir:

Elasticsearch Index = Mantıksal container
    ├── Shard 0 = 1 Lucene Index
    ├── Shard 1 = 1 Lucene Index
    ├── Shard 2 = 1 Lucene Index
    └── ...

Her Lucene Index = Birden fazla Segment
    ├── Segment 0 (immutable)
    ├── Segment 1 (immutable)
    ├── Segment 2 (immutable)
    └── ...

Yani bir Elasticsearch index'i 3 primary shard'a sahipse, aslında 3 bağımsız Lucene index'i vardır. Her Lucene index'i de birden fazla segmentten oluşur.

REST API ile segment bilgisi:

GET /my-index/_segments

// Yanıt:
{
  "_shards": { "total": 2, "successful": 2 },
  "indices": {
    "my-index": {
      "shards": {
        "0": [{
          "routing": { "state": "STARTED", "primary": true, "node": "node-1" },
          "num_committed_segments": 3,
          "num_search_segments": 3,
          "segments": {
            "_0": {
              "generation": 0,
              "num_docs": 15000,
              "deleted_docs": 200,
              "size_in_bytes": 5242880,
              "committed": true,
              "search": true,
              "version": "9.7.0",
              "compound": true
            },
            "_1": {
              "generation": 1,
              "num_docs": 8000,
              "deleted_docs": 50,
              "size_in_bytes": 2621440,
              "committed": true,
              "search": true
            }
          }
        }]
      }
    }
  }
}

2. Segment Yapısı — Immutable Veri Blokları

2.1 Segment Nedir?

Segment, Lucene'in temel veri birimidir. Her segment immutable (değiştirilemez) bir mini-index'tir. Bir segment oluşturulduktan sonra asla değiştirilmez — sadece silinebilir (merge sırasında).

Bir Segment İçeriği:
┌──────────────────────────────────┐
│           Segment _0              │
├──────────────────────────────────┤
│ Inverted Index                    │
│   term → [doc1, doc3, doc7, ...] │
│   term → [doc2, doc5, ...]       │
├──────────────────────────────────┤
│ Stored Fields                     │
│   doc1 → {_source JSON}          │
│   doc2 → {_source JSON}          │
├──────────────────────────────────┤
│ Doc Values (columnar)             │
│   field → [val1, val2, val3...]  │
├──────────────────────────────────┤
│ Term Vectors (opsiyonel)          │
├──────────────────────────────────┤
│ Norms (length normalization)      │
├──────────────────────────────────┤
│ Points (numeric, geo, date)       │
├──────────────────────────────────┤
│ Live Docs Bitset                  │
│   [1, 1, 0, 1, 1, 0, ...]       │
│   (0 = deleted)                   │
└──────────────────────────────────┘

2.2 Neden Immutable?

Segment'lerin değiştirilemez olmasının çok önemli nedenleri var:

1. Concurrency (Eşzamanlılık): Immutable veri yapıları lock gerektirmez. Birden fazla thread aynı segment'i aynı anda okuyabilir — hiçbir senkronizasyon overhead'i yoktur.

2. Cache Dostu: İşletim sistemi, immutable dosyaları OS file cache'inde tutabilir çünkü değişmeyeceklerini bilir. Bu, disk I/O'yu dramatik şekilde azaltır.

3. Sıkıştırma: Immutable veriler üzerinde daha agresif sıkıştırma uygulanabilir — çünkü veri değişmeyeceği için sıkıştırma bir kez yapılır.

4. Basitlik: Write-once semantiği, veri yapısını çok daha basit tutar. Karmaşık güncelleme mekanizmalarına gerek yoktur.

2.3 Doküman Güncelleme ve Silme

Segment'ler immutable olduğuna göre, güncelleme ve silme nasıl çalışır?

Silme: Doküman gerçekten silinmez. Segment içindeki live docs bitset'te ilgili bit 0'a çevrilir. Bu doküman artık arama sonuçlarında görünmez ama fiziksel olarak hâlâ segment'te durur.

Güncelleme: Eski doküman "silinir" (bitset'te 0), yeni versiyonu yeni bir segment'e yazılır. Yani her güncelleme aslında delete + insert'tir.

Doküman Güncelleme Akışı:
1. doc-1 (v1) → Segment A'da [live]
2. Update doc-1 (v2) gelir
3. Segment A'da doc-1 → [deleted] (bitset: 0)
4. doc-1 (v2) → In-memory buffer'a yazılır
5. Refresh → Yeni segment B oluşur, doc-1 (v2) burada [live]

Fiziksel silme ancak merge sırasında gerçekleşir — merge edilirken deleted dokümanlar gerçekten atılır.

// Silinen doküman sayısını görmek
GET /my-index/_stats/docs

// Yanıt:
{
  "_all": {
    "primaries": {
      "docs": {
        "count": 100000,      // Canlı doküman sayısı
        "deleted": 15000       // Silinmiş ama hâlâ disk'te olan
      }
    }
  }
}

⚠️ Dikkat: Çok fazla güncelleme yapan index'lerde deleted sayısı hızla artar. Bu, disk kullanımını ve arama performansını olumsuz etkiler çünkü her aramada deleted dokümanlar da kontrol edilir (sadece sonuçtan çıkarılır). Bu durumda force merge düşünülebilir.


3. Write İşlemi — Doküman Nasıl Index'lenir?

3.1 Write Akışı Detaylı

Bir doküman index'lediğinizde neler olur? Adım adım bakalım:

POST /products/_doc/1 { "title": "Laptop", "price": 999 }

Adım 1: Coordinating Node
    ├── Dokümanı alır
    ├── Routing hesaplar: shard = hash(_id) % num_primary_shards
    └── İlgili primary shard'a yönlendirir

Adım 2: Primary Shard (Lucene Level)
    ├── a) Doküman in-memory buffer'a eklenir
    ├── b) Doküman translog'a yazılır (disk'e fsync)
    └── c) "Acknowledge" döner

Adım 3: Replica Sync
    ├── Primary, dokümanı tüm replica'lara gönderir
    ├── Replica'lar da kendi buffer + translog'a yazar
    └── Tüm replica'lar onayladığında istemciye yanıt döner

Adım 4: Refresh (varsayılan 1 saniye sonra)
    ├── In-memory buffer → Yeni Lucene segment (in-memory)
    ├── Segment aranabilir hale gelir
    └── Buffer temizlenir

Adım 5: Flush (periyodik veya translog boyut limiti)
    ├── In-memory segment'ler disk'e yazılır
    ├── Commit point oluşturulur
    └── Translog temizlenir

3.2 In-Memory Buffer

Dokümanlar ilk olarak in-memory buffer'a (indexing buffer) yazılır. Bu buffer henüz aranabilir değildir — yani dokümanı yazdıktan hemen sonra arayamazsınız (near-realtime).

Buffer → [doc1, doc2, doc3, doc4, ...]
                    │
              refresh (1s)
                    │
                    ▼
          Yeni Segment (aranabilir)

Buffer boyutu indices.memory.index_buffer_size ile kontrol edilir:

# elasticsearch.yml
indices.memory.index_buffer_size: 10%    # Varsayılan: heap'in %10'u
indices.memory.min_index_buffer_size: 48mb
indices.memory.max_index_buffer_size: unbounded

💡 İpucu: Yoğun indexing yapıyorsanız (bulk import gibi), buffer size'ı artırmak performansı önemli ölçüde artırabilir. Ama bu, diğer işlemler için (arama, aggregation) daha az heap kalması demektir.


4. Transaction Log (Translog) — Veri Güvenliği

4.1 Translog Nedir?

In-memory buffer'daki veriler henüz disk'e yazılmamıştır. Sunucu çökerse bu veriler kaybolur. İşte translog (transaction log) tam da bu sorunu çözer.

Her write işlemi hem in-memory buffer'a hem de translog dosyasına yazılır. Translog, disk'e fsync edilir — yani güç kesilse bile veri korunur.

Write İşlemi:
    │
    ├──────────────► In-Memory Buffer (hızlı, volatile)
    │
    └──────────────► Translog (disk, fsync, durable)

Sunucu çöktüğünde yeniden başlatılırken:

  1. Son commit point'ten disk'teki segment'ler yüklenir

  2. Translog'daki commit point'ten sonraki işlemler tekrar oynatılır (replay)

  3. Veri kaybı olmaz

4.2 Translog Yapılandırması

PUT /my-index/_settings
{
  "index": {
    "translog": {
      "durability": "request",
      "sync_interval": "5s",
      "flush_threshold_size": "512mb"
    }
  }
}

`durability` parametresi:

  • `request` (varsayılan): Her index/delete/update işleminden sonra translog fsync edilir. En güvenli ama en yavaş

  • `async`: Translog sync_interval aralıklarıyla fsync edilir. Daha hızlı ama sync_interval kadar veri kaybı riski var

// Yüksek throughput gerektiğinde (bazı veri kaybı kabul edilebilir)
PUT /logs-index/_settings
{
  "index": {
    "translog": {
      "durability": "async",
      "sync_interval": "30s"
    }
  }
}

⚠️ Dikkat: durability: async ayarı, sync_interval kadar (varsayılan 5 saniye) veri kaybı riski taşır. Log verileri gibi kaybolması tolere edilebilir veriler için kullanılabilir. Finansal işlemler gibi kritik veriler için asla async kullanmayın.

`flush_threshold_size`: Translog bu boyuta ulaştığında otomatik flush tetiklenir. Varsayılan 512MB'tır.

4.3 Translog İstatistikleri

GET /my-index/_stats/translog

// Yanıt:
{
  "_all": {
    "primaries": {
      "translog": {
        "operations": 125000,
        "size_in_bytes": 67108864,
        "uncommitted_operations": 5000,
        "uncommitted_size_in_bytes": 2621440,
        "earliest_last_modified_age": 30000
      }
    }
  }
}
  • operations: Toplam translog işlem sayısı

  • uncommitted_operations: Henüz commit edilmemiş (flush bekleyen) işlem sayısı

  • earliest_last_modified_age: En eski uncommitted işlemin yaşı (ms)


5.1 Refresh Mekanizması

Refresh, in-memory buffer'daki dokümanları yeni bir in-memory segment haline getirip aranabilir yapma işlemidir. Segment henüz disk'e yazılmaz ama aranabilir hale gelir.

Refresh Öncesi:
Buffer: [doc1, doc2, doc3]    ← Aranamaz
Segment_0: [eski docs]        ← Aranabilir

Refresh Sonrası:
Buffer: []                     ← Temizlendi
Segment_0: [eski docs]        ← Aranabilir
Segment_1: [doc1,doc2,doc3]   ← Yeni, aranabilir!

Varsayılan refresh interval: 1 saniye. Bu, Elasticsearch'ün "near-realtime" (yakın gerçek zamanlı) olarak adlandırılmasının nedenidir — doküman yazıldıktan en fazla 1 saniye sonra aranabilir hale gelir.

5.2 Refresh Interval Yapılandırması

// Index bazında refresh interval
PUT /my-index/_settings
{
  "index": {
    "refresh_interval": "30s"
  }
}

Farklı senaryolar için önerilen değerler:

Senaryo                          refresh_interval
───────────────────────────────────────────────
Gerçek zamanlı arama (varsayılan)     1s
Log indexing (yoğun yazma)            30s
Bulk import (tek seferlik)            -1 (kapalı)
Near-realtime dashboard               5s
Yüksek throughput + toleranslı arama  10s-60s

5.3 Manuel Refresh

// Belirli bir index'i refresh et
POST /my-index/_refresh

// Tüm index'leri refresh et
POST /_refresh

Bulk import senaryosu — refresh'i kapatıp açma:

// 1. Refresh'i kapat
PUT /my-index/_settings
{ "index": { "refresh_interval": "-1" } }

// 2. Bulk import yap
POST /my-index/_bulk
{ "index": {} }
{ "title": "Doc 1" }
{ "index": {} }
{ "title": "Doc 2" }
// ... binlerce doküman

// 3. İmport bitti, refresh'i aç
PUT /my-index/_settings
{ "index": { "refresh_interval": "1s" } }

// 4. Son bir refresh yap
POST /my-index/_refresh

5.4 Refresh'in Maliyeti

Her refresh yeni bir segment oluşturur. Çok sık refresh → çok fazla küçük segment → daha fazla file descriptor, daha fazla merge ihtiyacı, daha yavaş arama.

1 saniye refresh ile 1 dakikada:
  → 60 yeni segment oluşur
  → Her segment OS file handle kullanır
  → Her aramada 60 segment taranır
  → Merge pressure artar

30 saniye refresh ile 1 dakikada:
  → 2 yeni segment oluşur
  → Çok daha az overhead

💡 İpucu: Eğer index'inize saniyede yüzlerce doküman yazıyorsanız ve arama gecikmeniz artıyorsa, ilk denemeniz gereken şey refresh_interval'ı artırmaktır. 1s → 5s veya 30s yapmak dramatik performans iyileştirmesi sağlayabilir.


6. Flush ve Commit Point

6.1 Flush Nedir?

Flush, refresh'ten farklıdır. Flush:

  1. In-memory segment'leri disk'e yazar (fsync)

  2. Yeni bir commit point oluşturur

  3. Translog'u temizler

Flush Öncesi:
  Memory:   [Segment_1] [Segment_2]    ← In-memory segment'ler
  Disk:     [Segment_0]                 ← Daha önce flush edilmiş
  Translog: [op1, op2, op3, ..., op500] ← Büyüyor

Flush Sonrası:
  Memory:   []                           ← Temizlendi
  Disk:     [Segment_0] [Segment_1] [Segment_2]  ← Hepsi disk'te
  Translog: []                           ← Temizlendi
  Commit:   [Segment_0, Segment_1, Segment_2]    ← Yeni commit point

6.2 Commit Point

Commit point, disk'teki hangi segment'lerin geçerli olduğunu kaydeden bir dosyadır. Sunucu çöküp yeniden başlatıldığında, commit point'teki segment'ler yüklenir ve translog'daki kalan işlemler replay edilir.

Commit Point Dosyası:
├── segments_3        ← Lucene commit point
│   ├── Segment _0    ← geçerli
│   ├── Segment _1    ← geçerli
│   └── Segment _2    ← geçerli
└── segments.gen      ← Güncel generation numarası

6.3 Flush Tetikleme

Flush otomatik olarak tetiklenir:

  • Translog boyutu flush_threshold_size'ı aştığında (varsayılan 512MB)

  • Belirli aralıklarla (Elasticsearch kendi yönetir)

Manuel flush:

// Manuel flush
POST /my-index/_flush

// Synced flush (deprecated in 8.x, artık otomatik)
POST /my-index/_flush/synced

6.4 Refresh vs Flush — Karşılaştırma

┌─────────────┬────────────────────────┬────────────────────────────┐
│             │ Refresh                │ Flush                      │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Ne yapar?   │ Buffer → In-memory     │ In-memory segment →       │
│             │ segment                │ Disk segment               │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Aranabilir? │ Evet (segment oluşur)  │ Zaten aranabilirdi         │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Disk yazımı │ Hayır                  │ Evet (fsync)               │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Translog    │ Değişmez               │ Temizlenir                 │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Commit point│ Değişmez               │ Yeni commit point oluşur   │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Varsayılan  │ Her 1 saniye           │ Translog 512MB olunca      │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Maliyet     │ Düşük (memory)         │ Yüksek (disk I/O)          │
├─────────────┼────────────────────────┼────────────────────────────┤
│ Kullanım    │ Aranabilirlik kontrolü │ Veri dayanıklılığı         │
└─────────────┴────────────────────────┴────────────────────────────┘

Veri akışı özet:

Doküman → Buffer (aranamaz) → [REFRESH] → In-memory Segment (aranabilir)
                                              → [FLUSH] → Disk Segment (kalıcı)
       → Translog (her zaman disk'te) ─────────[FLUSH]──→ Temizlenir

7. Merge — Segment Birleştirme

7.1 Neden Merge Gerekli?

Her refresh yeni bir segment oluşturur. Zamanla segment sayısı artar. Çok fazla segment:

  • Her aramada tüm segment'ler taranır → yavaşlama

  • Her segment OS file handle kullanır → kaynak tüketimi

  • Deleted dokümanlar hâlâ yer kaplar → disk israfı

Merge işlemi birden fazla küçük segmenti alır, birleştirir ve tek bir büyük segment oluşturur. Bu sırada deleted dokümanlar da fiziksel olarak temizlenir.

Merge Öncesi:
  [Seg_0: 1000 docs, 200 deleted]
  [Seg_1: 500 docs, 50 deleted]
  [Seg_2: 300 docs, 10 deleted]
  [Seg_3: 800 docs, 100 deleted]
  = 4 segment, 2600 live docs, 360 deleted

Merge Sonrası:
  [Seg_4: 2600 docs, 0 deleted]
  = 1 segment, 2600 live docs, 0 deleted

7.2 Merge Policy

Lucene, hangi segment'lerin ne zaman merge edileceğini belirlemek için bir merge policy kullanır. Elasticsearch varsayılan olarak TieredMergePolicy kullanır.

TieredMergePolicy parametreleri:

PUT /my-index/_settings
{
  "index": {
    "merge": {
      "policy": {
        "floor_segment": "2mb",
        "max_merge_at_once": 10,
        "max_merged_segment": "5gb",
        "segments_per_tier": 10,
        "deletes_pct_allowed": 20.0
      }
    }
  }
}
  • `floor_segment` (varsayılan 2MB): Bu boyutun altındaki segment'ler bu boyuta "yuvarlanır" — çok küçük segment'lerin merge önceliğini artırır

  • `max_merge_at_once` (varsayılan 10): Bir merge işleminde en fazla kaç segment birleştirilebilir

  • `max_merged_segment` (varsayılan 5GB): Merge sonucu oluşacak segment'in maksimum boyutu. Bundan büyük segment'ler merge edilmez (force merge hariç)

  • `segments_per_tier` (varsayılan 10): Her boyut katmanında kaç segment tutulacağı

  • `deletes_pct_allowed` (varsayılan 20.0): Segment'teki deleted doküman yüzdesi bu değeri aşarsa merge öncelik kazanır

7.3 Merge Scheduler

PUT /my-index/_settings
{
  "index": {
    "merge": {
      "scheduler": {
        "max_thread_count": 1,
        "max_merge_count": 6
      }
    }
  }
}
  • `max_thread_count`: Eşzamanlı merge thread sayısı

- HDD için: 1 (HDD sequential write'ta iyidir, paralel merge disk'i yorar) - SSD için: Math.max(1, Math.min(4, cores/2)) (varsayılan)

  • `max_merge_count`: Maksimum eşzamanlı merge sayısı. Bu limit aşılırsa indexing throttle edilir

💡 İpucu: HDD kullanıyorsanız max_thread_count: 1 ayarı çok önemlidir. Paralel merge HDD'de random I/O oluşturur ve performansı ciddi şekilde düşürür.

7.4 Force Merge

Force merge, segment sayısını manuel olarak azaltmanızı sağlar. Sadece artık yazılmayan index'ler için kullanılmalıdır.

// Segment sayısını 1'e düşür
POST /old-logs-2024.01/_forcemerge?max_num_segments=1

// Sadece deleted dokümanları temizle
POST /my-index/_forcemerge?only_expunge_deletes=true

Java ile force merge:

ForcemergeRequest forcemergeRequest = ForcemergeRequest.of(f -> f
    .index("old-logs-2024.01")
    .maxNumSegments(1L)
);

ForcemergeResponse response = client.indices().forcemerge(forcemergeRequest);
System.out.println("Shards: " + response.shards().successful() + " successful");

⚠️ Dikkat: Force merge çok kaynak yoğun bir işlemdir. Aktif yazılan index'lerde force merge yapmayın — yeni segment'ler oluşmaya devam edeceği için merge sonuçsuz kalır ve sadece disk I/O harcar. Force merge, "artık değişmeyecek" index'ler için idealdir (örneğin geçmiş ay log'ları).

7.5 Merge İstatistikleri

GET /my-index/_stats/merge

// Yanıt:
{
  "_all": {
    "primaries": {
      "merges": {
        "current": 0,
        "current_docs": 0,
        "current_size_in_bytes": 0,
        "total": 150,
        "total_time_in_millis": 45000,
        "total_docs": 500000,
        "total_size_in_bytes": 2147483648,
        "total_stopped_time_in_millis": 0,
        "total_throttled_time_in_millis": 5000
      }
    }
  }
}
  • current: Şu anda devam eden merge sayısı

  • total_throttled_time_in_millis: Merge throttling süresi — eğer bu değer yüksekse, merge indexing'i yavaşlatıyor demektir


8. Near-Realtime Search Mekanizması

8.1 Realtime vs Near-Realtime

Gerçek zamanlı (realtime): Doküman yazıldığı anda aranabilir. Elasticsearch bunu sağlamaz (varsayılan olarak).

Yakın gerçek zamanlı (near-realtime): Doküman yazıldıktan 1 saniye sonra (varsayılan refresh_interval) aranabilir. Elasticsearch bunu sağlar.

Timeline:
t=0.0s  → POST /products/_doc/1 {"title":"Laptop"}  → 201 Created
t=0.1s  → GET /products/_search?q=Laptop             → 0 hits (henüz refresh olmadı!)
t=0.5s  → GET /products/_search?q=Laptop             → 0 hits (hâlâ...)
t=1.0s  → [REFRESH gerçekleşir]
t=1.1s  → GET /products/_search?q=Laptop             → 1 hit ✅

8.2 Anında Aranabilirlik İstiyorsanız

İki seçenek var:

Seçenek 1: `refresh=true` parametresi

// Dokümanı yaz ve hemen refresh et
POST /products/_doc/1?refresh=true
{
  "title": "Laptop"
}
// Artık hemen aranabilir

Seçenek 2: `refresh=wait_for` parametresi

// Dokümanı yaz, sonraki refresh'i bekle
POST /products/_doc/1?refresh=wait_for
{
  "title": "Laptop"
}
// Yanıt, doküman aranabilir olduğunda döner

Fark:

  • refresh=true: Hemen yeni bir refresh tetikler. Çok sık kullanılırsa performansı öldürür

  • refresh=wait_for: Sonraki doğal refresh'i bekler. Daha nazik ama biraz daha yavaş

// Java'da refresh seçenekleri
// refresh=true
IndexRequest<Product> request = IndexRequest.of(i -> i
    .index("products")
    .id("1")
    .document(product)
    .refresh(Refresh.True)
);

// refresh=wait_for
IndexRequest<Product> request2 = IndexRequest.of(i -> i
    .index("products")
    .id("2")
    .document(product)
    .refresh(Refresh.WaitFor)
);

⚠️ Dikkat: refresh=true'yu her write işleminde kullanmak, indexing throughput'unu %50-90 oranında düşürebilir. Sadece gerçekten anında aranabilirlik gerektiğinde kullanın. Çoğu durumda 1 saniyelik gecikme kabul edilebilirdir.

8.3 GET API vs Search API

İlginç bir detay: GET /my-index/_doc/1 (doküman ID ile doğrudan getirme) near-realtime değil, realtime'dır! GET API, translog'dan da okuyabilir — yani refresh olmadan bile dokümanı bulur.

// Dokümanı yaz
POST /products/_doc/1
{ "title": "Laptop" }

// Hemen ID ile get → BULUR (realtime)
GET /products/_doc/1    → 200 OK, {"title": "Laptop"}

// Hemen search → BULAMAZ (near-realtime, refresh beklenmeli)
GET /products/_search?q=Laptop    → 0 hits

Bu fark, GET API'nin inverted index yerine doğrudan doküman ID ile translog/stored fields'tan okuması sayesindedir.

Eğer bu davranışı kapatmak isterseniz:

GET /products/_doc/1?realtime=false

9. Store Types ve OS Cache

9.1 Memory-Mapped Files ve OS Cache

Elasticsearch'ün performansının büyük kısmı OS file cache (page cache) sayesindedir. Lucene segment dosyaları OS tarafından RAM'e cache'lenir. Bu yüzden Elasticsearch'e verilen heap'in yanında, OS cache için de yeterli RAM bırakmak kritik önemdedir.

Elasticsearch varsayılan olarak hybridfs store type kullanır — mmapfs ve niofs'un hybrid'i, dosya tipine göre en uygun okuma yöntemini seçer.

Sunucu: 64 GB RAM
├── Elasticsearch JVM Heap: 31 GB  (max %50 önerisi)
├── OS / Diğer procesler: 1 GB
└── OS File Cache: ~32 GB  ← Segment dosyaları burada cache'lenir!

💡 İpucu: Elasticsearch sunucusunda heap'i 32GB'ın üzerine çıkarmayın. JVM 32GB altında Compressed OOPs (Ordinary Object Pointers) kullanır — pointerlar 4 byte yerine 8 byte'a çıkmaz. 32GB heap, compressed OOPs sayesinde ~48GB'lık adres alanını gösterebilir. 32GB'ı aşarsanız, compressed OOPs devre dışı kalır ve aslında daha az efektif bellek elde edersiniz.


10. Gerçek Dünya Optimizasyon Senaryosu

10.1 Senaryo: Yoğun Log Indexing

Saniyede 50,000 log satırı index'leyen bir sistemde performans sorunları yaşıyorsunuz. Aşağıdaki optimizasyonları sırayla uygulayalım:

// 1. Refresh interval'ı artır (log'lar için 30s yeterli)
PUT /logs-*/_settings
{
  "index": { "refresh_interval": "30s" }
}

// 2. Translog'u async yap (log verisi için kabul edilebilir risk)
PUT /logs-*/_settings
{
  "index": {
    "translog": {
      "durability": "async",
      "sync_interval": "30s",
      "flush_threshold_size": "1gb"
    }
  }
}

// 3. Merge thread'lerini disk tipine göre ayarla
PUT /logs-*/_settings
{
  "index": {
    "merge": {
      "scheduler": {
        "max_thread_count": 1   // HDD için
      }
    }
  }
}

// 4. Replica'ları indexing sırasında kapat, sonra aç
PUT /logs-*/_settings
{
  "index": { "number_of_replicas": 0 }
}
// Bulk import tamamlandıktan sonra:
PUT /logs-*/_settings
{
  "index": { "number_of_replicas": 1 }
}

Java ile bu optimizasyonları uygulama:

public class IndexingOptimizer {
    private final ElasticsearchClient client;

    public IndexingOptimizer(ElasticsearchClient client) {
        this.client = client;
    }

    public void optimizeForBulkIndexing(String indexPattern) throws IOException {
        // Refresh interval artır
        client.indices().putSettings(s -> s
            .index(indexPattern)
            .settings(st -> st
                .refreshInterval(t -> t.time("30s"))
            )
        );

        // Replica'ları kapat
        client.indices().putSettings(s -> s
            .index(indexPattern)
            .settings(st -> st
                .numberOfReplicas("0")
            )
        );

        System.out.println("Index optimized for bulk indexing");
    }

    public void restoreAfterBulkIndexing(String indexPattern) throws IOException {
        // Refresh interval'ı geri al
        client.indices().putSettings(s -> s
            .index(indexPattern)
            .settings(st -> st
                .refreshInterval(t -> t.time("1s"))
            )
        );

        // Replica'ları geri aç
        client.indices().putSettings(s -> s
            .index(indexPattern)
            .settings(st -> st
                .numberOfReplicas("1")
            )
        );

        // Force refresh
        client.indices().refresh(r -> r.index(indexPattern));

        System.out.println("Index settings restored for normal operation");
    }
}

10.2 İç Yapı Monitoring Checklist

Production'da izlenmesi gereken internal metrikler:

// 1. Segment sayısı ve boyutu
GET /my-index/_stats/segments
// segments.count çok yüksekse (>50/shard) → merge sorunları

// 2. Translog boyutu
GET /my-index/_stats/translog
// uncommitted_size_in_bytes çok büyükse → flush sorunları

// 3. Merge istatistikleri
GET /my-index/_stats/merge
// total_throttled_time yüksekse → disk I/O darboğazı

// 4. Refresh istatistikleri
GET /my-index/_stats/refresh
// total_time çok yüksekse → çok sık refresh

// 5. Store boyutu
GET /my-index/_stats/store
// Beklenen boyuttan büyükse → deleted dokümanlar birikmiş

// 6. Tüm detaylar tek seferde
GET /my-index/_stats?level=shards

11. Özet

  • Apache Lucene, Elasticsearch'ün altındaki gerçek arama motorudur. Segment bazlı, immutable veri yapısı kullanır

  • Segment'ler değiştirilemez mini-index'lerdir. Güncelleme = delete + insert, gerçek silme ancak merge sırasında olur

  • Translog, in-memory buffer'daki veri kaybı riskini ortadan kaldırır. Her write hem buffer'a hem translog'a yazılır

  • Refresh (varsayılan 1s), buffer'ı aranabilir in-memory segment'e dönüştürür. Elasticsearch bu yüzden "near-realtime"dır — "realtime" değil

  • Flush, in-memory segment'leri disk'e yazar ve commit point oluşturur. Translog temizlenir

  • Merge, küçük segment'leri birleştirir ve deleted dokümanları fiziksel olarak temizler. TieredMergePolicy parametreleri performansı doğrudan etkiler

  • OS file cache, Elasticsearch performansının en kritik bileşenlerinden biridir. Heap'i %50'nin üzerine çıkarmayın — kalan RAM OS cache için kullanılmalıdır