Elasticsearch’te Arama Sorguları: Query DSL Rehberi

Elasticsearch ile ciddi iş yapmaya başladığınızda, basit arama kutucuklarının ötesine geçmeniz kaçınılmaz oluyor. İşte tam bu noktada Query DSL devreye giriyor. JSON tabanlı bu sorgu dili, arama işlemlerini inanılmaz ölçüde esnek ve güçlü kılıyor. Ama aynı zamanda öğrenme eğrisi de bir o kadar dik. Bu yazıda, gerçek dünya senaryoları üzerinden Query DSL’i baştan sona ele alacağız.

Query DSL Nedir ve Neden Önemlidir

Elasticsearch, verilerinizi aramak için HTTP üzerinden JSON formatında sorgular kabul eder. Bu yapıya Query DSL (Domain Specific Language) deniyor. SQL’e alışkın olanlar için biraz alışılmadık gelebilir, ancak bir kez kavradığınızda SQL’in yapamayacağı şeyleri rahatlıkla yapabildiğinizi görürsünüz.

Query DSL’in iki temel bileşeni vardır:

  • Query context: Dokümanların ne kadar alakalı olduğunu hesaplar, _score değeri üretir
  • Filter context: Dokümanların kriterlere uyup uymadığını kontrol eder, skor hesaplamaz, önbelleğe alınır

Bu ayrım performans açısından kritik. Eğer sadece “evet/hayır” şeklinde filtreleme yapıyorsanız, filter context kullanmak çok daha hızlı sonuç verir çünkü Elasticsearch bu sonuçları önbelleğe alır.

Temel Sorgu Yapısı

Her şey GET index_adi/_search ile başlar. En basit haliyle bir arama şöyle görünür:

GET /urunler/_search
{
  "query": {
    "match_all": {}
  }
}

Bu sorgu, indeksteki tüm dokümanları döndürür. Ama biz daha spesifik sorgular yazmak istiyoruz.

Match Sorguları

match

En çok kullanılan sorgu türü. Tek bir alana karşı tam metin araması yapar:

GET /urunler/_search
{
  "query": {
    "match": {
      "urun_adi": {
        "query": "kablosuz kulaklık",
        "operator": "and"
      }
    }
  }
}

Burada operator: and kullanmak, her iki kelimenin de dokümanda geçmesini zorunlu kılar. Varsayılan or operatörüyle sadece birinin geçmesi yeterlidir. Bir e-ticaret sisteminde çalışıyorsanız ve kullanıcı “kablosuz kulaklık” arıyorsa, büyük ihtimalle her iki kelimeyi de içeren ürünleri göstermek istersiniz.

multi_match

Birden fazla alana aynı anda arama yapmak için kullanılır:

GET /makaleler/_search
{
  "query": {
    "multi_match": {
      "query": "linux sunucu yönetimi",
      "fields": ["baslik^3", "icerik", "etiketler^2"],
      "type": "best_fields"
    }
  }
}

^3 notasyonu, o alanın ağırlığını artırır. Başlıkta bulunan bir eşleşme, içerikte bulunana göre 3 kat daha önemli sayılır. Bu, arama sonuçlarının kalitesini ciddi ölçüde artırır.

multi_match türleri:

  • best_fields: En iyi eşleşen alanın skorunu alır, varsayılan
  • most_fields: Eşleşen tüm alanların skorlarını toplar
  • cross_fields: Alanları tek bir büyük alan gibi değerlendirir
  • phrase: Her alanda tam ifade araması yapar

Term Level Sorguları

Term sorguları analiz edilmeden çalışır. Yani büyük/küçük harf duyarlıdır ve tam eşleşme arar. Sayısal değerler, tarihler ve keyword alanları için idealdir.

term ve terms

GET /siparisler/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "durum": "tamamlandi"
          }
        },
        {
          "terms": {
            "kategori_id": [1, 5, 12, 34]
          }
        }
      ]
    }
  }
}

