Search Templates ve Async Search
Giriş — Sorguları Tekrar Tekrar Yazmaktan Bıktınız mı?
Bir e-ticaret sitesi düşünün. Ürün arama, kategori filtreleme, fiyat aralığı, sıralama — bu sorguların hepsini farklı sayfalarda, farklı parametrelerle tekrar tekrar yazıyorsunuz. Backend'de 15 farklı endpoint var ve hepsinde neredeyse aynı Elasticsearch sorgusu. Bir gün sorguda küçük bir değişiklik yapmanız gerekiyor — 15 yeri mi güncelleyeceksiniz?
Search Templates tam bu sorunu çözer. SQL'deki prepared statement'lar gibi, Elasticsearch sorgularını parametrik şablonlara dönüştürür. Şablonu bir kez tanımlarsınız, her çağrıda sadece parametreleri değiştirirsiniz.
İkinci konu ise Async Search. Normal search senkrondur — 30 saniye süren bir aggregation sorgusu istemciyi 30 saniye bekletir. Async search ile sorguyu arka planda başlatır, kısmi sonuçları alır, tamamlandığında çeker ve bitince temizlersiniz.
1. Search Templates — Mustache Syntax
Elasticsearch, search template'ler için Mustache şablon motorunu kullanır. Mustache, logic-less bir template dilidir — basit, güvenli ve öğrenmesi kolaydır.
1.1 Temel Mustache Syntax
// İnline template — hızlı test
POST my_index/_search/template
{
"source": {
"query": {
"match": {
"{{field}}": "{{query}}"
}
}
},
"params": {
"field": "title",
"query": "elasticsearch"
}
}{{field}} ve {{query}} Mustache değişkenleridir. params objesi ile değer atanır.
1.2 Koşullu Bloklar
POST my_index/_search/template
{
"source": """
{
"query": {
"bool": {
"must": [
{ "match": { "title": "{{query}}" } }
]
{{#category}}
,"filter": [
{ "term": { "category": "{{category}}" } }
]
{{/category}}
}
}
}
""",
"params": {
"query": "laptop",
"category": "Elektronik"
}
}{{#category}}...{{/category}} bloğu, category parametresi varsa render edilir, yoksa atlanır. Bu, opsiyonel filtreleme için mükemmeldir.
1.3 Liste İterasyonu
POST my_index/_search/template
{
"source": """
{
"query": {
"bool": {
"must": [
{ "match": { "title": "{{query}}" } }
],
"filter": [
{
"terms": {
"category": [
{{#categories}}
"{{.}}"
{{^last}},{{/last}}
{{/categories}}
]
}
}
]
}
}
}
""",
"params": {
"query": "telefon",
"categories": [
{"value": "Elektronik", "last": false},
{"value": "Aksesuar", "last": true}
]
}
}💡 İpucu: Mustache'da array virgül yönetimi zahmetlidir. {{#toJson}} helper kullanmak daha pratiktir:
POST my_index/_search/template
{
"source": """
{
"query": {
"bool": {
"filter": [
{ "terms": { "category": {{#toJson}}categories{{/toJson}} } }
]
}
}
}
""",
"params": {
"categories": ["Elektronik", "Aksesuar", "Giyim"]
}
}{{#toJson}} Elasticsearch'ün özel Mustache helper'ıdır. Parametreyi doğrudan JSON'a çevirir.
1.4 Varsayılan Değerler
POST my_index/_search/template
{
"source": """
{
"from": {{from}}{{^from}}0{{/from}},
"size": {{size}}{{^size}}10{{/size}},
"query": {
"match": { "title": "{{query}}" }
}
}
""",
"params": {
"query": "arama terimi"
}
}{{^from}}0{{/from}} — eğer from parametresi yoksa "0" değerini kullanır.
2. Stored Templates — _scripts API
Template'leri Elasticsearch cluster'ında saklayabilirsiniz. Bu sayede client sadece template ID ve parametreleri gönderir.
2.1 Template Kaydetme
PUT _scripts/product_search_v1
{
"script": {
"lang": "mustache",
"source": {
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "{{query}}",
"fields": ["title^3", "description", "brand^2"],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
],
"filter": [
{
"range": {
"price": {
"gte": "{{min_price}}",
"lte": "{{max_price}}"
}
}
}
]
}
},
"from": "{{from}}",
"size": "{{size}}",
"sort": [
{ "{{sort_field}}": { "order": "{{sort_order}}" } }
]
}
}
}2.2 Stored Template Kullanma
POST products/_search/template
{
"id": "product_search_v1",
"params": {
"query": "Samsung telefon",
"min_price": 5000,
"max_price": 20000,
"from": 0,
"size": 10,
"sort_field": "price",
"sort_order": "asc"
}
}2.3 Template'leri Yönetme
// Template'i görüntüle
GET _scripts/product_search_v1
// Template'i sil
DELETE _scripts/product_search_v1
// Template'i güncelle (aynı ID ile PUT)
PUT _scripts/product_search_v1
{
"script": {
"lang": "mustache",
"source": "... güncellenmiş template ..."
}
}2.4 Render Template — Debug
Template'in hangi sorguyu üreteceğini görmek için _render kullanın:
POST _render/template
{
"id": "product_search_v1",
"params": {
"query": "laptop",
"min_price": 10000,
"max_price": 50000,
"from": 0,
"size": 5,
"sort_field": "_score",
"sort_order": "desc"
}
}Bu çıktı gerçek sorguyu gösterir — template'in doğru çalıştığını doğrulamak için kullanın.
3. İleri Template Patterns
3.1 Koşullu Aggregation
PUT _scripts/faceted_search
{
"script": {
"lang": "mustache",
"source": """
{
"query": {
"bool": {
"must": [
{ "multi_match": { "query": "{{query}}", "fields": ["title", "description"] } }
]
{{#category}}
,"filter": [{ "term": { "category": "{{category}}" } }]
{{/category}}
}
},
"size": {{size}}{{^size}}10{{/size}},
{{#include_facets}}
"aggs": {
"categories": { "terms": { "field": "category", "size": 20 } },
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 100 },
{ "from": 100, "to": 500 },
{ "from": 500, "to": 1000 },
{ "from": 1000 }
]
}
},
"avg_price": { "avg": { "field": "price" } }
},
{{/include_facets}}
"from": {{from}}{{^from}}0{{/from}}
}
"""
}
}Kullanım:
// Facet'li arama
POST products/_search/template
{
"id": "faceted_search",
"params": {
"query": "telefon",
"include_facets": true,
"size": 20
}
}
// Facet'siz arama (sayfalama için)
POST products/_search/template
{
"id": "faceted_search",
"params": {
"query": "telefon",
"category": "Elektronik",
"from": 20,
"size": 20
}
}3.2 Multi-Search Template
Birden fazla template sorgusunu tek istekte çalıştırın:
POST _msearch/template
{}
{"id": "product_search_v1", "params": {"query": "telefon", "min_price": 0, "max_price": 50000, "from": 0, "size": 5, "sort_field": "_score", "sort_order": "desc"}}
{}
{"id": "product_search_v1", "params": {"query": "laptop", "min_price": 0, "max_price": 100000, "from": 0, "size": 5, "sort_field": "_score", "sort_order": "desc"}}İlk satır header (boş olabilir), ikinci satır template + params. Her çift bir ayrı sorgu. Dashboard'lar için çok faydalıdır — tek HTTP çağrısıyla birden fazla widget verisini çekersiniz.
4. Async Search API
Normal arama senkrondur: istek gider, sonuç gelene kadar bağlantı açık kalır. Uzun süren sorgularda (heavy aggregation, büyük veri setleri) bu sorun yaratır — timeout, connection drop, kullanıcı deneyimi bozulması.
4.1 Async Search Submit
POST products/_async_search?wait_for_completion_timeout=5s&keep_alive=1m
{
"query": {
"bool": {
"must": [
{ "match": { "description": "akıllı telefon" } }
]
}
},
"aggs": {
"brands": { "terms": { "field": "brand.keyword", "size": 100 } },
"price_stats": { "extended_stats": { "field": "price" } },
"monthly_sales": {
"date_histogram": {
"field": "created_at",
"calendar_interval": "month"
}
}
},
"size": 20
}Parametreler:
| Parametre | Açıklama |
|---|---|
wait_for_completion_timeout | Bu süre içinde tamamlanırsa sonuç döner, yoksa async devam eder |
keep_alive | Sonuçların ne kadar süre saklanacağı |
keep_on_completion | Tamamlansa bile sonuçları sakla (varsayılan: false) |
Yanıt:
{
"id": "FmRldE8zREVEUzA2VGJyUjdSR3JMd3caNkpvQ2RPaFR5U2Vqa0Z3Z3B2dXFSAA==",
"is_partial": true,
"is_running": true,
"start_time_in_millis": 1704067200000,
"expiration_time_in_millis": 1704067260000,
"response": {
"took": 5000,
"timed_out": false,
"num_reduce_phases": 2,
"_shards": { "total": 5, "successful": 3, "skipped": 0, "failed": 0 },
"hits": { "total": { "value": 1523, "relation": "gte" }, "hits": [] }
}
}is_running: true — sorgu hâlâ devam ediyor. is_partial: true — kısmi sonuçlar mevcut (3/5 shard tamamlandı).
4.2 Async Search Get — Sonucu Çekme
GET _async_search/FmRldE8zREVEUzA2VGJyUjdSR3JMd3caNkpvQ2RPaFR5U2Vqa0Z3Z3B2dXFSAA==Yanıt:
{
"id": "FmRldE8zREVEUzA2VGJyUjdSR3JMd3caNkpvQ2RPaFR5U2Vqa0Z3Z3B2dXFSAA==",
"is_partial": false,
"is_running": false,
"response": {
"took": 12450,
"_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 },
"hits": {
"total": { "value": 1523, "relation": "eq" },
"hits": [...]
},
"aggregations": { ... }
}
}is_running: false, is_partial: false — sorgu tamamlandı, tam sonuçlar hazır.
4.3 Async Search Status
Sonuçları indirmeden sadece durumu kontrol etmek için:
GET _async_search/status/FmRldE8zREVEUzA2VGJyUjdSR3JMd3caNkpvQ2RPaFR5U2Vqa0Z3Z3B2dXFSAA==Bu daha hafif bir çağrıdır — sadece is_running, is_partial, shard bilgisi döner, hits/aggregation döndürmez.
4.4 Async Search Delete
İşiniz bittiğinde kaynakları temizleyin:
DELETE _async_search/FmRldE8zREVEUzA2VGJyUjdSR3JMd3caNkpvQ2RPaFR5U2Vqa0Z3Z3B2dXFSAA==4.5 Long-Running Query Pattern
Client Elasticsearch
│ │
├─ POST _async_search ──────►│
│ (wait_for_completion=1s) │
│◄── id + is_running:true ───┤
│ │
├─ GET _async_search/id ────►│ (2 saniye sonra poll)
│◄── is_running:true ────────┤ (hâlâ devam ediyor)
│ │
├─ GET _async_search/id ────►│ (4 saniye sonra poll)
│◄── is_running:false ───────┤ (tamamlandı!)
│ + full results │
│ │
├─ DELETE _async_search/id ──►│ (temizle)
│◄── acknowledged ───────────┤⚠️ Dikkat: keep_alive süresi dolmadan sonuçları çekin, yoksa kaybolur. Tamamlanan async search'ler bellek tüketir — işiniz bitince mutlaka silin.
5. Search Profiler — Sorgu Performans Analizi
5.1 Profile API Kullanımı
GET products/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "title": "samsung telefon" } }
],
"filter": [
{ "range": { "price": { "gte": 5000, "lte": 20000 } } },
{ "term": { "in_stock": true } }
]
}
},
"aggs": {
"brands": { "terms": { "field": "brand.keyword" } }
}
}5.2 Profile Çıktısını Okumak
{
"profile": {
"shards": [
{
"id": "[nodeId][products][0]",
"searches": [
{
"query": [
{
"type": "BooleanQuery",
"description": "+title:samsung +title:telefon #price:[5000 TO 20000] #in_stock:true",
"time_in_nanos": 1542360,
"breakdown": {
"score": 425130,
"build_scorer_count": 8,
"match_count": 0,
"create_weight": 215340,
"next_doc": 612450,
"advance": 89340,
"score_count": 150
},
"children": [...]
}
],
"collector": [
{
"name": "SimpleTopScoreDocCollector",
"reason": "search_top_hits",
"time_in_nanos": 325120
}
]
}
],
"aggregations": [
{
"type": "GlobalOrdinalsStringTermsAggregator",
"description": "brands",
"time_in_nanos": 892310
}
]
}
]
}
}Önemli alanlar:
| Alan | Açıklama |
|---|---|
time_in_nanos | O bileşenin toplam süresi |
breakdown.score | Scoring süresi |
breakdown.next_doc | Doküman iterasyonu süresi |
breakdown.create_weight | Sorgu hazırlık süresi |
collector | Sonuç toplama süresi |
aggregations | Aggregation süresi |
5.3 Kibana Search Profiler
Kibana Dev Tools'ta Search Profiler sekmesi var. Profile çıktısını görsel olarak gösterir — hangi bileşenin ne kadar sürdüğünü bar chart olarak görürsünüz. Komut satırı çıktısından çok daha okunurdur.
6. Java'da Search Template
6.1 Stored Template Oluşturma
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.PutScriptRequest;
// Template kaydetme
client.putScript(r -> r
.id("product_search_v1")
.script(s -> s
.lang("mustache")
.source("""
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "{{query}}",
"fields": ["title^3", "description"],
"fuzziness": "AUTO"
}
}
],
"filter": [
{
"range": {
"price": { "gte": {{min_price}}, "lte": {{max_price}} }
}
}
]
}
},
"from": {{from}},
"size": {{size}}
}
""")
)
);
System.out.println("Template kaydedildi!");6.2 Template ile Arama
import co.elastic.clients.elasticsearch.core.SearchTemplateResponse;
import co.elastic.clients.json.JsonData;
import java.util.Map;
// Stored template ile arama
SearchTemplateResponse<Map> response = client.searchTemplate(r -> r
.index("products")
.id("product_search_v1")
.params("query", JsonData.of("Samsung telefon"))
.params("min_price", JsonData.of(5000))
.params("max_price", JsonData.of(20000))
.params("from", JsonData.of(0))
.params("size", JsonData.of(10)),
Map.class
);
System.out.println("Toplam sonuç: " + response.hits().total().value());
response.hits().hits().forEach(hit -> {
Map<String, Object> source = hit.source();
System.out.printf(" [%.2f] %s - %s TL%n",
hit.score(),
source.get("title"),
source.get("price")
);
});6.3 Render Template (Debug)
import co.elastic.clients.elasticsearch.core.RenderSearchTemplateResponse;
RenderSearchTemplateResponse rendered = client.renderSearchTemplate(r -> r
.id("product_search_v1")
.params("query", JsonData.of("laptop"))
.params("min_price", JsonData.of(10000))
.params("max_price", JsonData.of(50000))
.params("from", JsonData.of(0))
.params("size", JsonData.of(5))
);
System.out.println("Oluşan sorgu:");
System.out.println(rendered.templateOutput().toJson());7. Gerçek Dünya Örneği: E-ticaret Arama Sistemi
Tam bir e-ticaret arama sistemini template'lerle kuralım:
// Template 1: Ana ürün arama
PUT _scripts/ecommerce_search
{
"script": {
"lang": "mustache",
"source": """
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "{{query}}",
"fields": ["title^3", "title.folded^1.5", "description", "brand^2"],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
]
{{#category}}
,"filter": [
{ "term": { "category.keyword": "{{category}}" } }
]
{{/category}}
}
},
{{#include_aggs}}
"aggs": {
"categories": { "terms": { "field": "category.keyword", "size": 20 } },
"brands": { "terms": { "field": "brand.keyword", "size": 30 } },
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "key": "0-500", "to": 500 },
{ "key": "500-2000", "from": 500, "to": 2000 },
{ "key": "2000-5000", "from": 2000, "to": 5000 },
{ "key": "5000+", "from": 5000 }
]
}
}
},
{{/include_aggs}}
"highlight": {
"fields": { "title": {}, "description": { "fragment_size": 150 } }
},
"from": {{from}}{{^from}}0{{/from}},
"size": {{size}}{{^size}}20{{/size}},
"sort": [
{ "{{sort_field}}{{^sort_field}}_score{{/sort_field}}": "{{sort_order}}{{^sort_order}}desc{{/sort_order}}" }
]
}
"""
}
}
// Template 2: Autocomplete
PUT _scripts/ecommerce_suggest
{
"script": {
"lang": "mustache",
"source": """
{
"suggest": {
"product_suggest": {
"prefix": "{{prefix}}",
"completion": {
"field": "suggest",
"size": {{size}}{{^size}}5{{/size}},
"fuzzy": { "fuzziness": 1 }
}
}
}
}
"""
}
}
// Kullanım
POST products/_search/template
{
"id": "ecommerce_search",
"params": {
"query": "akıllı saat",
"include_aggs": true,
"size": 20
}
}
POST products/_search/template
{
"id": "ecommerce_suggest",
"params": { "prefix": "sam", "size": 8 }
}8. Yaygın Hatalar ve Çözümleri
Hata 1: Mustache JSON Escape Sorunu
// ❌ Mustache {{value}} içindeki özel karakterler JSON'ı bozabilir
// Kullanıcı "test "with" quotes" girerse JSON parse hatası
// ✅ Elasticsearch 7.7+ ile {{#toJson}} kullanın
"query": { "match": { "title": {{#toJson}}query{{/toJson}} } }Hata 2: Numeric Değerleri String Olarak Geçmek
// ❌ "from" ve "size" string olarak render olur
"from": "{{from}}" // "from": "0" — string!
// ✅ Tırnak koymayın, Mustache doğrudan değeri yazar
"from": {{from}} // "from": 0 — number!Hata 3: Koşullu Bloklarda Virgül Sorunu
// ❌ Koşullu blok yoksa JSON'da fazladan virgül kalır
{
"must": [...],
{{#category}}
"filter": [...]
{{/category}}
}
// ✅ Virgülü koşullu bloğun İÇİNE alın
{
"must": [...]
{{#category}}
,"filter": [...]
{{/category}}
}Hata 4: Async Search'ü Temizlememek
// ❌ keep_alive süresi dolana kadar bellek tüketir
POST products/_async_search?keep_alive=24h
{ ... }
// Sonucu çektin ama silmedin — 24 saat bellekte!
// ✅ İşin bitince hemen sil
DELETE _async_search/SEARCH_ID9. Best Practices
Template Yönetimi
| Uygulama | Neden |
|---|---|
Template'leri versiyonlayın (_v1, _v2) | Geriye uyumluluğu korursunuz |
_render/template ile test edin | Üretilen sorguyu doğrularsınız |
| Koşullu blokları minimumda tutun | Karmaşık template debug'ı çok zor |
{{#toJson}} helper kullanın | JSON escape sorunlarını önler |
Async Search
| Uygulama | Neden |
|---|---|
wait_for_completion_timeout ayarlayın | Kısa sorgular senkron döner, uzunlar async |
keep_alive'ı kısa tutun (1-5m) | Bellek israfını önler |
| Tamamlanan sonuçları hemen silin | Cluster kaynaklarını korur |
| Kısmi sonuçları kullanıcıya gösterin | UX iyileşir ("150+ sonuç bulundu, yükleniyor...") |
Özet
Search Templates sorguları parametrik şablonlara dönüştürür — DRY prensibi, tek noktadan güncelleme
Mustache syntax'ı basittir:
{{param}}değişken,{{#param}}...{{/param}}koşullu blok,{{#toJson}}JSON dönüşümüStored templates
_scriptsAPI ile cluster'da saklanır — client sadece ID ve params gönderir`_render/template` üretilen sorguyu önizlemenizi sağlar — debug için vazgeçilmez
Async Search uzun süren sorguları arka planda çalıştırır — submit → poll → get → delete döngüsü
Profile API sorgu performansını bileşen bazında ölçer — yavaş kısmı tespit eder
Template versiyonlama (
_v1,_v2) geriye uyumluluğu korur
AI Asistan
Sorularını yanıtlamaya hazır