← Kursa Dön
📄 Text · 30 min

Document Güncelleme — Update, Partial Update, Upsert

Update, Partial Update, Scripted Update, Upsert

Bir defterdeki yazıyı düzeltmeyi düşün. Kurşun kalemle yazdıysan silgisiyle silip yeniden yazarsın — ama kağıt hâlâ aynı. Tükenmez kalemle yazdıysan? O sayfayı yırtıp yenisine yazarsın. Elasticsearch ikinci yöntemi kullanır: dökümanlar immutable'dır — güncelleme aslında eski dökümanı silip yenisini yazmak demektir.

Ama endişelenme — bu "sil-yeniden yaz" işlemi perde arkasında olur ve sana şeffaf şekilde sunulur. Bu derste partial update, scripted update, upsert ve toplu güncelleme tekniklerini öğreneceğiz.


Güncelleme Nasıl Çalışır? — Perde Arkası

Elasticsearch'te güncelleme aslında üç adımlı bir işlemdir:

1. Mevcut dökümanı oku (_source'tan)
2. Değişiklikleri uygula (merge, script, replace)
3. Eski dökümanı "deleted" olarak işaretle + yeni dökümanı index'le

_version artar, _seq_no artar
Eski döküman segment merge sırasında fiziksel olarak temizlenir
         ┌─────────────────┐
Eski:    │ _version: 1     │ ← "deleted" işaretlenir
         │ name: "Laptop"  │    (merge'de temizlenecek)
         │ price: 15000    │
         └─────────────────┘
                 ↓ Güncelleme: price → 14000
         ┌─────────────────┐
Yeni:    │ _version: 2     │ ← Yeni döküman yazılır
         │ name: "Laptop"  │
         │ price: 14000    │
         └─────────────────┘

Bu mekanizmayı bilmek önemli çünkü:

  • Her güncelleme aslında bir tam yazma işlemi — performans maliyeti var

  • Sık güncellenen dökümanlar segment'lerde "deleted" döküman birikimine yol açar

  • _version her güncellemede artar


Yöntem 1: PUT ile Tam Değiştirme (Replace)

PUT ile aynı ID'ye yeni döküman gönderdiğinde, tüm döküman değişir:

// Mevcut döküman
GET /products/_doc/1
// {"name": "Laptop", "price": 15000, "category": "Elektronik", "in_stock": true}

// PUT ile güncelleme — TÜM DÖKÜMAN DEĞİŞİR
PUT /products/_doc/1
{
  "name": "Laptop Pro",
  "price": 17000
}

// Sonuç:
GET /products/_doc/1
// {"name": "Laptop Pro", "price": 17000}
// ⚠️ "category" ve "in_stock" alanları KAYBOLDU!

PUT, kısmi güncelleme yapmaz — tüm dökümanı replace eder. Bu genellikle istenen davranış değildir.


Yöntem 2: _update API ile Partial Update (Kısmi Güncelleme)

En yaygın güncelleme yöntemi. Sadece belirttiğin alanları günceller, geri kalanına dokunmaz:

// Mevcut döküman
// {"name": "Laptop", "price": 15000, "category": "Elektronik", "in_stock": true}

// Sadece fiyatı güncelle
POST /products/_update/1
{
  "doc": {
    "price": 14000
  }
}

// Yanıt:
{
  "_index": "products",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": { "total": 2, "successful": 2, "failed": 0 }
}

// Sonuç kontrol:
GET /products/_doc/1
// {"name": "Laptop", "price": 14000, "category": "Elektronik", "in_stock": true}
// ✅ Sadece price değişti, diğerleri aynen kaldı

Birden Fazla Alanı Güncelle

POST /products/_update/1
{
  "doc": {
    "price": 13500,
    "in_stock": false,
    "updated_at": "2025-01-20T10:00:00Z"
  }
}

Yeni Alan Ekle

// Mevcut dökümana yeni alan ekle
POST /products/_update/1
{
  "doc": {
    "discount_percentage": 10,
    "tags": ["indirim", "kampanya"]
  }
}
// Döküman artık discount_percentage ve tags alanlarını da içerir

İç İçe Obje Güncelleme

// Mevcut: {"specs": {"cpu": "i7", "ram": 16, "storage": 512}}

// İç objeyi güncelle
POST /products/_update/1
{
  "doc": {
    "specs": {
      "ram": 32
    }
  }
}

// ⚠️ DİKKAT: Bu tüm "specs" objesini replace eder!
// Sonuç: {"specs": {"ram": 32}}
// cpu ve storage KAYBOLDU!

// Doğru yaklaşım: Tüm objeyi gönder
POST /products/_update/1
{
  "doc": {
    "specs": {
      "cpu": "i7",
      "ram": 32,
      "storage": 512
    }
  }
}
// Veya scripted update kullan (aşağıda)

detect_noop — Gereksiz Güncellemeyi Önle

Eğer gönderdiğin değer mevcut değerle aynıysa, Elasticsearch güncelleme yapmaz:

// Mevcut fiyat: 14000
POST /products/_update/1
{
  "doc": {
    "price": 14000    // Aynı değer
  }
}

// Yanıt:
{
  "result": "noop"    // Hiçbir şey değişmedi — gereksiz yazma yapılmadı
}

// detect_noop'u kapat (her zaman yaz)
POST /products/_update/1
{
  "doc": {
    "price": 14000
  },
  "detect_noop": false    // Aynı olsa bile yaz (_version artar)
}

Yöntem 3: Scripted Update

Basit alan güncellemenin ötesinde — hesaplama, koşul, array manipülasyonu gerektiren güncellemeler için Painless script kullanılır.

Painless Script Nedir?

Elasticsearch'ün yerleşik scripting dili. Java'ya benzer syntax, güvenli ve hızlı.

Temel Scripted Update

// Fiyatı %10 artır
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.price *= 1.10"
  }
}

