← Kursa Dön
📄 Text · 30 min

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:

ParametreAçıklama
wait_for_completion_timeoutBu süre içinde tamamlanırsa sonuç döner, yoksa async devam eder
keep_aliveSonuçların ne kadar süre saklanacağı
keep_on_completionTamamlansa 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:

AlanAçıklama
time_in_nanosO bileşenin toplam süresi
breakdown.scoreScoring süresi
breakdown.next_docDoküman iterasyonu süresi
breakdown.create_weightSorgu hazırlık süresi
collectorSonuç toplama süresi
aggregationsAggregation 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_ID

9. Best Practices

Template Yönetimi

UygulamaNeden
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 tutunKarmaşık template debug'ı çok zor
{{#toJson}} helper kullanınJSON escape sorunlarını önler
UygulamaNeden
wait_for_completion_timeout ayarlayınKısa sorgular senkron döner, uzunlar async
keep_alive'ı kısa tutun (1-5m)Bellek israfını önler
Tamamlanan sonuçları hemen silinCluster kaynaklarını korur
Kısmi sonuçları kullanıcıya gösterinUX 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 _scripts API 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