Bu sorguda bool + filter kombinasyonu kullandım. Çünkü sipariş durumu ve kategori ID değerleri için skor hesaplamasına gerek yok, sadece filtreleme yapıyoruz. Bu şekilde Elasticsearch bu filtreyi önbelleğe alır ve tekrarlayan sorgularda çok daha hızlı yanıt verir.

range

Tarih aralığı sorguları ve sayısal aralık filtrelemesi için vazgeçilmez:

GET /log_kayitlari/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "seviye": "ERROR"
          }
        }
      ],
      "filter": [
        {
          "range": {
            "zaman_damgasi": {
              "gte": "2024-01-01T00:00:00",
              "lte": "2024-01-31T23:59:59",
              "format": "yyyy-MM-dd'T'HH:mm:ss"
            }
          }
        },
        {
          "range": {
            "yanit_suresi_ms": {
              "gte": 1000
            }
          }
        }
      ]
    }
  }
}

Bu sorgu, Ocak 2024 boyunca 1 saniyeden uzun süren ERROR seviyeli logları getiriyor. Gerçek bir prodüksiyon ortamında performans sorunlarını tespit etmek için bu tür sorgular çok işe yarıyor.

Bool Sorgusu: Sorguların Birleştirilmesi

bool sorgusu, Query DSL’in belkemiğidir. Birden fazla sorgu koşulunu birleştirmenizi sağlar:

  • must: Tüm sorgular eşleşmeli, skora katkı yapar
  • should: En az biri eşleşmeli, skora katkı yapar
  • must_not: Hiçbiri eşleşmemeli, skor hesaplanmaz
  • filter: Tüm sorgular eşleşmeli, skor hesaplanmaz, önbelleğe alınır

Gerçek dünya örneği olarak bir iş ilanı sitesi düşünelim:

GET /is_ilanlari/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "pozisyon": "backend developer"
          }
        }
      ],
      "should": [
        {
          "term": {
            "teknolojiler": "go"
          }
        },
        {
          "term": {
            "teknolojiler": "rust"
          }
        },
        {
          "match": {
            "aciklama": "uzaktan çalışma"
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "aktif": false
          }
        }
      ],
      "filter": [
        {
          "term": {
            "sehir": "istanbul"
          }
        },
        {
          "range": {
            "min_maas": {
              "gte": 50000
            }
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

minimum_should_match: 1 parametresi, should koşullarından en az birinin sağlanmasını zorunlu kılar. Bu sayede hem backend developer hem de minimum maaş kriterlerini karşılayan ve İstanbul’da olan aktif ilanları, Go veya Rust veya uzaktan çalışma imkanı olan sıralamayla görürsünüz.

Fuzzy ve Wildcard Sorgular

Yazım hatalarını tolere etmek veya kısmi eşleşme yapmak için bu sorgu türleri kullanılır.

GET /musteriler/_search
{
  "query": {
    "fuzzy": {
      "isim": {
        "value": "mehmet",
        "fuzziness": "AUTO",
        "prefix_length": 2,
        "max_expansions": 50
      }
    }
  }
}

fuzziness: AUTO ayarı, kelime uzunluğuna göre otomatik düzenleme mesafesi belirler. 3 karakterden kısa kelimeler için 0, 3-5 karakter için 1, 5 karakterden uzun kelimeler için 2 düzenleme mesafesine izin verir.

prefix_length: 2 parametresi önemli. İlk 2 karakterin kesinlikle doğru olması gerektiğini söyler. Bu hem performansı artırır hem de çok alakasız sonuçların gelmesini engeller.

Nested ve Parent-Child Sorgular

İlişkisel veri yapıları söz konusu olduğunda Elasticsearch’te nested objeler veya parent-child ilişkisi kullanılır.

GET /siparisler/_search
{
  "query": {
    "nested": {
      "path": "urunler",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "urunler.ad": "laptop"
              }
            },
            {
              "range": {
                "urunler.fiyat": {
                  "gte": 10000,
                  "lte": 30000
                }
              }
            }
          ]
        }
      },
      "score_mode": "avg"
    }
  }
}

Bu sorgu, içinde 10.000 ile 30.000 TL arasında laptop olan siparişleri bulur. Nested sorgular olmadan, bir siparişteki herhangi bir ürünün adı “laptop” ve herhangi bir ürünün fiyatı bu aralıkta olsaydı eşleşme sağlanırdı. Nested sayesinde bu koşulların aynı ürün için sağlanması zorunlu hale gelir.

Aggregations ile Birlikte Kullanım

Sorgular genellikle aggregation’larla birlikte kullanılır. Hem filtreleme hem de istatistik toplama:

GET /satis_verileri/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "tarih": {
              "gte": "now-30d/d",
              "lte": "now/d"
            }
          }
        },
        {
          "term": {
            "durum": "tamamlandi"
          }
        }
      ]
    }
  },
  "aggs": {
    "gunluk_satis": {
      "date_histogram": {
        "field": "tarih",
        "calendar_interval": "day"
      },
      "aggs": {
        "toplam_gelir": {
          "sum": {
            "field": "tutar"
          }
        },
        "ortalama_siparis": {
          "avg": {
            "field": "tutar"
          }
        }
      }
    },
    "kategori_bazli": {
      "terms": {
        "field": "kategori",
        "size": 10
      },
      "aggs": {
        "kategori_geliri": {
          "sum": {
            "field": "tutar"
          }
        }
      }
    }
  }
}

size: 0 kullanmak, dokümanların kendisini döndürmez, sadece aggregation sonuçlarını alırsınız. Büyük veri setlerinde dashboard’lar için bu tür sorgular çalıştırıyorsanız bu parametre olmadan çok gereksiz veri transfer edersiniz.

Highlight ve Source Filtering

Kullanıcıya hangi parçanın eşleştiğini göstermek için highlight, gereksiz alanları döndürmemek için _source kullanılır:

GET /belgeler/_search
{
  "_source": ["baslik", "yazar", "tarih"],
  "query": {
    "multi_match": {
      "query": "veritabanı optimizasyonu",
      "fields": ["baslik", "icerik"]
    }
  },
  "highlight": {
    "fields": {
      "icerik": {
        "fragment_size": 200,
        "number_of_fragments": 3,
        "pre_tags": ["<strong>"],
        "post_tags": ["</strong>"]
      },
      "baslik": {}
    }
  },
  "from": 0,
  "size": 10,
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    },
    {
      "tarih": {
        "order": "desc"
      }
    }
  ]
}

fragment_size: 200 eşleşme etrafında kaç karakter gösterileceğini, number_of_fragments: 3 ise kaç farklı parça döndürüleceğini belirler. Belge içeriği 10.000 kelime olsa bile sadece ilgili parçaları görürsünüz.

Performans İpuçları

Filter context’i maksimum kullanın. Skor hesaplamaya ihtiyaç duymadığınız her koşulu filter içine alın. Özellikle sabit değerler, tarih aralıkları ve kategori filtreleri için bu kritik.

Deep pagination’dan kaçının. from + size değeri 10.000’i geçmesin. Geçmesi gerekiyorsa search_after veya scroll API kullanın. On binlerce sonuç sayfalaması hem performansı hem cluster’ı etkiler.

Wildcard sorgularda dikkatli olun. Özellikle başında joker karakter olan sorgular (*kelime) full table scan yapar. Mümkünse prefix sorgusu kullanın ya da ngram tokenizer ile indexleme yapın.

Profile API kullanın. Sorgularınızın nerede yavaşladığını anlamak için:

GET /indeks/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        {"match": {"icerik": "optimizasyon"}}
      ],
      "filter": [
        {"term": {"aktif": true}}
      ]
    }
  }
}