// Fiyattan 500 TL düş
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.price -= 500"
  }
}

Parametreli Script

// Parametreli script (güvenli ve performanslı — script derleme cache'lenir)
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.price -= params.discount",
    "params": {
      "discount": 1000
    }
  }
}

// Stok sayısını güncelle
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.stock_count -= params.quantity",
    "params": {
      "quantity": 2
    }
  }
}

💡 İpucu: Script'lerde hardcoded değer yerine params kullan. Elasticsearch script'leri derler ve cache'ler. Parametre değişse bile aynı derleme kullanılır — parametre inline olursa her farklı değer yeni derleme gerektirir.

Koşullu Güncelleme

// Stok sıfırsa "in_stock" false yap
POST /products/_update/1
{
  "script": {
    "source": """
      if (ctx._source.stock_count <= 0) {
        ctx._source.in_stock = false;
        ctx._source.stock_count = 0;
      }
    """
  }
}

// Fiyat belirli bir değerin altına düşmesin
POST /products/_update/1
{
  "script": {
    "source": """
      ctx._source.price -= params.discount;
      if (ctx._source.price < params.min_price) {
        ctx._source.price = params.min_price;
      }
    """,
    "params": {
      "discount": 5000,
      "min_price": 1000
    }
  }
}

Array Manipülasyonu

// Array'e eleman ekle
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.tags.add(params.tag)",
    "params": { "tag": "yeni-sezon" }
  }
}

// Array'den eleman çıkar
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.tags.remove(ctx._source.tags.indexOf(params.tag))",
    "params": { "tag": "eski-sezon" }
  }
}

// Array'de eleman yoksa ekle (duplicate engelleme)
POST /products/_update/1
{
  "script": {
    "source": """
      if (!ctx._source.tags.contains(params.tag)) {
        ctx._source.tags.add(params.tag);
      }
    """,
    "params": { "tag": "premium" }
  }
}

// Birden fazla eleman ekle
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.tags.addAll(params.new_tags)",
    "params": {
      "new_tags": ["2025", "yeni-koleksiyon"]
    }
  }
}

İleri Script Örnekleri

// Tarih alanını güncelle
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.updated_at = params.now",
    "params": {
      "now": "2025-01-20T15:30:00Z"
    }
  }
}

