Elasticsearch ile Temel CRUD İşlemleri: REST API Kullanımı

Elasticsearch’ü production ortamına aldığında ya da sadece denemek için local’de ayağa kaldırdığında, ilk yapman gereken şey REST API üzerinden nasıl veri yazıp okuyacağını öğrenmek. Zira Elasticsearch’ün tüm gücü bu HTTP tabanlı API’dan geliyor. SQL’e alışkın birine başta tuhaf gelebilir ama birkaç istek gönderip cevapları görünce “aslında çok mantıklı” diyeceksin.

Bu yazıda gerçek bir senaryo üzerinden gideceğiz: Bir e-ticaret sitesinin ürün kataloğunu Elasticsearch’e aktarıyoruz. Ürün ekleme, güncelleme, silme ve sorgu işlemlerini hem teorik hem pratik açıdan ele alacağız.

Elasticsearch REST API Temelleri

Elasticsearch ile konuşmak için standart HTTP metodlarını kullanıyorsun. Yani özel bir driver ya da kütüphane olmadan sadece curl ile her şeyi yapabilirsin. Bu aslında çok büyük bir avantaj, özellikle debug yaparken.

Temel HTTP metodları ve karşılıkları:

  • GET: Veri okuma
  • POST: Yeni kayıt oluşturma ya da arama
  • PUT: Kayıt oluşturma veya tamamen güncelleme
  • DELETE: Kayıt ya da index silme

Elasticsearch default olarak 9200 portunda çalışır ve her istek şu pattern’ı takip eder:

http://host:9200/{index}/{endpoint}

Eski versiyonlarda _type kavramı da vardı (/index/type/id) ama Elasticsearch 7.x ile birlikte type’lar deprecated oldu. 8.x’te tamamen kalktı. Bunu bilmek önemli çünkü internette eski dokümanlar hala type kullanıyor ve karışıklık yaratıyor.

Bağlantının çalışıp çalışmadığını test etmek için:

curl -X GET "http://localhost:9200/"

Eğer TLS aktifse (Elasticsearch 8.x default olarak TLS açık gelir):

curl -X GET "https://localhost:9200/" 
  --cacert /etc/elasticsearch/certs/http_ca.crt 
  -u elastic:SifreninBuraya

Cluster’ın sağlık durumunu kontrol etmek için de şunu kullanabilirsin:

curl -X GET "http://localhost:9200/_cluster/health?pretty"

?pretty parametresi JSON çıktısını okunabilir formatta gösterir. Development sırasında her zaman ekle, production script’lerinde ise çıkar.

Index Oluşturma

Elasticsearch’te veriler “index” adı verilen yapılarda tutulur. SQL dünyasından geliyorsan index’i bir veritabanı gibi düşünebilirsin ama tamamen aynı değil. Index içinde her veri birimi “document” olarak geçiyor.

Bir index’i mapping tanımlamadan da oluşturabilirsin, Elasticsearch kendisi otomatik algılar. Ama production’da bu kötü bir fikir. Field type’larını sen belirlemezsen, Elasticsearch tahmin eder ve çoğu zaman yanlış tahmin eder. Örneğin fiyat alanını text olarak indexleyebilir, sonra nümerik sorgular çalışmaz.

Ürün kataloğu için bir index oluşturalım:

curl -X PUT "http://localhost:9200/urunler" 
  -H "Content-Type: application/json" 
  -d '{
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    },
    "mappings": {
      "properties": {
        "urun_adi": {
          "type": "text",
          "analyzer": "standard"
        },
        "kategori": {
          "type": "keyword"
        },
        "fiyat": {
          "type": "float"
        },
        "stok": {
          "type": "integer"
        },
        "aktif": {
          "type": "boolean"
        },
        "eklenme_tarihi": {
          "type": "date",
          "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
        }
      }
    }
  }'

Burada dikkat edilmesi gereken birkaç nokta var:

  • text: Full-text arama için kullanılır, analiz edilir ve tokenize edilir. “gaming mouse” aratıldığında “gaming” ve “mouse” ayrı ayrı eşleşir.
  • keyword: Tam eşleşme için kullanılır. Filtreleme, sıralama ve aggregation’larda kullanman gereken type bu.
  • number_of_replicas: 0: Bu sadece single-node kurulumlar için geçerli. Replica shard başka bir node’a yerleştirilmesi gerektiğinden, tek node’lu cluster’da replica tutarsan index “yellow” status’ta kalır.

Create: Yeni Döküman Ekleme

ID Belirleyerek Ekleme

Kendi ID’ni belirlemek istiyorsan PUT metodunu kullanırsın. E-ticaret senaryomuzda ürün ID’leri zaten var, o yüzden bunları korumak mantıklı:

curl -X PUT "http://localhost:9200/urunler/_doc/1001" 
  -H "Content-Type: application/json" 
  -d '{
    "urun_adi": "Logitech MX Master 3S Kablosuz Mouse",
    "kategori": "Bilgisayar Çevre Birimleri",
    "fiyat": 1899.99,
    "stok": 45,
    "aktif": true,
    "eklenme_tarihi": "2024-01-15"
  }'

Başarılı yanıt şuna benzeyecek:

{
  "_index": "urunler",
  "_id": "1001",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  }
}

result: created görürsen yeni kayıt oluşturulmuş demek. Eğer aynı ID’ye tekrar PUT atarsan result: updated gelir ve _version artar. Bu davranış bazen istemediğin bir şey olabilir. Eğer “sadece yeni kayıt oluştur, varsa hata ver” istiyorsan _create endpoint’ini kullan:

curl -X PUT "http://localhost:9200/urunler/_create/1001" 
  -H "Content-Type: application/json" 
  -d '{
    "urun_adi": "Logitech MX Master 3S Kablosuz Mouse",
    "fiyat": 1899.99
  }'

Bu ürün zaten varsa 409 Conflict hatası döner.

Otomatik ID ile Ekleme

ID’yi Elasticsearch’ün üretmesini istiyorsan POST kullanırsın. Log verileri ya da event stream’ler için bu yaklaşım daha pratik:

curl -X POST "http://localhost:9200/urunler/_doc" 
  -H "Content-Type: application/json" 
  -d '{
    "urun_adi": "Samsung 27 inç Curved Monitor",
    "kategori": "Monitörler",
    "fiyat": 4299.00,
    "stok": 12,
    "aktif": true,
    "eklenme_tarihi": "2024-01-20"
  }'

Elasticsearch _id alanına UUID benzeri bir string atar. Bu ID’yi response’tan alıp saklamalısın, sonradan ihtiyacın olacak.

Read: Veri Okuma

ID ile Tek Döküman Getirme

En basit okuma işlemi, bilinen bir ID ile dökümanı direkt çekmek:

curl -X GET "http://localhost:9200/urunler/_doc/1001?pretty"

Sadece dökümanın kendisini, metadata olmadan istiyorsan _source endpoint’ini kullan:

curl -X GET "http://localhost:9200/urunler/_source/1001?pretty"

Büyük dökümanlardan sadece belirli alanları çekmek istiyorsan bant genişliği açısından _source_includes parametresi hayat kurtarır:

curl -X GET "http://localhost:9200/urunler/_doc/1001?_source_includes=urun_adi,fiyat&pretty"

Search API ile Sorgulama

Elasticsearch’ün gerçek gücü burada ortaya çıkıyor. _search endpoint’i son derece kapsamlı bir query DSL sunuyor.

Tüm ürünleri listele (default olarak ilk 10 gelir):

curl -X GET "http://localhost:9200/urunler/_search?pretty" 
  -H "Content-Type: application/json" 
  -d '{
    "query": {
      "match_all": {}
    },
    "size": 20,
    "sort": [
      {"fiyat": {"order": "asc"}}
    ]
  }'

Belirli bir kategorideki aktif ürünleri fiyat aralığıyla filtrele:

curl -X GET "http://localhost:9200/urunler/_search?pretty" 
  -H "Content-Type: application/json" 
  -d '{
    "query": {
      "bool": {
        "must": [
          {"term": {"aktif": true}},
          {"term": {"kategori": "Bilgisayar Çevre Birimleri"}}
        ],
        "filter": [
          {
            "range": {
              "fiyat": {
                "gte": 500,
                "lte": 3000
              }
            }
          }
        ]
      }
    }
  }'

Burada must içindeki sorgular relevance score’u etkilerken, filter içindekiler sadece filtreler ve score’u etkilemez. Performans açısından önemli bir fark bu. Eğer sıralama için score’a ihtiyacın yoksa mümkün olduğunca filter kullan, cache’lenir ve çok daha hızlı çalışır.

Ürün adında tam metin araması yapmak için:

curl -X GET "http://localhost:9200/urunler/_search?pretty" 
  -H "Content-Type: application/json" 
  -d '{
    "query": {
      "match": {
        "urun_adi": {
          "query": "kablosuz mouse",
          "operator": "and"
        }
      }
    }
  }'

operator: and demek “kablosuz” VE “mouse” kelimelerinin ikisi de geçmeli demek. Default olarak or gelir.

Update: Döküman Güncelleme

Kısmi Güncelleme

Bir dökümanı tamamen değiştirmeden sadece belirli alanları güncellemek için _update endpoint’ini kullan. Bu en sık kullanılan güncelleme yöntemi:

curl -X POST "http://localhost:9200/urunler/_update/1001" 
  -H "Content-Type: application/json" 
  -d '{
    "doc": {
      "fiyat": 1749.99,
      "stok": 38
    }
  }'