Bu, her sorgu bileşeninin ne kadar süre harcadığını gösterir. Prodüksiyonda değil, test ortamında kullanın.

_source filtering uygulayın. Dokümanlarınız büyük JSON objelerse ve sadece birkaç alana ihtiyacınız varsa, tüm kaynağı ağ üzerinden taşımak gereksiz. _source: ["alan1", "alan2"] ile sadece ihtiyacınız olanları çekin.

Gerçek Dünya Senaryosu: Log Analizi

Diyelim ki bir uygulama hata izleme sistemi kuruyorsunuz. Farklı servislerden gelen logları Elasticsearch’te tutuyorsunuz ve operations ekibi için bir arayüz hazırlıyorsunuz:

GET /app_logs-*/_search
{
  "_source": ["zaman", "servis", "seviye", "mesaj", "host", "trace_id"],
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "connection refused timeout",
            "fields": ["mesaj", "hata_detayi"],
            "type": "cross_fields",
            "operator": "or"
          }
        }
      ],
      "filter": [
        {
          "terms": {
            "seviye": ["ERROR", "CRITICAL"]
          }
        },
        {
          "range": {
            "zaman": {
              "gte": "now-1h"
            }
          }
        },
        {
          "terms": {
            "servis": ["api-gateway", "auth-service", "payment-service"]
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "suppress": true
          }
        }
      ]
    }
  },
  "sort": [
    {"zaman": {"order": "desc"}}
  ],
  "size": 50,
  "aggs": {
    "servis_bazli_hatalar": {
      "terms": {
        "field": "servis",
        "size": 20
      }
    },
    "zaman_bazli": {
      "date_histogram": {
        "field": "zaman",
        "fixed_interval": "5m"
      }
    }
  }
}

Bu sorgu birden fazla index pattern’a aynı anda (app_logs-*) bakıyor, son 1 saatteki ERROR ve CRITICAL seviyeli logları çekiyor, belirli servislere odaklanıyor ve bununla birlikte hem servis bazlı hata sayısını hem de zaman grafiği için veriyi tek sorguda alıyor.

Sorgu Testi ve Debugging

Sorgularınızı production’a almadan önce validate API kullanın:

GET /urunler/_validate/query?explain=true
{
  "query": {
    "bool": {
      "must": [
        {"match": {"ad": "telefon"}}
      ]
    }
  }
}

Ayrıca explain parametresiyle bir dokümanın neden o skoru aldığını anlayabilirsiniz:

GET /urunler/_explain/doc_id_123
{
  "query": {
    "match": {
      "ad": "akıllı telefon"
    }
  }
}

Bu özellik, arama sonuçlarının sıralaması beklendiği gibi değilse neden böyle olduğunu anlamak için çok değerli.

Sonuç

Query DSL, ilk bakışta karmaşık gelebilir ama temel prensipleri bir kez oturtunca son derece mantıklı ve güçlü bir yapı sunuyor. Öğrenme sürecinde en çok işinize yarayacak pratik kuralları şöyle özetleyebilirim:

  • Skor hesabına ihtiyaç duymayan her koşulu filter içine alın, performans açısından büyük fark yaratır
  • bool sorgusunu ana iskelet olarak kullanın, neredeyse her karmaşık sorgu onun üzerine inşa edilir
  • Term-level sorgular exact match için, full-text sorgular analiz edilmiş içerik için kullanılır, bu ayrımı asla karıştırmayın
  • Aggregation’ları sorgularınızla birleştirerek tek API çağrısında hem veriyi hem istatistiği alın
  • Profile API ve explain endpoint’lerini test ortamında aktif kullanın

Elasticsearch’i sadece “hızlı arama motoru” olarak değil, gerçek bir analitik platform olarak kullanmak istiyorsanız Query DSL’e hakim olmak zorundasınız. Prodüksiyonda karşılaştığınız her yeni senaryo, bu dili daha iyi anlamanıza katkı sağlayacak.

Bir yanıt yazın

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