// Yeni bir iç obje alanı ekle
POST /products/_update/1
{
  "script": {
    "source": """
      if (ctx._source.containsKey('stats') == false) {
        ctx._source.stats = new HashMap();
      }
      ctx._source.stats.view_count = (ctx._source.stats.view_count ?: 0) + 1;
    """
  }
}

// Koşula göre güncelle veya silme
POST /products/_update/1
{
  "script": {
    "source": """
      if (ctx._source.stock_count <= 0) {
        ctx.op = 'delete';   // Dökümanı sil!
      } else {
        ctx.op = 'none';     // Hiçbir şey yapma (noop)
      }
    """
  }
}
// ctx.op seçenekleri: 'index' (güncelle), 'delete' (sil), 'none' (noop)

Yöntem 4: Upsert — Varsa Güncelle, Yoksa Oluştur

Çok yaygın bir pattern: döküman varsa güncelle, yoksa yenisini oluştur.

doc_as_upsert

// Döküman varsa güncelle, yoksa bu dökümanı oluştur
POST /products/_update/1
{
  "doc": {
    "name": "Laptop",
    "price": 15000,
    "category": "Elektronik"
  },
  "doc_as_upsert": true
}

// ID=1 yoksa: Yeni döküman oluşturulur (result: "created")
// ID=1 varsa: Mevcut döküman güncellenir (result: "updated")

upsert + script

// Varsa: Script çalıştır (view_count artır)
// Yoksa: upsert dökümanını oluştur
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.view_count += params.increment",
    "params": { "increment": 1 }
  },
  "upsert": {
    "name": "Yeni Ürün",
    "price": 0,
    "view_count": 1
  }
}

// İlk çağrı (döküman yok): upsert dökümanı oluşturulur (view_count: 1)
// Sonraki çağrılar: view_count her seferinde 1 artar

scripted_upsert

// Her durumda (var/yok) script çalıştır
POST /products/_update/1
{
  "scripted_upsert": true,
  "script": {
    "source": """
      if (ctx.op == 'create') {
        ctx._source.name = params.name;
        ctx._source.view_count = 1;
      } else {
        ctx._source.view_count += 1;
      }
    """,
    "params": { "name": "Dinamik Ürün" }
  },
  "upsert": {}
}

Update By Query — Toplu Güncelleme

Belirli kriterlere uyan tüm dökümanları güncelle:

// Tüm "Elektronik" kategorisindeki ürünlere %10 indirim
POST /products/_update_by_query
{
  "query": {
    "term": { "category.keyword": "Elektronik" }
  },
  "script": {
    "source": "ctx._source.price = (long)(ctx._source.price * 0.90)",
    "lang": "painless"
  }
}

// Yanıt:
{
  "took": 150,
  "timed_out": false,
  "total": 250,           // Toplam eşleşen
  "updated": 250,         // Güncellenen
  "deleted": 0,
  "batches": 1,
  "version_conflicts": 0, // Çakışma
  "noops": 0,
  "failures": []
}

Update By Query — Pratik Örnekler

// Stokta olmayan ürünleri işaretle
POST /products/_update_by_query
{
  "query": {
    "term": { "in_stock": false }
  },
  "script": {
    "source": """
      ctx._source.status = 'UNAVAILABLE';
      ctx._source.updated_at = params.now;
    """,
    "params": {
      "now": "2025-01-20T10:00:00Z"
    }
  }
}

// Eski ürünlere "archive" tag'i ekle
POST /products/_update_by_query
{
  "query": {
    "range": {
      "created_at": {
        "lt": "2023-01-01"
      }
    }
  },
  "script": {
    "source": """
      if (ctx._source.tags == null) {
        ctx._source.tags = new ArrayList();
      }
      if (!ctx._source.tags.contains('archived')) {
        ctx._source.tags.add('archived');
      }
    """
  }
}

Update By Query — Parametreler

// Sadece ilk 100 dökümanı güncelle
POST /products/_update_by_query?max_docs=100
{
  "query": { "match_all": {} },
  "script": { "source": "ctx._source.batch = 1" }
}

// Çakışmalarda devam et (proceed) veya dur (abort)
POST /products/_update_by_query?conflicts=proceed
{
  "query": { "match_all": {} },
  "script": { "source": "ctx._source.migrated = true" }
}

// Asenkron çalıştır (büyük veri setleri için)
POST /products/_update_by_query?wait_for_completion=false
{
  "query": { "match_all": {} },
  "script": { "source": "ctx._source.reprocessed = true" }
}

// Yanıt: {"task": "node-1:12345"}
// Task durumunu kontrol et:
GET /_tasks/node-1:12345

Update By Query — Slicing (Paralel İşleme)

// 5 dilime böl — paralel güncelleme
POST /products/_update_by_query?slices=5
{
  "query": { "match_all": {} },
  "script": { "source": "ctx._source.version = 2" }
}
// 5 paralel iş parçası oluşturulur — büyük veri setlerinde çok daha hızlı

// Otomatik dilim sayısı (shard sayısı kadar)
POST /products/_update_by_query?slices=auto
{
  "query": { "match_all": {} },
  "script": { "source": "ctx._source.processed = true" }
}

Retry on Conflict

Concurrent güncelleme durumlarında çakışma olabilir:

// _update API'de retry
POST /products/_update/1?retry_on_conflict=3
{
  "doc": {
    "price": 14000
  }
}
// Çakışma olursa 3 kez daha dener

// _update_by_query'de
POST /products/_update_by_query?conflicts=proceed
{
  "query": { "match_all": {} },
  "script": { "source": "ctx._source.count += 1" }
}
// Çakışan dökümanları atlayarak devam eder

Java ile Document Güncelleme

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch._types.Script;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;

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())
        );

        // 1. Partial Update
        UpdateResponse<Map> updateResponse = client.update(u -> u
            .index("products")
            .id("1")
            .doc(Map.of("price", 14000, "in_stock", false)),
            Map.class
        );
        System.out.println("Result: " + updateResponse.result());

        // 2. Scripted Update
        UpdateResponse<Map> scriptResponse = client.update(u -> u
            .index("products")
            .id("1")
            .script(s -> s
                .inline(i -> i
                    .source("ctx._source.price -= params.discount")
                    .params("discount", JsonData.of(500))
                )
            ),
            Map.class
        );
        System.out.println("Scripted: " + scriptResponse.result());

        // 3. Upsert
        UpdateResponse<Map> upsertResponse = client.update(u -> u
            .index("products")
            .id("999")
            .doc(Map.of("view_count", 1))
            .upsert(Map.of(
                "name", "Yeni Ürün",
                "price", 0,
                "view_count", 1
            )),
            Map.class
        );
        System.out.println("Upsert: " + upsertResponse.result());
        // İlk çağrı: "created", sonraki: "updated"

        // 4. Retry on conflict
        UpdateResponse<Map> retryResponse = client.update(u -> u
            .index("products")
            .id("1")
            .doc(Map.of("price", 13500))
            .retryOnConflict(3),
            Map.class
        );

        // 5. Update by query
        var ubqResponse = client.updateByQuery(u -> u
            .index("products")
            .query(q -> q
                .term(t -> t
                    .field("category.keyword")
                    .value("Elektronik")
                )
            )
            .script(s -> s
                .inline(i -> i
                    .source("ctx._source.sale = true")
                )
            )
        );
        System.out.println("Updated: " + ubqResponse.updated() + " docs");

        restClient.close();
    }
}

Best Practices

Partial update kullan — PUT ile replace yerine _update API ile kısmi güncelleme

Script'lerde `params` kullan — Hardcoded değer yerine parametreli script (cache dostu)

`retry_on_conflict` ekle — Concurrent güncelleme senaryolarında

Upsert pattern'ı kullan — Varsa güncelle yoksa oluştur — doc_as_upsert: true

Büyük toplu güncellemelerde `slices` kullan — Paralel işleme ile hızlandır

`detect_noop: true` varsayılanı koru — Gereksiz yazma işlemlerini önler


Gerçek Dünya Senaryosu: E-Ticaret Fiyat Güncelleme

Bir e-ticaret platformunda toplu fiyat güncelleme senaryosu:

// Senaryo: Yeni yıl kampanyası — tüm elektronik ürünlere %15 indirim
// Ama minimum fiyat 500 TL'nin altına düşmemeli

// 1. Kaç ürün etkilenecek kontrol et
GET /products/_count
{
  "query": {
    "bool": {
      "must": [
        { "term": { "category.keyword": "Elektronik" } },
        { "term": { "in_stock": true } }
      ]
    }
  }
}

// 2. Toplu güncelleme — slices ile paralel
POST /products/_update_by_query?slices=auto&conflicts=proceed
{
  "query": {
    "bool": {
      "must": [
        { "term": { "category.keyword": "Elektronik" } },
        { "term": { "in_stock": true } }
      ]
    }
  },
  "script": {
    "source": """
      // Orijinal fiyatı sakla
      if (!ctx._source.containsKey('original_price')) {
        ctx._source.original_price = ctx._source.price;
      }
      
      // %15 indirim uygula
      def discounted = (long)(ctx._source.original_price * (1 - params.discount_rate));
      
      // Minimum fiyat kontrolü
      ctx._source.price = Math.max(discounted, params.min_price);
      
      // Kampanya bilgisi ekle
      ctx._source.campaign = params.campaign_name;
      ctx._source.campaign_end = params.campaign_end;
    """,
    "params": {
      "discount_rate": 0.15,
      "min_price": 500,
      "campaign_name": "yeni-yil-2025",
      "campaign_end": "2025-01-31T23:59:59Z"
    }
  }
}

// 3. Kampanya bitince fiyatları geri al
POST /products/_update_by_query
{
  "query": {
    "term": { "campaign": "yeni-yil-2025" }
  },
  "script": {
    "source": """
      if (ctx._source.containsKey('original_price')) {
        ctx._source.price = ctx._source.original_price;
        ctx._source.remove('original_price');
        ctx._source.remove('campaign');
        ctx._source.remove('campaign_end');
      }
    """
  }
}

Yaygın Hatalar

❌ "PUT ile partial update yapıyorum"

PUT tüm dökümanı replace eder. Sadece fiyatı güncellemek isterken diğer alanları kaybedersin. POST /_update kullan.

❌ "İç objeyi partial update ile güncelliyorum"

doc ile iç obje güncelleme, o objenin tamamını replace eder. Script kullan veya tüm objeyi gönder.

❌ "Script'te inline değer kullanıyorum"

// ❌ Her farklı değer yeni derleme gerektirir
"source": "ctx._source.price -= 500"
"source": "ctx._source.price -= 1000"

// ✅ Aynı script, farklı parametre — cache'ten çalışır
"source": "ctx._source.price -= params.discount"

❌ "update_by_query sırasında index'e yazma devam ediyor"

update_by_query, çalışırken snapshot alır. Arada yeni dökümanlar eklenirse bunlar güncellenmez. İdeal olarak yazma trafiğini durdur veya conflicts=proceed kullan.

❌ "Çok sık güncelleme yapıyorum"

Her güncelleme = sil + yeniden yaz. Saniyede yüzlerce güncelleme yapıyorsan segment'lerde "deleted" doküman birikir, merge yükü artar. Mümkünse güncellemeleri batch'le.


Özet

  • Elasticsearch'te güncelleme aslında sil + yeniden yaz işlemidir — dökümanlar immutable

  • PUT tüm dökümanı replace eder, _update ile partial update yapılır

  • Scripted update ile hesaplama, koşul ve array manipülasyonu yapılabilir

  • Upsert (doc_as_upsert) ile varsa güncelle, yoksa oluştur pattern'ı

  • update_by_query ile sorguya uyan tüm dökümanları toplu güncelle

  • detect_noop gereksiz yazmaları önler — aynı değeri gönderirsen "noop" döner

  • retry_on_conflict concurrent güncelleme çakışmalarını handle eder

  • Script'lerde params kullanmak performans için kritik — script derleme cache'lenir

Bir sonraki derste Document Silme işlemlerini öğreneceğiz — DELETE, Delete by Query ve Bulk API!