Sadece belirttiğin alanlar güncellenir, diğerleri dokunulmaz. Eğer döküman yoksa hata verir. “Yoksa oluştur, varsa güncelle” davranışı istiyorsan upsert kullan:

curl -X POST "http://localhost:9200/urunler/_update/2005" 
  -H "Content-Type: application/json" 
  -d '{
    "doc": {
      "stok": 25
    },
    "upsert": {
      "urun_adi": "Yeni Ürün",
      "kategori": "Genel",
      "fiyat": 999.99,
      "stok": 25,
      "aktif": true,
      "eklenme_tarihi": "2024-01-25"
    }
  }'

Script ile Güncelleme

Stok gibi numerik değerleri artırmak ya da azaltmak için Painless script kullanabilirsin. Bu özellikle race condition sorunlarını önlemek açısından değerli, çünkü okuma-değiştirme-yazma yerine atomic bir işlem oluyor:

curl -X POST "http://localhost:9200/urunler/_update/1001" 
  -H "Content-Type: application/json" 
  -d '{
    "script": {
      "source": "ctx._source.stok -= params.miktar",
      "lang": "painless",
      "params": {
        "miktar": 3
      }
    }
  }'

Sepetten ürün çıkarıldığında stok güncellemesi için bu yaklaşım production’da çok işe yarar. Script içinde koşul da ekleyebilirsin:

curl -X POST "http://localhost:9200/urunler/_update/1001" 
  -H "Content-Type: application/json" 
  -d '{
    "script": {
      "source": "if (ctx._source.stok >= params.miktar) { ctx._source.stok -= params.miktar } else { ctx.op = "noop" }",
      "lang": "painless",
      "params": {
        "miktar": 5
      }
    }
  }'

Stok yetersizse işlemi hiç yapma (noop) şeklinde çalışır.

Query ile Toplu Güncelleme

Tek tek güncellemek yerine bir sorgu sonucundaki tüm dökümanları güncellemek gerektiğinde _update_by_query kullan. Örneğin belirli bir kategorideki tüm ürünleri aktif duruma çekelim:

curl -X POST "http://localhost:9200/urunler/_update_by_query" 
  -H "Content-Type: application/json" 
  -d '{
    "query": {
      "term": {
        "kategori": "Monitörler"
      }
    },
    "script": {
      "source": "ctx._source.aktif = true",
      "lang": "painless"
    }
  }'

Bu işlem büyük dataset’lerde yavaş olabilir çünkü her dökümanı ayrı ayrı günceller. Milyonlarca kayıt üzerinde çalışacaksan wait_for_completion=false ekleyerek async çalıştır ve task API’dan durumu takip et.

Delete: Döküman Silme

Tek Döküman Silme

curl -X DELETE "http://localhost:9200/urunler/_doc/1001"

Yanıt olarak result: deleted görürsen işlem başarılı. Olmayan bir ID’yi silmeye çalışırsan result: not_found gelir ve HTTP 404 döner. Bunu script’lerde kontrol etmeden geçme.

Query ile Toplu Silme

Belirli kriterlere uyan tüm dökümanları sil. Örneğin stoğu tükenmiş ve 6 aydan eski ürünleri temizleyelim:

curl -X POST "http://localhost:9200/urunler/_delete_by_query" 
  -H "Content-Type: application/json" 
  -d '{
    "query": {
      "bool": {
        "must": [
          {"term": {"aktif": false}},
          {
            "range": {
              "eklenme_tarihi": {
                "lt": "2023-07-01"
              }
            }
          }
        ]
      }
    }
  }'

Index Silme

Tüm index’i silmek için:

curl -X DELETE "http://localhost:9200/urunler"

Bu işlem geri alınamaz. Production’da yaparken iki kez düşün. Index’i silmeden önce döküman sayısını kontrol etmek iyi bir alışkanlık:

curl -X GET "http://localhost:9200/urunler/_count?pretty"

Bulk API ile Toplu İşlemler

Yüzlerce ya da binlerce kayıt üzerinde tek tek istek atmak hem yavaş hem de verimsiz. Bulk API ile tek HTTP isteğinde çok sayıda CREATE/UPDATE/DELETE işlemi yapabilirsin. Veri migrasyonu veya toplu import senaryolarında olmazsa olmaz.

curl -X POST "http://localhost:9200/_bulk" 
  -H "Content-Type: application/x-ndjson" 
  --data-binary @- << 'EOF'
{"index": {"_index": "urunler", "_id": "2001"}}
{"urun_adi": "Corsair K70 Mekanik Klavye", "kategori": "Bilgisayar Çevre Birimleri", "fiyat": 2499.00, "stok": 20, "aktif": true, "eklenme_tarihi": "2024-01-22"}
{"index": {"_index": "urunler", "_id": "2002"}}
{"urun_adi": "HyperX Cloud II Kulaklık", "kategori": "Ses Sistemleri", "fiyat": 1199.00, "stok": 35, "aktif": true, "eklenme_tarihi": "2024-01-22"}
{"update": {"_index": "urunler", "_id": "1001"}}
{"doc": {"stok": 50}}
{"delete": {"_index": "urunler", "_id": "9999"}}
EOF

