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
| Özellik | Query Context | Filter Context |
|---|---|---|
| Soru | "Ne kadar alakalı?" | "Uyuyor mu?" |
| _score | Hesaplanır | Hesaplanmaz (0.0) |
| Performans | Daha yavaş (scoring) | Daha hızlı (no scoring) |
| Cache | Cachelenemez | ✅ Bitset cache |
| Kullanım | Full-text search | Filtreleme (boolean, range, exact match) |
Neden Filter Daha Hızlı?
Scoring hesaplanmaz: BM25 hesaplama maliyeti yok
Cache: Filter sonuçları bitset olarak cache'lenir — aynı filtre tekrar çalıştığında cache'ten döner
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 → Filterbool 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üm | Mantık | Scoring | Context |
|---|---|---|---|
| must | AND | ✅ Evet | Query |
| should | OR | ✅ Evet | Query |
| must_not | NOT | ❌ Hayır | Filter |
| filter | AND | ❌ Hayır | Filter |
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ı:
must: "hızlı laptop" araması — scoring yapılır, en alakalı en üstte
filter: Stok ve fiyat filtreleri — scoring yok, cache'lenir, hızlı
should: Apple veya Lenovo olursa bonus skor — uymayana da sonuç gelir ama sırası düşük
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üçük2. 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ğerli3. 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 çıkarSorgu 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_stringTerm-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şmeCompound 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ı sorguPratik: 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üresiKibana'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ı önceliklendirebilirsinScoring 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!
AI Asistan
Sorularını yanıtlamaya hazır