Query Optimizasyonu — Profiling ve Slow Log
Giriş — Filter Cache, Profiling, Slow Log ve Execution Plan
Bir restoranda düşün: Garson siparişi alıp mutfağa taşıyor, aşçı pişiriyor, garson masaya getiriyor. Yemek 40 dakika sürüyorsa, sorun garsonda mı, aşçıda mı, serviste mi? Bunu anlamak için her adımı ayrı ayrı ölçmen gerek. "Yemek yavaş geldi" demek yetmez — tam olarak nerede yavaş olduğunu bilmen gerek.
Elasticsearch query optimizasyonu aynı mantık. Bir sorgu yavaşsa, nedeni onlarca farklı şey olabilir: kötü yapılandırılmış query, cache'lenmeyen filter, büyük result set, aşırı shard taraması... Sorunu bulmak için Elasticsearch'ün sunduğu araçları — Profile API, Slow Log, Filter Cache mekanizmasını ve Search Profiler'ı — ustaca kullanman gerek.
1. Query Context vs Filter Context — Temel Fark
Bu konuyu daha önce gördük ama optimizasyon açısından tekrar vurgulayalım çünkü performans farkı devasa.
Query Context
Soru: "Bu document ne kadar iyi eşleşiyor?" (relevance scoring)
Sonuç:
_scorehesaplanırCache'lenmez
Filter Context
Soru: "Bu document eşleşiyor mu, evet veya hayır?" (boolean)
Sonuç:
_scorehesaplanmaz → daha hızlıCache'lenir → tekrarlayan sorgularda devasa hız kazanımı
// ❌ YAVAŞ — Her şey query context'te
GET products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "laptop" } },
{ "range": { "price": { "gte": 5000, "lte": 15000 } } },
{ "term": { "category": "electronics" } },
{ "term": { "in_stock": true } }
]
}
}
}
// ✅ HIZLI — Scoring gerektirmeyen kısımlar filter'da
GET products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "laptop" } }
],
"filter": [
{ "range": { "price": { "gte": 5000, "lte": 15000 } } },
{ "term": { "category": "electronics" } },
{ "term": { "in_stock": true } }
]
}
}
}İkinci sorguda range, category, in_stock filtreleri:
Score hesaplamaz → CPU tasarrufu
Bitset olarak cache'lenir → aynı filtre tekrar geldiğinde disk'e bile gitmez
Filter Cache Nasıl Çalışır?
Elasticsearch, filter sonuçlarını node-level request cache ve shard-level query cache olarak saklar.
Node Request Cache:
# Cache durumunu kontrol et
GET _nodes/stats/indices/request_cache
# Belirli bir index için
GET _cat/indices/products?v&h=index,request_cache.memory_size,request_cache.hit_count,request_cache.miss_countCache'i temizleme (test ortamında):
# Tüm cache'leri temizle
POST _cache/clear
# Belirli index
POST products/_cache/clear
# Sadece request cache
POST _cache/clear?request=true💡 İpucu: Filter cache, aynı sorgu aynı shard'a tekrar geldiğinde otomatik çalışır. Siz bir şey yapmanız gerekmez — sadece filter context kullanmayı unutmayın.
2. Profile API — Sorgunun Röntgenini Çekme
Profile API, bir sorgunun her adımında ne kadar zaman harcandığını gösterir. Yavaş sorguların teşhisinde en güçlü araç.
Temel Kullanım
GET products/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "name": "gaming laptop" } }
],
"filter": [
{ "range": { "price": { "gte": 5000, "lte": 20000 } } },
{ "term": { "brand": "asus" } }
]
}
}
}Profile Çıktısını Okuma
{
"profile": {
"shards": [
{
"id": "[node-1][products][0]",
"searches": [
{
"query": [
{
"type": "BooleanQuery",
"description": "+name:gaming +name:laptop #(price:[5000 TO 20000] #brand:asus)",
"time_in_nanos": 1250000,
"breakdown": {
"score": 450000,
"build_scorer_count": 2,
"build_scorer": 320000,
"create_weight": 80000,
"create_weight_count": 1,
"advance": 150000,
"advance_count": 500,
"match": 200000,
"match_count": 500,
"next_doc": 50000,
"next_doc_count": 500
},
"children": [
{
"type": "TermQuery",
"description": "name:gaming",
"time_in_nanos": 380000,
"breakdown": { "..." : "..." }
},
{
"type": "TermQuery",
"description": "name:laptop",
"time_in_nanos": 420000,
"breakdown": { "..." : "..." }
}
]
}
],
"collector": [
{
"name": "SimpleTopScoreDocCollector",
"reason": "search_top_hits",
"time_in_nanos": 200000
}
]
}
],
"aggregations": []
}
]
}
}Breakdown Metrikleri
| Metrik | Açıklama |
|---|---|
create_weight | Sorgu ağacını hazırlama süresi |
build_scorer | Scorer objesini oluşturma |
next_doc | Sonraki eşleşen document'ı bulma |
advance | Belirli bir document'a atlama |
match | İki-fazlı eşleştirme (phrase query gibi) |
score | Relevance score hesaplama |
⚠️ Dikkat:
profile: truesorguyu yavaşlatır (profiling overhead). Production'da sürekli açık bırakmayın — sadece debug amaçlı kullanın.
Aggregation Profiling
GET sales/_search
{
"profile": true,
"size": 0,
"aggs": {
"monthly_sales": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
},
"aggs": {
"total_revenue": {
"sum": { "field": "revenue" }
}
}
}
}
}Aggregation profili, hangi agg'in ne kadar sürdüğünü ayrıntılı gösterir. Özellikle nested aggregation'larda darboğaz tespiti için kritik.
3. Slow Log — Yavaş Sorguları Yakalama
Slow Log, belirlediğiniz eşik süresini aşan sorguları otomatik loglar. Production'da "hangi sorgular yavaş?" sorusunun cevabı.
Slow Log Ayarlama
PUT products/_settings
{
"index.search.slowlog.threshold.query.warn": "5s",
"index.search.slowlog.threshold.query.info": "2s",
"index.search.slowlog.threshold.query.debug": "1s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "500ms",
"index.search.slowlog.threshold.fetch.debug": "200ms",
"index.search.slowlog.threshold.fetch.trace": "100ms",
"index.search.slowlog.level": "info"
}Query vs Fetch fazı:
Query: Eşleşen document'ları bulma (scoring, filtering)
Fetch: Bulunan document'ların
_source'unu getirme
Indexing Slow Log
Sadece arama değil, yazma operasyonları için de slow log ayarlayabilirsiniz:
PUT products/_settings
{
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.source": "1000"
}index.indexing.slowlog.source: Log'a yazılacak source'un max karakter sayısı. 0 kapalı, true tamamı.
Slow Log Çıktısını Okuma
Log dosyası: <es-path>/logs/<cluster-name>_index_search_slowlog.log
[2024-01-15T14:30:45,123][WARN ][index.search.slowlog.query] [node-1]
[products/abc123] took[5.2s], took_millis[5234],
total_hits[15234], types[], stats[],
search_type[QUERY_THEN_FETCH],
total_shards[5], source[{"query":{"match":{"description":"gaming laptop..."}},"size":100}]Bu log'dan anlayacaklarınız:
Sorgu 5.2 saniye sürmüş
15.234 hit bulmuş
5 shard taramış
Sorgunun kendisi
sourcealanında
Cluster-Level Slow Log (Tüm index'ler için)
PUT _cluster/settings
{
"persistent": {
"cluster.search.slow_log.threshold.query.warn": "5s",
"cluster.search.slow_log.threshold.query.info": "2s"
}
}4. Query Optimizasyon Teknikleri
4.1. Gereksiz Field'ları Getirme
// ❌ YAVAŞ — Tüm _source getiriliyor (büyük document'larda ağır)
GET products/_search
{
"query": { "match": { "name": "laptop" } }
}
// ✅ HIZLI — Sadece gereken field'lar
GET products/_search
{
"_source": ["name", "price", "brand"],
"query": { "match": { "name": "laptop" } }
}
// ✅ DAHA DA HIZLI — stored_fields veya docvalue_fields
GET products/_search
{
"_source": false,
"docvalue_fields": ["price", "brand", "created_at"],
"query": { "match": { "name": "laptop" } }
}docvalue_fields, keyword ve numeric field'lar için disk'ten columnar formatta okur — _source parse etmekten hızlıdır.
4.2. Size ve Pagination Optimizasyonu
// ❌ YAVAŞ — Deep pagination
GET products/_search
{
"from": 10000,
"size": 10,
"query": { "match_all": {} }
}
// 10.010 document sıralanıp ilk 10.000'i atılır!
// ✅ HIZLI — search_after kullan
GET products/_search
{
"size": 10,
"query": { "match_all": {} },
"sort": [
{ "created_at": "desc" },
{ "_id": "asc" }
],
"search_after": ["2024-01-15T10:00:00Z", "abc123"]
}4.3. Shard Routing ile Hedefli Arama
// ❌ Tüm shard'lar taranıyor
GET logs/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "tenant_id": "acme-corp" } }
]
}
}
}
// ✅ Sadece ilgili shard taranıyor
GET logs/_search?routing=acme-corp
{
"query": {
"bool": {
"filter": [
{ "term": { "tenant_id": "acme-corp" } }
]
}
}
}4.4. Preference ile Cache Optimizasyonu
// Her seferinde aynı shard kopyasına git → cache hit oranı artar
GET products/_search?preference=_prefer_local
{
"query": { "match": { "name": "laptop" } }
}
// Kullanıcı bazlı preference (session affinity)
GET products/_search?preference=user_12345
{
"query": { "match": { "name": "laptop" } }
}preference parametresi, aynı sorguyu hep aynı shard kopyasına yönlendirir. Bu, o shard'ın OS page cache'ini sıcak tutar.
4.5. Wildcard ve Regexp Optimizasyonu
// ❌ ÇOK YAVAŞ — Leading wildcard (tüm term'leri tarar)
GET products/_search
{
"query": {
"wildcard": { "name": "*laptop*" }
}
}
// ✅ DAHA İYİ — Prefix wildcard (index'teki sıralı yapıyı kullanır)
GET products/_search
{
"query": {
"wildcard": { "name": "laptop*" }
}
}
// ✅ EN İYİ — match query (analyzer kullanır)
GET products/_search
{
"query": {
"match": { "name": "laptop" }
}
}⚠️ Dikkat:
*laptop*şeklindeki wildcard'lar, index'teki tüm unique term'leri tarar. Milyonlarca unique term varsa saniyeler sürebilir.
4.6. Scripting Performansı
// ❌ YAVAŞ — Her document için script çalışır
GET products/_search
{
"query": {
"script_score": {
"query": { "match_all": {} },
"script": {
"source": "doc['price'].value * doc['discount_rate'].value"
}
}
}
}
// ✅ DAHA İYİ — Önceden hesaplanmış field kullan
// Index-time'da discounted_price field'ı hesapla
PUT products/_mapping
{
"properties": {
"discounted_price": { "type": "float" }
}
}
// Sonra basit range query yeterli
GET products/_search
{
"query": {
"range": {
"discounted_price": { "gte": 1000, "lte": 5000 }
}
}
}4.7. Aggregation Optimizasyonu
// ❌ YAVAŞ — Global aggregation (tüm document'lar)
GET logs/_search
{
"size": 0,
"aggs": {
"status_codes": {
"terms": {
"field": "status_code",
"size": 100
}
}
}
}
// ✅ HIZLI — Filtreli aggregation
GET logs/_search
{
"size": 0,
"query": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
},
"aggs": {
"status_codes": {
"terms": {
"field": "status_code",
"size": 10
}
}
}
}Aggregation'lar her zaman query sonuçları üzerinde çalışır. Önce filtre ile sonuç setini küçültmek, aggregation'ı hızlandırır.
5. Execution Plan — Sorgunun Arka Planı
Search Type
Elasticsearch iki aşamalı arama yapar:
Query Phase: Her shard'da eşleşen document ID'leri ve score'lar bulunur
Fetch Phase: En iyi sonuçların
_source'u getirilir
// Varsayılan: query_then_fetch
GET products/_search?search_type=query_then_fetch
{
"query": { "match": { "name": "laptop" } }
}
// DFS query then fetch: Daha doğru scoring (distributed term frequencies)
GET products/_search?search_type=dfs_query_then_fetch
{
"query": { "match": { "name": "laptop" } }
}dfs_query_then_fetch ne zaman kullanılır? Küçük veri setlerinde term frequency dağılımı shard'lar arasında dengesiz olabilir. Bu durumda scoring tutarsız olur. DFS, önce tüm shard'lardan term frequency toplar, sonra arama yapar.
Adaptive Replica Selection
Elasticsearch 7.x+ varsayılan olarak sorguları en hızlı yanıt verecek shard kopyasına yönlendirir:
// Bu özellik varsayılan olarak açık
PUT _cluster/settings
{
"persistent": {
"cluster.routing.use_adaptive_replica_selection": true
}
}6. Java ile Query Profiling
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Profile;
import co.elastic.clients.elasticsearch.core.search.ShardProfile;
import co.elastic.clients.elasticsearch.core.search.QueryProfile;
// Profile'lı arama
SearchResponse<Product> response = client.search(s -> s
.index("products")
.profile(true)
.query(q -> q
.bool(b -> b
.must(m -> m
.match(mt -> mt
.field("name")
.query("gaming laptop")
)
)
.filter(f -> f
.range(r -> r
.field("price")
.gte(JsonData.of(5000))
.lte(JsonData.of(20000))
)
)
)
),
Product.class
);
// Profile sonuçlarını oku
Profile profile = response.profile();
if (profile != null) {
for (ShardProfile shard : profile.shards()) {
System.out.println("Shard: " + shard.id());
for (var search : shard.searches()) {
for (QueryProfile query : search.query()) {
System.out.println(" Query type: " + query.type());
System.out.println(" Time: " + query.timeInNanos() / 1_000_000.0 + "ms");
System.out.println(" Description: " + query.description());
}
}
}
}Slow Query Logger (Java Client)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.time.Instant;
public class ElasticsearchQueryLogger {
private static final Logger log = LoggerFactory.getLogger(ElasticsearchQueryLogger.class);
private static final long SLOW_QUERY_THRESHOLD_MS = 1000;
private final ElasticsearchClient client;
public ElasticsearchQueryLogger(ElasticsearchClient client) {
this.client = client;
}
public <T> SearchResponse<T> searchWithLogging(
SearchRequest request, Class<T> clazz) throws Exception {
Instant start = Instant.now();
SearchResponse<T> response = client.search(
SearchRequest.of(b -> b
.index(request.index())
.query(request.query())
.size(request.size())
), clazz);
long elapsed = Duration.between(start, Instant.now()).toMillis();
if (elapsed > SLOW_QUERY_THRESHOLD_MS) {
log.warn("SLOW QUERY detected! Index: {}, Duration: {}ms, " +
"Hits: {}, Shards: {}/{}",
request.index(),
elapsed,
response.hits().total().value(),
response.shards().successful(),
response.shards().total()
);
}
return response;
}
}7. Request Cache ve Shard Query Cache
Request Cache
Index değişmediyse, aynı sorgunun sonucu cache'ten döner.
// Request cache'i index bazında kontrol et
GET _cat/indices/products?v&h=index,rc.memory_size,rc.evictions
// Request cache'i açma/kapama
PUT products/_settings
{
"index.requests.cache.enable": true
}
// Tek bir sorguda cache'i devre dışı bırak
GET products/_search?request_cache=false
{
"size": 0,
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
}Request cache kuralları:
size: 0olan sorgularda (sadece aggregation) varsayılan açıkDocument döndüren sorgularda varsayılan kapalı
Index refresh olunca cache invalidate olur
Shard-Level Query Cache (Node Query Cache)
Filter context'teki sorguların bitset sonuçlarını cache'ler.
# Node query cache istatistikleri
GET _nodes/stats/indices/query_cache
# Çıktıda önemli metrikler:
# - memory_size_in_bytes: Cache boyutu
# - hit_count: Cache'ten dönen sorgu sayısı
# - miss_count: Cache'te bulunamayan
# - evictions: Cache'ten çıkarılan entry sayısıCache boyutu varsayılan: heap'in %10'u. Değiştirmek için elasticsearch.yml:
indices.queries.cache.size: 15%8. Bütünleşik Örnek: E-Commerce Arama Optimizasyonu
Bir e-ticaret sitesinde ürün arama sorgusunu adım adım optimize edelim.
Başlangıç — Optimize Edilmemiş Sorgu
// Orijinal sorgu — 850ms süre, kabul edilemez
GET products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "samsung galaxy" } },
{ "term": { "category": "phones" } },
{ "term": { "in_stock": true } },
{ "range": { "price": { "gte": 5000, "lte": 25000 } } },
{ "term": { "seller_verified": true } }
]
}
},
"sort": [
{ "_score": "desc" }
],
"from": 0,
"size": 50
}Adım 1: Filter Context Kullanımı
// must → filter (scoring gerekmeyenler)
// 850ms → 320ms
GET products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "samsung galaxy" } }
],
"filter": [
{ "term": { "category": "phones" } },
{ "term": { "in_stock": true } },
{ "range": { "price": { "gte": 5000, "lte": 25000 } } },
{ "term": { "seller_verified": true } }
]
}
},
"from": 0,
"size": 50
}Adım 2: Source Filtering
// Sadece gerekli field'ları getir
// 320ms → 280ms
GET products/_search
{
"_source": ["name", "price", "image_url", "rating", "seller_name"],
"query": {
"bool": {
"must": [
{ "match": { "name": "samsung galaxy" } }
],
"filter": [
{ "term": { "category": "phones" } },
{ "term": { "in_stock": true } },
{ "range": { "price": { "gte": 5000, "lte": 25000 } } },
{ "term": { "seller_verified": true } }
]
}
},
"from": 0,
"size": 20
}Adım 3: Size Küçültme + Routing
// Routing ekle (multi-tenant senaryo)
// 280ms → 95ms
GET products/_search?routing=marketplace_tr&preference=_local
{
"_source": ["name", "price", "image_url", "rating", "seller_name"],
"query": {
"bool": {
"must": [
{ "match": { "name": "samsung galaxy" } }
],
"filter": [
{ "term": { "category": "phones" } },
{ "term": { "in_stock": true } },
{ "range": { "price": { "gte": 5000, "lte": 25000 } } },
{ "term": { "seller_verified": true } }
]
}
},
"from": 0,
"size": 20
}Adım 4: Profile ile Doğrulama
GET products/_search?routing=marketplace_tr
{
"profile": true,
"_source": ["name", "price"],
"query": {
"bool": {
"must": [
{ "match": { "name": "samsung galaxy" } }
],
"filter": [
{ "term": { "category": "phones" } },
{ "term": { "in_stock": true } }
]
}
},
"size": 20
}Sonuç: 850ms → 95ms. ~9x hızlanma, sadece query yapısını değiştirerek.
9. Best Practices
✅ Yap
| Konu | Öneri |
|---|---|
| Score gerekmiyorsa | filter context kullan |
| Büyük document'lar | _source filtering veya docvalue_fields |
| Deep pagination | search_after kullan, from/size değil |
| Slow log | Production'da her zaman açık olsun |
| Wildcard | Leading wildcard (*text) kullanma |
| Aggregation | Query ile sonuç setini küçült, sonra aggregate et |
| Script | Mümkünse index-time'da hesapla |
❌ Yapma
| Konu | Neden |
|---|---|
profile: true production'da | Overhead ekler, debug amaçlı |
from: 10000 | 10K document sıralanıp atılır |
match_all + büyük size | Tüm index'i tarar ve hafızaya alır |
| Script-based sorting | Her document için script çalışır |
_all field arama | Deprecated ve yavaş |
10. Yaygın Hatalar ve Çözümleri
Hata 1: Filter Kullanmayı Unutmak
// Sorun: Tüm clause'lar must'ta → cache yok, score hesaplanıyor
// Çözüm: Score gerekmeyen clause'ları filter'a taşı
// ❌
"must": [
{ "term": { "status": "active" } },
{ "match": { "title": "elasticsearch" } }
]
// ✅
"must": [
{ "match": { "title": "elasticsearch" } }
],
"filter": [
{ "term": { "status": "active" } }
]Hata 2: Slow Log Eşiklerini Çok Yüksek Tutmak
// ❌ Sadece 10s üstü loglanıyor — çoğu yavaş sorgu kaçıyor
"index.search.slowlog.threshold.query.warn": "10s"
// ✅ Daha agresif eşikler
"index.search.slowlog.threshold.query.warn": "3s",
"index.search.slowlog.threshold.query.info": "1s",
"index.search.slowlog.threshold.query.debug": "500ms"Hata 3: Cache Invalidation Beklememek
// Sorun: Filter cache var ama her seferinde miss oluyor
// Neden: Index çok sık refresh oluyor (varsayılan 1s)
// Çözüm: Yazma yoğun index'lerde refresh interval'ı artır
PUT heavy-write-index/_settings
{
"index.refresh_interval": "30s"
}Hata 4: Gereksiz Highlight
// ❌ Tüm field'larda highlight — yavaş
GET articles/_search
{
"query": { "match": { "content": "elasticsearch" } },
"highlight": {
"fields": {
"*": {}
}
}
}
// ✅ Sadece gerekli field'da
GET articles/_search
{
"query": { "match": { "content": "elasticsearch" } },
"highlight": {
"fields": {
"content": {
"fragment_size": 150,
"number_of_fragments": 3
}
}
}
}Hata 5: Track Total Hits
// Elasticsearch 7+ varsayılan olarak 10.000'den sonra total hit saymaz
// Bu bir optimizasyon — zorla kapatmayın
// ❌ Tüm hit'leri saydırmak (yavaşlatır)
GET products/_search
{
"track_total_hits": true,
"query": { "match_all": {} }
}
// ✅ Varsayılanı koru veya kapalı tut
GET products/_search
{
"track_total_hits": false,
"query": { "match": { "name": "laptop" } }
}11. Monitoring: Query Performance Dashboard
Production'da sorgu performansını sürekli izlemek için temel metrikler:
# Arama performans metrikleri
GET _nodes/stats/indices/search
# Önemli alanlar:
# - search.query_total: Toplam sorgu sayısı
# - search.query_time_in_millis: Toplam sorgu süresi
# - search.query_current: Şu an çalışan sorgu sayısı
# - search.fetch_total: Toplam fetch sayısı
# - search.fetch_time_in_millis: Toplam fetch süresi
# Index bazında
GET _cat/indices?v&h=index,search.query_total,search.query_timeOrtalama sorgu süresi formülü:
Avg Query Time = search.query_time_in_millis / search.query_totalBu değer 100ms'in altındaysa genel olarak iyi durumdasınız. 500ms üzeriyse optimizasyon gerekir.
Özet
Filter context kullanın — score gerekmiyorsa
filterbloğuna koyun. Cache'lenir ve daha hızlıdır.Profile API yavaş sorguların teşhisinde en güçlü araç — her query bileşeninin süresini gösterir, ama production'da sürekli açık bırakmayın.
Slow Log production'da her zaman aktif olmalı — yavaş sorguları otomatik yakalar, sonradan analiz imkanı verir.
Source filtering ile sadece gereken field'ları getirin — özellikle büyük document'larda belirgin fark yaratır.
Deep pagination yerine search_after kullanın —
from: 10000demek 10K document'ı sıralayıp atmak demektir.Wildcard'ın başına * koymayın — leading wildcard tüm term'leri tarar. Match query veya prefix wildcard tercih edin.
AI Asistan
Sorularını yanıtlamaya hazır