Mert Özen Her Satırda Daha İleri
Veritabanı Elasticsearch ile Arama — #08

Autocomplete ve Search-as-You-Type: edge_ngram, search_as_you_type ve Completion Suggester

Mert Özen 14 Kas 2026 12 dk 10 görüntülenme
Autocomplete ve Search-as-You-Type: edge_ngram, search_as_you_type ve Completion Suggester

Elasticsearch'te yazdıkça öneri için kelime başlarını index'e koyan bir alan gerekir: edge_ngram, search_as_you_type ya da completion suggester; Bookstore için ortadaki kelimeden de öneren ve filtrelenebilen edge_ngram'ı seçtik. Gerçek çıktılarla "kür" ile Kürk Mantolu Madonna, analyzer sırası, bool_prefix'in or tuzağı, completion context'lerinin zorunlu olması, _disk_usage ile alan boyutları ve _source filtreli, collapse'lı, Output Cache'li bir /books/suggest endpoint'i.

Sürüm notu: Bu yazıdaki örnekler Elasticsearch 9.5.6, .NET 10, ASP.NET Core 10 Output Cache ve Elastic.Clients.Elasticsearch 9.5.3 ile, 2. yazıda kurduğumuz lab'da ve 20.035 kitaplık books index'inde çalıştırılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için arama kutusuna öneri ekliyoruz: kullanıcı yazarken altta kitap başlıkları belirecek. Kullanıcı "kür" yazdı. 7. yazıda kurduğumuz /books/search endpoint'i bunu bulur mu? Bulsa bile, her tuşa basışta o endpoint'i çağırmak doğru mu? Bir de şu: "mantolu" yazan, yani başlığın ortasındaki bir kelimeyle başlayan kullanıcıya Kürk Mantolu Madonna önerilmeli mi; Elasticsearch'ün bu iş için sunduğu üç yaklaşımın hangileri bunu yapabilir?

İlk sorunun cevabı hayır: "kür" bir kelime değil, bir kelimenin başı. Arama alanlarındaki term'ler tam kelimeler ("kurk", "kürk"); "kür" hiçbiriyle eşleşmiyor ve fuzziness de üç harften birini değiştirerek "kurk"e ulaşamıyor. Autocomplete için index'te kelimenin başlarının da bulunması gerekiyor.

Üç Yaklaşım ve Mapping'i Yerinde Genişletmek

Elasticsearch'te "yazdıkça öner" için üç yol var. Üçünü de başlığın yeni alt alanları olarak ekleyip karşılaştıracağız:

  • edge_ngram: kendi analyzer'ımız; her kelimenin başlarını ("k", "ku", "kur", "kurk") ayrı term'ler olarak index'liyor. En esnek, sorgu tarafında normal bir match.
  • search_as_you_type: Elasticsearch'ün hazır field tipi; kelime çiftleri, üçlüleri ve başlangıçları için gizli alt alanları kendisi oluşturuyor, bool_prefix tipindeki multi_match ile sorgulanıyor.
  • completion suggester: arama değil öneri için ayrı bir veri yapısı; bellekte tutulan bir FST (finite state transducer; önekten sonuçlara giden sıkıştırılmış bir graf) üzerinde çalışıyor. Çok hızlı ama yalnız baştan eşleşiyor ve filtre yeteneği sınırlı.

Yeni alt alanlar için yeni bir analyzer gerekiyor. 3. yazıda gördüğümüz gibi mapping'deki mevcut bir alan değiştirilemez ama yeni alan ve yeni multi-field eklenebilir. Analyzer eklemek ise index'in kapalı olmasını istiyor. Lab'da Dev Tools'ta, elastic kullanıcısıyla:

POST /books/_close

PUT /books/_settings
{
  "analysis": {
    "filter": {
      "autocomplete_edge": { "type": "edge_ngram", "min_gram": 1, "max_gram": 15 }
    },
    "analyzer": {
      "tr_autocomplete": {
        "type": "custom",
        "tokenizer": "standard",
        "filter": ["apostrophe", "tr_lowercase", "asciifolding", "autocomplete_edge"]
      }
    }
  }
}

POST /books/_open

PUT /books/_mapping
{
  "properties": {
    "title": {
      "type": "text",
      "fields": {
        "autocomplete": { "type": "text", "analyzer": "tr_autocomplete", "search_analyzer": "tr_folded" },
        "sayt": { "type": "search_as_you_type", "analyzer": "tr_folded" },
        "complete": { "type": "completion", "analyzer": "tr_folded" }
      }
    }
  }
}

POST /books/_update_by_query?refresh=true&filter_path=took,total,updated,version_conflicts,failures
{"took":1156,"total":20035,"updated":20035,"version_conflicts":0,"failures":[]}

Yeni alt alanlar yalnız bundan sonra yazılan document'lar için dolu; mevcut 20.035 kitap için _update_by_query'yi gövdesiz çalıştırıyoruz. Bu, her document'ı _source'undan yeniden index'liyor ve yeni alanlar oluşuyor. İki uyarı var. Kapalı index aranamaz; lab'da birkaç saniye sorun değil ama production'da bu bir kesinti, orada yol 11. yazıdaki yeni index ve alias. İkincisi _update_by_query'nin internal versioning kullanması: Dune'un _version'ı 22121'den 22122'ye çıktı. 5. yazıda version'ı SQL Server'ın rowversion'ından veriyorduk; bir sonraki gerçek değişiklik bu sayıdan büyük bir rowversion alacağı için lab'da sorun yok, ama _update_by_query'yi sık çalıştırmak index'teki version'ı gerçek değişikliklerin önüne geçirebilir ve o değişiklik 409 ile "zaten güncel" sayılıp sessizce kaybolur. External version kullanan bir index'te mapping genişletmenin güvenli yolu yine yeni index'e tam yüklemedir. search/books.index.json da aynı alanlarla güncellendi; sıfırdan kurulan index bu adımlara gerek duymuyor.

edge_ngram: Kendi Autocomplete Analyzer'ımız

tr_autocomplete, 4. yazıdaki tr_folded zincirinin sonuna bir edge_ngram filtresi ekliyor:

GET /books/_analyze?filter_path=tokens.token
{ "analyzer": "tr_autocomplete", "text": "Kürk Mantolu" }
{"tokens":[{"token":"k"},{"token":"ku"},{"token":"kur"},{"token":"kurk"},{"token":"m"},{"token":"ma"},{"token":"man"},{"token":"mant"},{"token":"manto"},{"token":"mantol"},{"token":"mantolu"}]}

Sıra önemli. edge_ngram küçük harfe çevirme ve folding'den sonra geliyor; tersini yapsaydık "Kü" gibi büyük harfli ve Türkçe karakterli parçalar index'e girer, "kur" yazan kullanıcı bulamazdı. Arama tarafında ise search_analyzer ngram üretmeyen tr_folded: kullanıcının yazdığı "kür" tek bir term'e ("kur") dönüşüyor ve index'teki "kur" parçasıyla eşleşiyor. Search analyzer'ı belirtmeseydin "kür" de "k", "ku", "kur" diye parçalanır ve "k" ile başlayan her kitap sonuç olurdu.

GET /books/_search?filter_path=took,hits.total.value,hits.hits._score,hits.hits._source.title
{ "size": 3, "_source": ["title"], "query": { "match": { "title.autocomplete": { "query": "kür", "operator": "and" } } } }
{"took":9,"hits":{"total":{"value":1},"hits":[{"_score":14.40775,"_source":{"title":"Kürk Mantolu Madonna"}}]}}

{ "match": { "title.autocomplete": { "query": "kurk ma", "operator": "and" } } }
{"took":4,"hits":{"total":{"value":1},"hits":[{"_score":18.88143,"_source":{"title":"Kürk Mantolu Madonna"}}]}}

{ "match": { "title.autocomplete": { "query": "madonna", "operator": "and" } } }
{"took":2,"hits":{"total":{"value":1},"hits":[{"_score":14.40775,"_source":{"title":"Kürk Mantolu Madonna"}}]}}

{ "match": { "title.autocomplete": { "query": "the sil", "operator": "and" } } }
{"took":3,"hits":{"total":{"value":694},"hits":[{"_score":5.7208166,"_source":{"title":"The Silent Mountain"}},{"_score":5.7208166,"_source":{"title":"The Silent River"}},{"_score":5.7208166,"_source":{"title":"The Silent Harbour"}}]}}

"kür" ve Türkçe karaktersiz "kurk ma" aynı kitabı buluyor; "madonna" da başlığın ortasındaki kelimeden yakalıyor. operator: and burada şart: her yazılan kelime (sonuncusu yarım da olsa) başlıkta bir kelimenin başı olmalı. Bedeli index boyutu: 15 karaktere kadar her önek ayrı bir term. max_gram'ı kısaltmak boyutu düşürür ama bir tuzağı var: index'te o uzunluktan uzun önek kalmaz, arama tarafı ise kelimeyi kesmez. max_gram: 5 ile "mantolu" yazan kullanıcı hiçbir şey bulamaz, çünkü index'teki en uzun parça "manto". Ya max_gram'ı katalogdaki uzun kelimelere göre seçersin ya da search analyzer'ına aynı uzunlukta bir truncate filtresi eklersin.

search_as_you_type

search_as_you_type alanı aynı fikri hazır olarak sunuyor. Mapping'e tek satır eklendi; Elasticsearch arkada title.sayt._2gram, title.sayt._3gram (kelime çiftleri ve üçlüleri) ve title.sayt._index_prefix (bunların önekleri) alanlarını oluşturdu. Sorgu tipi bool_prefix: son kelimeyi önek, öncekileri tam term olarak arıyor.

GET /books/_search?filter_path=took,hits.total.value,hits.hits._score,hits.hits._source.title
{
  "size": 3, "_source": ["title"],
  "query": {
    "multi_match": {
      "query": "kurk ma", "type": "bool_prefix",
      "fields": ["title.sayt", "title.sayt._2gram", "title.sayt._3gram"]
    }
  }
}
{"took":4,"hits":{"total":{"value":1658},"hits":[{"_score":12.862545,"_source":{"title":"Kürk Mantolu Madonna"}},{"_score":1.0,"_source":{"title":"A Library in Map 67"}},{"_score":1.0,"_source":{"title":"Mountain of the Distant Map"}}]}}

"operator": "and"
{"took":3,"hits":{"total":{"value":1},"hits":[{"_score":12.862545,"_source":{"title":"Kürk Mantolu Madonna"}}]}}

Varsayılan or ile "ma" öneki "Map" geçen 1.657 başka kitabı da getirdi; sıralama doğru olsa bile toplam sayı ve ikinci sıradan sonrası anlamsız. operator: and ile sonuç edge_ngram'inkiyle aynı. Kelime çifti alanları, kelimeleri yazıldığı sırayla eşleşen başlığa daha yüksek skor veriyor; "kurk ma" için 12,86'ya karşı 1,0 farkı oradan geliyor. Bu tipin avantajı ayar gerektirmemesi; dezavantajı birazdan göreceğimiz index boyutu ve analyzer zincirinin edge_ngram kadar kontrol edilebilir olmaması.

completion Suggester

completion alanı farklı çalışıyor: sorgu query ile değil suggest ile yapılıyor ve önek, bellekteki bir FST üzerinde aranıyor:

GET /books/_search?filter_path=took,suggest.*.options.text
{ "_source": false, "suggest": { "titles": { "prefix": "kür", "completion": { "field": "title.complete", "size": 3, "skip_duplicates": true } } } }
{"took":2,"suggest":{"titles":[{"options":[{"text":"Kürk Mantolu Madonna"}]}]}}

"prefix": "the sil"
{"took":3,"suggest":{"titles":[{"options":[{"text":"The Silent Bridge"},{"text":"The Silent City"},{"text":"The Silent Clock"}]}]}}

"prefix": "mantolu"
{"took":1}

"prefix": "kurk mantlu", "completion": { "field": "title.complete", "size": 3, "fuzzy": { "fuzziness": 1 } }
{"suggest":{"titles":[{"options":[{"text":"Kürk Mantolu Madonna"}]}]}}

"mantolu" için öneri yok (seçenek listesi boş olduğu için filter_path hepsini eledi): completion yalnız başlığın başından eşleşiyor. Ortadaki kelimelerden öneri istiyorsan her kitap için birden çok input vermen gerekiyor ("Kürk Mantolu Madonna", "Mantolu Madonna", "Madonna"); bu da veriyi index'e yazan kodu karmaşıklaştırıyor. skip_duplicates aynı başlıklı sentetik kitapları tekilleştiriyor, fuzzy yazım hatasına izin veriyor.

Completion'ın kategori filtresi context ile yapılıyor. Önce ayrı bir books-ac-test index'inde denedim, çünkü bir alana context eklemenin bedeli var:

PUT /books-ac-test
{
  "mappings": {
    "properties": {
      "categories": { "type": "keyword" },
      "title": { "type": "text", "fields": { "complete": { "type": "completion",
        "contexts": [ { "name": "category", "type": "category", "path": "categories" } ] } } }
    }
  }
}

GET /books-ac-test/_search
{ "suggest": { "titles": { "prefix": "the sil", "completion": { "field": "title.complete" } } } }
{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"Missing mandatory contexts in context query"}], ...},"status":400}

{ "suggest": { "titles": { "prefix": "the sil", "completion": { "field": "title.complete", "contexts": { "category": ["Mystery"] } } } } }
{"suggest":{"titles":[{"options":[{"text":"The Silent Clock"},{"text":"The Silent Garden"}]}]}}

Context tanımlı bir completion alanında her sorgu context vermek zorunda; "bütün kategoriler" diye bir seçenek yok, ya hepsini listelersin ya da context'siz ikinci bir alan eklersin. Bunu canlı index'te deneseydim, mapping'den context'i geri almanın yolu olmayacaktı. Bookstore'un ana alanını context'siz bıraktık.

Index Boyutu: _disk_usage ile Ölçmek

Üç yaklaşımın bedelini _disk_usage API'si alan alan gösteriyor. Ölçümden önce index'i tek segment'e indirdim (_forcemerge?max_num_segments=1); karşılaştırma için alanlar eklenmeden önce de aynı ölçümü almıştım:

POST /books/_disk_usage?run_expensive_tasks=true&filter_path=books.store_size,books.all_fields.total,books.fields.title*.total

# alanlar eklenmeden önce
{"books":{"store_size":"5mb","all_fields":{"total":"4.9mb"},"fields":{"title":{"total":"149kb"},"title.en":{"total":"123.6kb"},"title.folded":{"total":"148.9kb"},"title.keyword":{"total":"238kb"},"title.sort":{"total":"296.8kb"},"title.tr":{"total":"148.9kb"}}}}

# _update_by_query ve _forcemerge sonrası
{"books":{"store_size":"13.7mb","all_fields":{"total":"13.4mb"},"fields":{"title":{"total":"290.5kb"},"title.autocomplete":{"total":"604.4kb"},"title.complete":{"total":"681.7kb"},"title.en":{"total":"240.4kb"},"title.folded":{"total":"290.5kb"},"title.keyword":{"total":"347.4kb"},"title.sayt":{"total":"168.2kb"},"title.sayt._2gram":{"total":"212.1kb"},"title.sayt._3gram":{"total":"192.3kb"},"title.sayt._index_prefix":{"total":"2.1mb"},"title.sort":{"total":"406.2kb"},"title.tr":{"total":"290.3kb"}}}}

İlk dikkat çeken, hiç dokunmadığımız title'ın bile 149 KB'tan 290 KB'a çıkması. Sebep _update_by_query: her kitabın eski kopyası silinmiş olarak işaretlendi ama Elasticsearch, replica'ların kurtarılması için silinen kopyaları bir süre (varsayılan retention lease 12 saat) tutuyor; _stats 40.076 silinmiş Lucene document'ı gösterdi (kitaplar ve nested yazarlar). Bu yüzden toplam boyutu değil, yeni alanları eski title.folded'ın 148,9 KB'ı ile karşılaştırmak doğru: edge_ngram alanı yaklaşık 4 katı (604 KB), completion yaklaşık 4,6 katı (682 KB), search_as_you_type ise gizli alanlarıyla birlikte 2,6 MB, yani 18 katı. En büyük kalem önek alanı. Kısa başlıklarda bu sayılar küçük; aynı oran milyonlarca kitabın açıklamasına uygulansa index boyutu katlanır. Bu yüzden autocomplete yalnız kısa alanlara (başlık, yazar adı) uygulanır.

ASP.NET Core Endpoint'i: /books/suggest

Bookstore için edge_ngram'i seçtik: ortadaki kelimelerden de öneriyor, normal bir sorgu olduğu için kategori filtresi ve 6. yazıdaki her şey çalışıyor ve boyutu kabul edilebilir. Endpoint'i /books/search'ten ayrı tuttuk, çünkü ihtiyacı farklı: highlight yok, toplam sayı yok, yalnız birkaç başlık.

app.MapGet("/books/suggest", async (string? q, string? category, ElasticsearchClient client, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
    // Bir iki harf binlerce başlıkla eşleşir; öneriye ikinci karakterden başla.
    if (string.IsNullOrWhiteSpace(q) || q.Trim().Length < 2)
    {
        return Results.Ok(Array.Empty<BookSuggestion>());
    }

    List<Query> filters = [];
    if (!string.IsNullOrWhiteSpace(category))
    {
        filters.Add(new TermQuery { Field = "categories", Value = category });
    }

    // Tarayıcı isteği iptal edince (yeni bir tuş) token iptal olur; Elasticsearch çağrısı da durur.
    SearchResponse<Book> response = await client.SearchAsync<Book>(s => s
        .Size(MaxSuggestions)
        .Source(new SourceConfig(new SourceFilter { Includes = Fields.FromStrings(["isbn", "title"]) }))
        .TrackTotalHits(new TrackHits(false))
        // Katalogda aynı başlıklı birden çok kitap var; her başlığı bir kez göster.
        .Collapse(new FieldCollapse { Field = "title.keyword" })
        .Query(new BoolQuery
        {
            Must = [new MatchQuery { Field = "title.autocomplete", Query = q, Operator = Operator.And }],
            Should = [new MatchPhrasePrefixQuery { Field = "title.folded", Query = q }],
            Filter = filters
        }), cancellationToken);
    ...
    return Results.Ok(response.Hits.Select(h => new BookSuggestion(h.Source!.Isbn, h.Source.Title)).ToList());
})
.CacheOutput(CachePolicy);

Her satırın bir sebebi var. _source filtresi Elasticsearch'ün göndereceği veriyi küçültüyor: aynı sekiz sonuçlu "the sil" sorgusunun cevabı filtresiz 3.273 bayt, _source: ["isbn", "title"] ile 1.155 bayt. Her tuşa basışta çağrılan bir endpoint için bu fark doğrudan gecikme ve bant genişliği. TrackTotalHits(false) toplam sayımı kapatıyor; öneri listesinde sayıya ihtiyaç yok. collapse, sentetik katalogdaki aynı başlıklı kitapları tek satıra indiriyor; ilk denemede "The Silent River" listede iki kez çıkmıştı. match_phrase_prefix'li should, kelimeleri başlıktaki sırayla yazan kullanıcıya ek puan veriyor. Endpoint hata durumunda boş liste dönüyor: öneri kutusunun hata mesajı göstermesi kullanıcıya bir şey kazandırmaz.

Output Cache aynı q ve category için cevabı 30 saniye saklıyor; popüler önekler ("ha", "the", "kü") binlerce kullanıcı tarafından aynı şekilde yazılıyor:

// Autocomplete cevapları herkes için aynı: kısa süre cache'le (8. yazı).
builder.Services.AddOutputCache(options => options.AddPolicy(BookSuggestEndpoints.CachePolicy, policy => policy
    .Expire(TimeSpan.FromSeconds(30))
    .SetVaryByQuery("q", "category")));

WebApplication app = builder.Build();

app.UseOutputCache();
curl "http://localhost:5080/books/suggest?q=k%C3%BCr"
[{"isbn":"9781000000030","title":"Kürk Mantolu Madonna"}]

curl -s -w '\n%{size_download} bytes\n' "http://localhost:5080/books/suggest?q=the%20sil"
[{"isbn":"9782000091639","title":"The Silent Mountain"},{"isbn":"9782000091882","title":"The Silent River"},{"isbn":"9782000092346","title":"The Silent Harbour"},{"isbn":"9782000092759","title":"The Silent Promise"},{"isbn":"9782000092797","title":"The Silent Library"},{"isbn":"9782000095279","title":"The Silent Kingdom"},{"isbn":"9782000095750","title":"The Silent Window"},{"isbn":"9782000099024","title":"The Silent Forest"}]
430 bytes

curl "http://localhost:5080/books/suggest?q=the%20sil&category=Mystery"
[{"isbn":"9782000091882","title":"The Silent River"},{"isbn":"9782000098614","title":"The Silent Kingdom"},{"isbn":"9782000101598","title":"The Silent Clock"},{"isbn":"9782000102397","title":"The Silent Window"},{"isbn":"9782000104957","title":"The Silent Road"},{"isbn":"9782000105893","title":"The Silent Library"},{"isbn":"9782000115243","title":"The Silent House"},{"isbn":"9782000122616","title":"The Silent Voyage"}]

curl "http://localhost:5080/books/suggest?q=madonna"
[{"isbn":"9781000000030","title":"Kürk Mantolu Madonna"}]

curl "http://localhost:5080/books/suggest?q=k"
[]

"the sil" isteğini iki kez daha gönderdim; API log'unda Elasticsearch'e giden her istek için yazılan satırlardan yalnız beş tane var: "kür", "kurk ma", "the sil", kategorili "the sil" ve "madonna". Tekrarlanan iki istek cache'ten döndü. Caching serisinin "ASP.NET Core Output Cache Derinlemesine: Policy'ler, Vary-By, Tag ile Eviction, Resource Locking ve Redis Store" yazısı policy'leri ve birden çok sunucuda Redis store kullanımını anlatıyor.

İstemci tarafında iki kural var. Birincisi debounce: her tuşta değil, kullanıcı 200–300 ms durduğunda istek at. İkincisi iptal: yeni bir istek gittiğinde öncekini iptal et ki geç gelen eski bir cevap yenisinin üstüne yazmasın. Angular'da bu debounceTime ve switchMap ikilisi; RxJS serisinin "Zamanla Çalışan Operator'ler ve Scheduler'lar: debounceTime, throttleTime, auditTime, timeout ve Polling" ile "Higher-order Mapping: switchMap, mergeMap, concatMap ve exhaustMap Arasında Doğru Seçim" yazıları ikisini ayrıntılı anlatıyor. switchMap eski HTTP isteğini iptal ettiğinde ASP.NET Core RequestAborted'ı tetikliyor; endpoint'teki CancellationToken bunu Elasticsearch client'ına taşıyor ve gereksiz sorgu orada da kesiliyor.

Üretimde Dikkat

Autocomplete en çok çağrılan endpoint'in olacak. Her kullanıcı bir arama için 3–5 öneri isteği üretiyor; kapasite planını arama değil öneri trafiğine göre yap ve took değerini ayrı bir metric olarak izle.

Önerileri arama sonuçlarıyla tutarlı tut. Öneride görünen bir kitap, kullanıcı Enter'a bastığında arama sonucunda da olmalı. Bizim kurgumuzda iki endpoint de aynı index'i ve aynı analiz zincirini (tr_folded) kullanıyor; farklı bir öneri kaynağı (ayrı bir tablo, ayrı bir index) bu tutarlılığı kaybettirebilir.

Cache süresini kısa tut. Fiyatı ya da stoğu değişen kitap önerinin içeriğini değiştirmiyor ama yeni eklenen ya da satıştan kalkan kitap değiştiriyor; 30 saniye, 5. yazıdaki senkronizasyon gecikmesiyle aynı mertebede.

Production'da mapping'i yerinde genişletme. Bu yazıdaki close/open adımları index'i kapatıyor ve _update_by_query external version'larla çakışabiliyor; 11. yazıdaki yeni index ve alias yolu her ikisinden de kaçınıyor.

Sık Düşülen Tuzaklar

Her tuşa basışta tam arama sorgusu çalıştırmak. Highlight, suggester, toplam sayım ve function_score içeren bir sorgu öneri için gereksiz pahalı; öneri için ayrı, küçük bir sorgu yaz.

Ngram'ı search tarafında da uygulamak. search_analyzer verilmezse kullanıcının yazdığı da öneklere bölünür ve "k" ile başlayan her başlık eşleşir.

edge_ngram'i lowercase ve folding'den önce koymak. Index'e büyük harfli ve Türkçe karakterli parçalar girer; "kur" yazan "Kürk"ü bulamaz.

Ngram'ı uzun alanlara uygulamak. Açıklama gibi alanlarda boyut katlanır, üstelik autocomplete için gereksizdir; yalnız başlık ve yazar adı gibi kısa alanlara uygula.

bool_prefix'i or ile bırakmak. Son kelimenin öneki yüzlerce alakasız document getirir; toplam sayı ve sıralamanın alt kısmı anlamsızlaşır.

Completion alanına sonradan geri alınamayacak context eklemek. Context zorunlu hâle gelir; önce ayrı bir index'te dene.

Küçük Bir Deneme

★☆☆ /books/suggest'e yazar önerisi ekle: authors.name için de bir autocomplete alt alanı tanımla (nested olduğunu unutma) ve "sabah" yazınca Sabahattin Ali'nin kitaplarını getir.

★★☆ tr_autocomplete'in max_gram değerini 15'ten 8'e indirerek books-ac adında ayrı bir index kur, aynı veriyi _reindex ile kopyala ve _disk_usage ile title.autocomplete'in boyutunu karşılaştır. "saatleri ayarlama" yazan kullanıcı ne görüyor?

★★★ Completion suggester'a dayanan ikinci bir endpoint yaz (/books/suggest/fast) ve iki endpoint'i 1.000 rastgele önekle çağıran küçük bir yük testi yap. took ve uçtan uca süre farkı ne kadar? Ortadaki kelimeden öneri kaybı buna değer mi?

Bir sonraki yazıda arama sayfasının sol tarafına geçiyoruz: kategori, fiyat aralığı, dil ve puan facet'leri. Aggregation'larla sayıları aynı istekte hesaplayacak, seçili bir filtrenin kendi facet'indeki sayıları bozmaması için post_filter'ı kullanacak ve terms aggregation'ın sayılarının neden yaklaşık olabildiğini birden çok shard'lı bir index'te göreceğiz.