← Kursa Dön
📄 Text · 25 min

Query DSL'e Giriş — Query vs Filter Context

Query vs Filter Context, Relevance Scoring

Bir arama motoru kullanırken iki farklı türde soru sorarsın. Birincisi: "Bu belge ne kadar alakalı?" — Google'a "en iyi Java kitabı" yazdığında, sonuçlar alakalılıklarına göre sıralanır. İkincisi: "Bu belge kritere uyuyor mu?" — evet ya da hayır. Fiyatı 100-500 TL arası mı? Stokta mı? Bu ikili ayrım, Elasticsearch'ün en temel kavramlarından biri.

Birincisine Query Context, ikincisine Filter Context denir. Bu ayrımı anlamak, hem doğru sonuçlar almak hem de performanslı sorgular yazmak için kritik.


Query DSL Nedir?

Query DSL (Domain Specific Language), Elasticsearch'ün JSON tabanlı sorgu dilidir. SQL'e alternatif olarak tasarlanmıştır — ama SQL'den çok daha güçlü ve esnek.

Temel Yapı

GET /products/_search
{
  "query": {
    <QUERY_TYPE>: {
      <FIELD>: <VALUE_OR_PARAMETERS>
    }
  }
}

En Basit Sorgu — match_all

// Tüm dökümanları getir
GET /products/_search
{
  "query": {
    "match_all": {}
  }
}

match_all her dökümana eşit skor (1.0) verir. Tüm veriyi listelemek, test yapmak veya filtreleme ile birlikte kullanmak için ideal.

Sorgu Anatomisi

GET /products/_search
{
  "query": { ... },        // Arama sorgusu
  "from": 0,               // Başlangıç offset (sayfalama)
  "size": 10,              // Döndürülecek sonuç sayısı
  "sort": [ ... ],         // Sıralama
  "_source": [ ... ],      // Döndürülecek alanlar
  "aggs": { ... },         // Aggregation'lar
  "highlight": { ... },    // Sonuçlarda vurgulama
  "explain": false,        // Skor açıklaması
  "timeout": "10s"         // Zaman aşımı
}

Query Context vs Filter Context

Bu ayrım Elasticsearch'ün en kritik kavramlarından biri.

Query Context — "Ne kadar alakalı?"

// Query context — relevance scoring yapılır
GET /products/_search
{
  "query": {
    "match": {
      "description": "hızlı güvenilir laptop"
    }
  }
}

Query context'te Elasticsearch her döküman için _score hesaplar:

  • "hızlı" kelimesi var mı? Kaç kez geçiyor? Ne kadar nadir?

  • "güvenilir" kelimesi var mı?

  • "laptop" kelimesi var mı?

  • Alan uzunluğu ne?

BM25 algoritmasıyla tüm bu faktörler birleştirilir ve her dökümana bir skor verilir. Sonuçlar skora göre sıralanır.

Filter Context — "Uyuyor mu, uymuyor mu?"

// Filter context — scoring YOK, sadece evet/hayır
GET /products/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "in_stock": true } },
        { "range": { "price": { "gte": 1000, "lte": 50000 } } }
      ]
    }
  }
}

Filter context'te scoring hesaplanmaz. Döküman ya filtreye uyar ya uymaz. İkili (binary) sonuç.

Karşılaştırma

ÖzellikQuery ContextFilter Context
Soru"Ne kadar alakalı?""Uyuyor mu?"
_scoreHesaplanırHesaplanmaz (0.0)
PerformansDaha yavaş (scoring)Daha hızlı (no scoring)
CacheCachelenemez✅ Bitset cache
KullanımFull-text searchFiltreleme (boolean, range, exact match)

Neden Filter Daha Hızlı?

  1. Scoring hesaplanmaz: BM25 hesaplama maliyeti yok

  2. Cache: Filter sonuçları bitset olarak cache'lenir — aynı filtre tekrar çalıştığında cache'ten döner

  3. Bitset operasyonları: AND/OR işlemleri bit düzeyinde yapılır — çok hızlı

İlk çalıştırma:
  "in_stock: true" → [1,1,0,1,1,0,1,1,1,0] bitset → Cache'e yaz

Sonraki çalıştırmalar:
  "in_stock: true" → Cache'ten oku → Anında sonuç!

Altın Kural

Scoring gerekiyorsa → Query context
Scoring gerekmiyorsa → Filter context (DAHA HIZLI!)

Pratik kural:
- Full-text arama (match, multi_match) → Query
- Boolean filtreler (term, exists) → Filter
- Sayısal aralıklar (range) → Filter
- Tarih aralıkları (range) → Filter
- Geo filtreler → Filter

bool Query — Her Şeyin Temeli

bool query, birden fazla sorguyu birleştirmenin ana yolu. Dört bölümü var:

GET /products/_search
{
  "query": {
    "bool": {
      "must": [ ... ],       // VE — her biri eşleşmeli + scoring'e katkı
      "should": [ ... ],     // VEYA — en az biri eşleşmeli + scoring'e katkı
      "must_not": [ ... ],   // DEĞİL — hiçbiri eşleşmemeli (filter context)
      "filter": [ ... ]      // VE — her biri eşleşmeli (filter context, no scoring)
    }
  }
}
BölümMantıkScoringContext
mustAND✅ EvetQuery
shouldOR✅ EvetQuery
must_notNOT❌ HayırFilter
filterAND❌ HayırFilter

Gerçek Dünya Örneği — E-Ticaret Araması

// Kullanıcı: "hızlı laptop" aratıyor
// Filtreler: Stokta olan, 5000-50000 TL arası, Apple veya Lenovo markası
// Sıralama: Önce alakalılık, sonra fiyat

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "hızlı laptop",
            "fields": ["name^3", "description"]
          }
        }
      ],
      "filter": [
        { "term": { "in_stock": true } },
        { "range": { "price": { "gte": 5000, "lte": 50000 } } }
      ],
      "should": [
        { "term": { "brand.keyword": "Apple" } },
        { "term": { "brand.keyword": "Lenovo" } }
      ],
      "must_not": [
        { "term": { "status": "discontinued" } }
      ]
    }
  },
  "sort": [
    { "_score": "desc" },
    { "price": "asc" }
  ]
}

Bu sorgunun parçaları:

  1. must: "hızlı laptop" araması — scoring yapılır, en alakalı en üstte

  2. filter: Stok ve fiyat filtreleri — scoring yok, cache'lenir, hızlı

  3. should: Apple veya Lenovo olursa bonus skor — uymayana da sonuç gelir ama sırası düşük

  4. must_not: Discontinued ürünleri hariç tut — filter context, scoring yok

should'un Davranışı

should, must veya filter ile birlikte kullanıldığında opsiyonel olur — eşleşmese de sonuç gelir, ama eşleşen döküman daha yüksek skor alır.

// must + should → should opsiyonel (bonus scoring)
{
  "bool": {
    "must": [
      { "match": { "name": "laptop" } }
    ],
    "should": [
      { "term": { "brand.keyword": "Apple" } }
      // Apple markalı laptoplar daha yüksek skor alır
      // Ama Apple olmayan laptoplar da sonuçta var
    ]
  }
}

// Sadece should → minimum 1 should eşleşmeli
{
  "bool": {
    "should": [
      { "match": { "name": "laptop" } },
      { "match": { "name": "tablet" } }
    ]
    // "laptop" VEYA "tablet" — en az biri eşleşmeli
  }
}

minimum_should_match

// En az 2 should koşulu eşleşmeli
{
  "bool": {
    "should": [
      { "term": { "tags": "premium" } },
      { "term": { "tags": "yeni" } },
      { "term": { "tags": "indirimli" } }
    ],
    "minimum_should_match": 2
  }
}

Relevance Scoring Derinlemesine

BM25 Algoritması

Elasticsearch 5.0'dan beri varsayılan scoring algoritması BM25 (Best Matching 25):

score(q, d) = Σ IDF(qi) × (TF(qi, d) × (k1 + 1)) / (TF(qi, d) + k1 × (1 - b + b × |d| / avgdl))

Üç temel bileşen:

1. TF (Term Frequency) — Kelime Sıklığı

"laptop" Doc A'da 3 kez geçiyor → TF yüksek
"laptop" Doc B'de 1 kez geçiyor → TF düşük
Ama logaritmik: 3 kez ile 30 kez arasındaki fark küçük

2. IDF (Inverse Document Frequency) — Ters Döküman Sıklığı

"ve" kelimesi 10.000 dökümandan 9.000'inde geçiyor → IDF çok düşük
"elasticsearch" 10.000'den 50'sinde geçiyor → IDF çok yüksek
Nadir kelimeler daha değerli

3. Field Length Norm

"laptop" 3 kelimelik başlıkta geçiyor → Yüksek norm
"laptop" 5.000 kelimelik makalede geçiyor → Düşük norm
Kısa alanda eşleşme daha anlamlı

Explain API — Skoru Anlama

// Bir dökümanın skorunu açıkla
GET /products/_explain/1
{
  "query": {
    "match": {
      "name": "hızlı laptop"
    }
  }
}

// Yanıt (kısaltılmış):
{
  "matched": true,
  "explanation": {
    "value": 3.456,
    "description": "sum of:",
    "details": [
      {
        "value": 1.789,
        "description": "weight(name:hızlı in 0)",
        "details": [
          { "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5))" },
          { "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl))" }
        ]
      },
      {
        "value": 1.667,
        "description": "weight(name:laptop in 0)"
      }
    ]
  }
}

Arama Sonuçlarında Explain

// Tüm sonuçlarda skor açıklaması
GET /products/_search
{
  "explain": true,
  "query": {
    "match": {
      "name": "hızlı laptop"
    }
  }
}

Scoring'i Etkileyen Faktörler

// 1. Boosting — Alan önceliklendirme
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "laptop",
      "fields": ["name^3", "description^1", "tags^2"]
      // name alanındaki eşleşme 3 kat daha değerli
    }
  }
}

// 2. Constant score — Scoring'i sabitleme
GET /products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "category.keyword": "Laptop" }
      },
      "boost": 1.5
    }
  }
}
// Her eşleşen döküman sabit 1.5 skor alır

// 3. Function score — Özel scoring fonksiyonları
GET /products/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "name": "laptop" }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "popularity",
            "modifier": "log1p",
            "factor": 0.5
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}
// Text arama skoru × popularity faktörü
// Popüler ürünler daha üstte çıkar

Sorgu Tipleri — Genel Bakış

Elasticsearch'te onlarca sorgu tipi var. Bunları kategorize edelim:

Full-Text Queries (Analiz edilir)

match           → Temel full-text arama
multi_match     → Birden fazla alanda arama
match_phrase    → Tam cümle eşleşmesi
match_phrase_prefix → Cümle prefix'i
query_string    → Lucene sorgu syntax'ı
simple_query_string → Basitleştirilmiş query_string

Term-Level Queries (Analiz edilmez)

term            → Exact match (tek değer)
terms           → Exact match (birden fazla değer)
range           → Aralık sorgusu (sayı, tarih)
exists          → Alan var mı?
prefix          → Prefix eşleşmesi
wildcard        → Wildcard eşleşmesi (* ve ?)
regexp          → Regular expression
fuzzy           → Yaklaşık eşleşme (typo tolerans)
ids             → ID listesiyle eşleşme

Compound Queries (Birleştirici)

bool            → must, should, must_not, filter
boosting        → Positive + negative scoring
dis_max         → En iyi eşleşen alanın skoru
constant_score  → Sabit skor
function_score  → Özel scoring fonksiyonları

Diğer Sorgular

nested          → Nested objeler için
has_child/has_parent → Parent-child ilişkiler
geo_distance    → Coğrafi mesafe
geo_bounding_box → Coğrafi kutu
more_like_this  → Benzer dökümanlar
script          → Script tabanlı sorgu

Pratik: Adım Adım Sorgu Geliştirme

Sıfırdan bir e-ticaret arama sorgusu geliştirelim:

Adım 1: Basit Arama

// Sadece metin araması
GET /products/_search
{
  "query": {
    "match": {
      "name": "kablosuz kulaklık"
    }
  }
}

Adım 2: Birden Fazla Alanda Arama

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "kablosuz kulaklık",
      "fields": ["name^3", "description", "tags^2"]
    }
  }
}

Adım 3: Filtreler Ekle

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "kablosuz kulaklık",
            "fields": ["name^3", "description", "tags^2"]
          }
        }
      ],
      "filter": [
        { "term": { "in_stock": true } },
        { "range": { "price": { "gte": 500, "lte": 10000 } } }
      ]
    }
  }
}

Adım 4: Bonus Scoring + Hariç Tutma

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "kablosuz kulaklık",
            "fields": ["name^3", "description", "tags^2"]
          }
        }
      ],
      "filter": [
        { "term": { "in_stock": true } },
        { "range": { "price": { "gte": 500, "lte": 10000 } } },
        { "range": { "rating": { "gte": 4.0 } } }
      ],
      "should": [
        { "term": { "brand.keyword": { "value": "Sony", "boost": 2.0 } } },
        { "term": { "brand.keyword": { "value": "Apple", "boost": 1.5 } } },
        { "term": { "tags": "premium" } }
      ],
      "must_not": [
        { "term": { "status": "discontinued" } },
        { "term": { "condition": "refurbished" } }
      ]
    }
  },
  "_source": ["name", "brand", "price", "rating", "thumbnail"],
  "sort": [
    { "_score": "desc" },
    { "rating": "desc" }
  ],
  "size": 20
}

Java ile Query DSL

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
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.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())
        );

        // Bool query ile e-ticaret araması
        SearchResponse<Map> response = client.search(s -> s
            .index("products")
            .query(q -> q
                .bool(b -> b
                    // must: Full-text arama
                    .must(m -> m
                        .multiMatch(mm -> mm
                            .query("kablosuz kulaklık")
                            .fields("name^3", "description", "tags^2")
                        )
                    )
                    // filter: Filtreleme (no scoring)
                    .filter(f -> f
                        .term(t -> t.field("in_stock").value(true))
                    )
                    .filter(f -> f
                        .range(r -> r
                            .field("price")
                            .gte(co.elastic.clients.json.JsonData.of(500))
                            .lte(co.elastic.clients.json.JsonData.of(10000))
                        )
                    )
                    // must_not: Hariç tut
                    .mustNot(mn -> mn
                        .term(t -> t.field("status").value("discontinued"))
                    )
                )
            )
            .source(src -> src
                .filter(f -> f.includes("name", "brand", "price", "rating"))
            )
            .size(20),
            Map.class
        );

        System.out.println("Toplam: " + response.hits().total().value());
        System.out.println("Süre: " + response.took() + "ms");
        System.out.println();

        for (Hit<Map> hit : response.hits().hits()) {
            System.out.printf("Score: %.3f | %s%n", hit.score(), hit.source());
        }

        restClient.close();
    }
}

Sorgu Profiling — Performans Analizi

Sorguların ne kadar sürdüğünü ve darboğazları bulmak için Profile API kullanabilirsin:

GET /products/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "laptop" } }
      ],
      "filter": [
        { "term": { "in_stock": true } },
        { "range": { "price": { "gte": 1000, "lte": 50000 } } }
      ]
    }
  }
}

// Yanıtın "profile" bölümünde her shard'ın detaylı çalışma süresi gösterilir:
// - Query phase: Hangi sorgu ne kadar sürdü
// - Collector: Sonuç toplama süresi
// - Rewrite: Sorgu optimize etme süresi

Kibana'da Search Profiler (Dev Tools → Search Profiler) görsel olarak sorgu performansını analiz eder — hangi bölüm en çok zaman alıyor, kolayca görürsün.


Best Practices

Scoring gerekmiyorsa filter context kullan — Cache'lenir, daha hızlı

bool query'de filter bölümünü sık kullan — Exact match, range, boolean → filter'a koy

Boosting ile alan önceliklendirme yap — Başlık daha önemli → name^3

Explain API ile scoring debug et — Beklenmedik sonuçlarda _explain kullan

_source filtreleme yap — Gereksiz alan transfer etme

minimum_should_match'i bilinçli kullan — should koşullarının kaçının eşleşmesini istediğini belirt


Yaygın Hatalar

❌ "Her şeyi must'a koyuyorum"

Exact match ve range sorgularını filter'a koy. must'a koymak gereksiz scoring hesabı ve cache kaybı yaratır.

// ❌ Yanlış — scoring gereksiz
{ "must": [ { "term": { "in_stock": true } } ] }

// ✅ Doğru — filter context, cache'lenir
{ "filter": [ { "term": { "in_stock": true } } ] }

❌ "Query ve filter farkını bilmiyorum"

Kural basit: Kullanıcı ne yazdıysa → query (scoring lazım). Sistem filtreleri → filter (scoring lazım değil).

❌ "Explain çok karmaşık, bakmıyorum"

Explain çıktısı karmaşık görünür ama düzenli okuyunca pattern'i kavrarsın. Unexpected sonuçlarda debug için vazgeçilmez.

❌ "should ile OR mantığı yapıyorum ama must da var"

must + should birlikte kullanıldığında should opsiyonel olur — OR mantığı yapmaz. Gerçek OR istiyorsan sadece should kullan veya minimum_should_match: 1 belirt.

❌ "from + size ile derin sayfalama yapıyorum"

from: 9990, size: 10 dediğinde her shard top 10.000 hesaplar. Çok pahalı. Derin sayfalama için search_after kullan.


Özet

  • Query DSL, Elasticsearch'ün JSON tabanlı sorgu dilidir — SQL'den çok daha güçlü

  • Query Context scoring yapar ("ne kadar alakalı?"), Filter Context yapmaz ("uyuyor mu?")

  • Filter context cache'lenir ve scoring olmadığı için daha hızlıdır

  • bool query dört bölümden oluşur: must (AND+scoring), should (OR+scoring), filter (AND, no scoring), must_not (NOT, no scoring)

  • BM25 algoritması TF, IDF ve Field Length Norm ile skor hesaplar

  • Boosting (^ operatörü) ile alanları önceliklendirebilirsin

  • Scoring gerekmiyorsa her zaman filter context kullan — performans farkı büyük

Bir sonraki derste Full-Text Queries derinlemesine öğreneceğiz — match, multi_match, match_phrase!