Bulk API’nın formatı biraz farklı: Her işlem için önce action metadata satırı, sonra data satırı geliyor. Delete için data satırı yok. Content-Type olarak application/x-ndjson kullanman gerekiyor.

Bulk response’u incelemek önemli çünkü bazı işlemler başarılı olurken bazıları hata verebilir. Response içindeki errors: true alanını kontrol et:

curl -X POST "http://localhost:9200/_bulk?filter_path=errors,items.*.error" 
  -H "Content-Type: application/x-ndjson" 
  --data-binary @bulk_data.ndjson

filter_path ile sadece hata bilgilerini çekerek gereksiz response boyutunu azaltabilirsin.

Gerçek Dünya Senaryosu: Ürün Arama Servisi

Bir e-ticaret sitesinde arama kutusuna yazılan metni birden fazla alanda aynı anda aramak yaygın bir ihtiyaç. multi_match query bunun için biçilmiş kaftan:

curl -X GET "http://localhost:9200/urunler/_search?pretty" 
  -H "Content-Type: application/json" 
  -d '{
    "query": {
      "bool": {
        "must": [
          {
            "multi_match": {
              "query": "mekanik klavye",
              "fields": ["urun_adi^3", "kategori^1"],
              "type": "best_fields",
              "fuzziness": "AUTO"
            }
          }
        ],
        "filter": [
          {"term": {"aktif": true}},
          {"range": {"stok": {"gt": 0}}}
        ]
      }
    },
    "highlight": {
      "fields": {
        "urun_adi": {}
      }
    },
    "from": 0,
    "size": 10
  }'

Bu sorguda birkaç önemli detay var:

  • fields içindeki ^3: urun_adi alanındaki eşleşmelere 3 kat daha fazla ağırlık ver demek. Böylece ürün adında geçen terimler kategoride geçenlerden daha önemli sayılır.
  • fuzziness: AUTO: Yazım hatalarını tolere eder. “klavye” yerine “klavey” yazan kullanıcılar da sonuç görebilir.
  • highlight: Sonuçlarda eşleşen kelimeleri HTML tag’ları içinde işaretler, arama sonucu sayfasında kalın göstermek için kullanılır.
  • from/size: Pagination için. from: 10, size: 10 dersen ikinci sayfayı alırsın.

Index ve Döküman Durumunu İzleme

Günlük operasyonlarda bazı bilgilere sık sık ihtiyaç duyarsın:

# Index istatistikleri
curl -X GET "http://localhost:9200/urunler/_stats?pretty"

# Index mapping'ini görüntüle
curl -X GET "http://localhost:9200/urunler/_mapping?pretty"

# Var olan tüm index'leri listele
curl -X GET "http://localhost:9200/_cat/indices?v"

# Index'in ayarlarını görüntüle
curl -X GET "http://localhost:9200/urunler/_settings?pretty"

_cat API’ı human-readable tablo çıktısı verir ve terminalde hızlı kontrol için çok kullanışlı. ?v parametresi sütun başlıklarını gösterir.

Sonuç

Elasticsearch ile CRUD işlemleri başta karmaşık görünebilir, özellikle SQL’e alışkınsan. Ama temel pattern’ları kavradığında şunu fark edeceksin: Her şey HTTP ve JSON. Bu basitlik aynı zamanda büyük bir güç, çünkü herhangi bir dilden, herhangi bir araçtan Elasticsearch ile konuşabiliyorsun.

Özetlemek gerekirse:

  • Index oluştururken mapping’i her zaman kendin tanımla, otomatik algılamaya bırakma.
  • Numeric alanları güncellerken Painless script kullan, race condition sorunlarından korunursun.
  • Toplu işlemlerde Bulk API‘dan kaçınma, performans farkı çok büyük.
  • Sorgu performansı için filtering ile scoring’i birbirinden ayır: Relevance önemliyse must, sadece filtre uygulayacaksan filter kullan.
  • Production’da ?pretty parametresini kaldır, gereksiz parse yükü oluşturur.

Bir sonraki adım olarak aggregation’lara bakmanı tavsiye ederim. Örneğin kategori bazında ortalama fiyat, en çok satılan ürünler gibi analizler için SQL’deki GROUP BY’ın çok daha güçlü versiyonu diyebileceğimiz aggregation’lar devreye giriyor. Ama bu konuyu başka bir yazıya bırakalım.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir