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

Relevance Tuning: Field Boost, function_score, Fuzzy Arama, Highlight ve "Bunu mu Demek İstediniz?"

Mert Özen 14 Kas 2026 14 dk 12 görüntülenme
Relevance Tuning: Field Boost, function_score, Fuzzy Arama, Highlight ve "Bunu mu Demek İstediniz?"

Arama sıralamasını tahminle değil, sorgu ve beklenen sonuçlardan oluşan bir setle _rank_eval API'sinde ölçerek ayarlarsın. Gerçek çıktılarla nDCG@5'i 0,905'ten 0,979'a çıkaran yazar boost'u, fuzziness ve phrase clause'u; ölçünce bozan tie_breaker ve sert bir function_score; rescore, html encoder'lı highlight ile XSS'e karşı güvenli render ve phrase suggester ile "bunu mu demek istediniz?".

Sürüm notu: Bu yazıdaki örnekler Elasticsearch 9.5.6, .NET 10 ve Elastic.Clients.Elasticsearch 9.5.3 ile, 2. yazıda kurduğumuz lab'da ve 5. yazının sonundaki 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 yazdığımız /books/search endpoint'i, 4. yazıda tahminle verdiğimiz alan ağırlıklarıyla çalışıyor: başlık ^3, Türkçe ve Türkçe karaktersiz başlık ^2, geri kalanı 1. Ürün ekibinden üç öneri geliyor:

  • Yazar eşleşmesinin ağırlığını 3 katına çıkaralım.
  • Yazım hatalarına tolerans için her alanda fuzziness: AUTO açalım.
  • Puanı yüksek ve yeni çıkmış kitaplar öne gelsin (function_score).

Hangisi aramayı iyileştirir, hangisi bozar? Daha önemlisi: bunu nasıl bileceksin? "Birkaç arama yaptım, iyi görünüyor" bir ölçüm değil. Bu yazı önce ölçmeyi kuruyor, sonra her değişikliği o ölçümle karar veriyor.

Önce Ölç: Relevance Seti ve _rank_eval

Relevance'ı ölçmek için iki şeye ihtiyaç var: gerçek aramalardan seçilmiş sorgular ve her sorgu için "hangi kitaplar, ne kadar iyi sonuç" yargısı. Bookstore için bunu repo'da search/relevance/judgments.json olarak tutuyoruz. 20 sorgu var; Türkçe ve İngilizce aramalar, yazar aramaları, Türkçe karaktersiz yazımlar ve bir yazım hatası:

[
  { "query": "ince memed", "ratings": { "9781000000016": 3, "9781000000023": 2 } },
  { "query": "sabahattin ali", "ratings": { "9781000000030": 2, "9781000000047": 2, "9781000000054": 2 } },
  { "query": "the hobbit", "ratings": { "9781000000252": 3, "9781000000207": 2 } },
  { "query": "jane austen", "ratings": { "9781000000238": 2, "9781000000245": 2 } },
  { "query": "suç ve ceza", "ratings": { "9781000000177": 3, "9781000000306": 2 } },
  { "query": "madona", "ratings": { "9781000000030": 3 } },
  ...
]

3 "aranan kitabın kendisi", 2 "çok ilgili" demek; listede olmayan her kitap 0 sayılıyor. Gerçek bir sistemde bu sorgular arama loglarından gelir: en sık 100 sorgu, sıfır sonuçlu sorgular ve müşteri hizmetlerine şikâyet olarak gelenler. Yargıları ise kataloğu tanıyan biri (Bookstore'da Product Owner) verir.

Elasticsearch'ün _rank_eval API'si bu seti alıp her sorguyu çalıştırıyor ve sonucu bir metric ile puanlıyor. Metric olarak nDCG@5 (normalized discounted cumulative gain; ilk beş sonucu, iyi sonuçlar üstte olduğunda daha çok puan vererek değerlendiren, 0 ile 1 arası bir ölçü) kullanıyoruz; API bunun yanında precision, recall, MRR (mean reciprocal rank; ilk doğru sonucun sırasına göre puan veren ölçü) ve expected reciprocal rank metric'lerini de destekliyor. Önemli karar şu: _rank_eval'e endpoint'in çalıştırdığı sorgunun aynısını vermek. Bu yüzden sorguyu kuran kodu API'den Bookstore.Search'teki BookSearchQuery'ye taşıdık ve sıralamayı etkileyen her ayarı tek bir SearchTuning record'unda topladık. Lab'daki rank-eval komutu:

RankEvalRequest request = new()
{
    Indices = SearchClientFactory.BooksIndex,
    Metric = new RankEvalMetric { Dcg = new RankEvalMetricDiscountedCumulativeGain { K = 5, Normalize = true } },
    Requests = judgments.Select(j => new RankEvalRequestItem
    {
        Id = j.Query,
        Request = new RankEvalQuery { Query = BookSearchQuery.Build(new BookSearchFilter(j.Query), tuning), Size = 5 },
        Ratings = j.Ratings.Select(r => new DocumentRating { Index = SearchClientFactory.BooksIndex, Id = r.Key, Rating = r.Value }).ToList()
    }).ToList()
};

RankEvalResponse response = await LabClient.Create().RankEvalAsync(request);

6. yazının sorgusuyla (SearchTuning.Baseline) ilk ölçüm:

dotnet run --project src/Bookstore.SearchLab -- rank-eval
nDCG@5 0.905
  0.000  madona                     top:
  0.693  jane austen                top: 9781000000313(-), 9781000000238(2), 9781000000245(2)
  0.787  crime and punishment       top: 9781000000306(3), 9781000000245(-), 9781000000320(-)
  0.787  suç ve ceza                top: 9781000000177(3), 9781000000184(-), 9781000000023(-)
  0.834  the hobbit                 top: 9781000000207(2), 9781000000252(3), 9781000000320(-)

Komut yalnız tam puan almayan sorguları ve ilk üç sonucu yazdırıyor; (-) setinde yargısı olmayan bir kitap. Tablo şimdiden çok şey söylüyor. "madona" hiçbir şey bulmuyor. "jane austen" için ilk sırada Jane Eyre var: başlıktaki "jane" kelimesi ^3 ile yazarın adını geçiyor. "the hobbit"te Türkçe Hobbit İngilizce baskının önünde, çünkü tek kelimelik başlık daha kısa ve "the" sentetik kitapların yüzlercesinde geçtiği için neredeyse puan getirmiyor. "suç ve ceza" ile "crime and punishment" ise aynı eserin diğer dildeki baskısını bulamıyor; bu metin eşleşmesiyle çözülecek bir sorun değil, bir eş anlamlı kuralı ya da iki baskıyı bağlayan bir "eser" alanı gerektiriyor. Setin işi tam da bu: neyin ayarla, neyin modelle çözüleceğini ayırmak.

Ölçerek Ayarlamak: Boost, tie_breaker ve Phrase

rank-eval komutu her ayarı parametre olarak alıyor; her denemede tek bir şeyi değiştirdim:

DenemenDCG@5Ne oldu
Baseline (6. yazı)0.905Yukarıdaki beş zayıf sorgu
--tie 0.30.892"sabahattin ali"de Yaprak Dökümü (açıklamada "Ali Rıza Bey") öne geçti
--type most_fields0.892Aynı bozulma
--author-boost 30.920"jane austen" düzeldi
--fuzzy AUTO0.955"madona" Kürk Mantolu Madonna'yı buldu
--phrase 20.913"the hobbit" düzeldi
--author-boost 3 --fuzzy AUTO --phrase 20.979Yalnız iki dil arası sorgu kaldı

tie_breaker ve most_fields kâğıt üstünde mantıklı görünüyordu, ölçünce kötüleştirdi: açıklamadaki eşleşmelerin skora katılması, yazar aramasında açıklamasında aynı isim geçen kitapları öne itti. İki iyileştirme eklenen birer should clause'u. Yazar sorgusunun boost'u 3 oldu ve Türkçe karaktersiz yazar adı (authors.name.folded) eklendi; bu ikincisi setteki puanı değiştirmedi ama "yasar kemal" yazan kullanıcının "kemal" yerine iki kelimeyle de eşleşmesini sağlıyor. Phrase clause'u ise kullanıcının kelimeleri başlıkta aynı sırayla geçiyorsa ek puan veriyor, hiçbir sonucu dışarıda bırakmıyor:

if (tuning.PhraseBoost is float phraseBoost)
{
    // Kullanıcının kelimeleri başlıkta aynı sırayla: zorunlu değil, ek bir should clause'u.
    clauses.Add(new MatchPhraseQuery { Field = "title.folded", Query = text, Slop = 1, Boost = phraseBoost });
}

Sonuç SearchTuning.Tuned olarak koda geçti ve endpoint artık bunu kullanıyor:

// rank-eval ile ölçüldü (7. yazı): search/relevance/judgments.json'da nDCG@5 0.905 -> 0.979.
public static SearchTuning Tuned { get; } = Baseline with
{
    AuthorFields = ["authors.name", "authors.name.folded"],
    AuthorBoost = 3,
    Fuzziness = "AUTO",
    PhraseBoost = 2,
    RatingAndRecency = true
};

20 sorgu küçük bir set; 0,979'u "arama yüzde 98 doğru" diye okuma. Değeri, bir değişikliğin hangi sorguyu bozduğunu anında göstermesinde. RAG (retrieval-augmented generation; dil modeline cevap ürettirmeden önce ilgili belgeleri bulup veren yaklaşım) serisinin "RAG'i Ölçmek: Recall@k, MRR, nDCG, Faithfulness ve Groundedness" yazısı bu metric'leri ve aralarındaki farkı ayrıntılı anlatıyor.

Fuzzy Arama: fuzziness, AUTO ve prefix_length

fuzziness, sorgudaki her term için belirli sayıda karakter değişikliğine (ekleme, silme, değiştirme, yan yana iki harfin yer değiştirmesi) izin veriyor. AUTO bu sayıyı term'in uzunluğuna göre seçiyor: 1–2 karakterde tam eşleşme, 3–5 karakterde bir değişiklik, daha uzunlarda iki. prefix_length: 1 ilk harfin doğru yazıldığını varsayıyor; aday term sayısını ve dolayısıyla maliyeti ciddi biçimde düşürüyor.

dotnet run --project src/Bookstore.SearchLab -- search madona
"madona": 0 hits
dotnet run --project src/Bookstore.SearchLab -- search madona --fuzzy AUTO
"madona": 1 hits
   25.70  Kürk Mantolu Madonna (tr, 4.80, 2019)

dotnet run --project src/Bookstore.SearchLab -- search dostoyevsky
"dostoyevsky": 0 hits
dotnet run --project src/Bookstore.SearchLab -- search dostoyevsky --fuzzy AUTO
"dostoyevsky": 2 hits
    8.64  Suç ve Ceza (tr, 4.80, 2022)
    8.55  Crime and Punishment (en, 4.70, 2019)

Yazarın adı Türkçe baskıda "Dostoyevski", İngilizce baskıda "Dostoevsky"; "dostoyevsky" ikisine de bir harf uzaklıkta ve iki baskıyı birden buldu. Türkçe karakter hataları için fuzziness'a güvenme: "kurk" ile "kürk" bir değişiklik uzaklıkta ama bunu zaten 4. yazıdaki folded alt alanı bedavaya çözüyor; fuzzy, gerçek yazım hataları için kalmalı.

Fuzzy'nin bedeli alakasız eşleşmeler. Beş harflik "yaban" artık iki kitap buluyor:

dotnet run --project src/Bookstore.SearchLab -- search yaban --fuzzy AUTO
"yaban": 2 hits
   41.58  Yaban (tr, 4.10, 2016)
    7.36  Dokuzuncu Hariciye Koğuşu (tr, 4.30, 2015)

İkinci kitabın açıklamasında "hastanede yatan" geçiyor; "yatan" ile "yaban" arasında bir harf var. Skor farkı büyük olduğu için burada zararsız, ama kısa kelimelerin çok olduğu alanlarda (kategori adları, kodlar) fuzzy açmak sonuç listesini anlamsız kelimelerle doldurur. Bir kısıt daha: cross_fields tipi fuzziness kabul etmiyor; denediğimde Fuzziness not allowed for type [cross_fields] hatasıyla 400 döndü.

function_score: Puan ve Yenilik Sinyali

Metin eşleşmesi eşit olan kitaplardan hangisi önce gelmeli? Bookstore'un index'inde satış sayısı yok ama ortalama puan (averageRating) ve yayın tarihi (publishedOn) var. function_score, metin skorunu bu alanlardan hesaplanan çarpanlarla değiştirmeye yarıyor. İlk denemem her iki fonksiyonu çarpan olarak birleştirdi (score_mode: multiply) ve yeniliği beş yıllık bir Gauss eğrisiyle hesapladı. Sonuç 0,979'dan 0,962'ye düştü: 2023 tarihli Türkçe Hobbit, 2020 tarihli İngilizce baskıyı "the hobbit" aramasında yine geçti. Yenilik çarpanı eski kitapları neredeyse sıfırla çarpıyordu. Son hâli daha yumuşak:

// Metin skoru ana sinyal kalıyor; puan ve yenilik yalnız hafifçe kaydırıyor.
private static Query WithRatingAndRecency(Query textQuery) => new FunctionScoreQuery
{
    Query = textQuery,
    Functions =
    [
        new FunctionScore
        {
            FieldValueFactor = new FieldValueFactorScoreFunction
            {
                Field = "averageRating",
                Modifier = FieldValueFactorModifier.Log1p,
                Missing = 3
            }
        },
        new FunctionScore
        {
            Weight = 0.5,
            Gauss = new DateDecayFunction
            {
                Field = "publishedOn",
                Placement = new DecayPlacement<DateMath, Duration> { Origin = DateMath.Now, Scale = "3650d", Offset = "365d", Decay = 0.5 }
            }
        }
    ],
    ScoreMode = FunctionScoreMode.Sum,
    BoostMode = FunctionBoostMode.Multiply
};

Puan fonksiyonu log1p ile sıkıştırılıyor: 2,5 puanlı bir kitapla 5 puanlı bir kitabın çarpanı 0,54 ile 0,78 arasında kalıyor. Yenilik son bir yılda tam puan, sonra on yılda yarıya iniyor ve weight: 0.5 ile katkısı sınırlanıyor. İki fonksiyon toplanıp metin skoruyla çarpılıyor. Bu hâliyle set yine 0,979 verdi; yani belirli bir kitabı arayan sorguları bozmuyor. Faydası, metin eşleşmesinin eşit olduğu geniş aramalarda görünüyor:

dotnet run --project src/Bookstore.SearchLab -- search silent garden --author-boost 3 --fuzzy AUTO --phrase 2
"silent garden": 5382 hits
   33.44  The Silent Garden (en, 4.00, 2015)
   33.44  The Silent Garden (en, 3.90, 2024)
   33.44  The Silent Garden (en, 4.80, 2021)
   33.44  The Silent Garden (en, 3.10, 2025)
   33.44  The Silent Garden (en, 3.00, 2005)

dotnet run --project src/Bookstore.SearchLab -- search silent garden --tuned
"silent garden": 5382 hits
   42.19  The Silent Garden (en, 4.90, 2024)
   40.39  The Silent Garden (en, 4.80, 2021)
   39.55  The Silent Garden (en, 3.90, 2024)
   37.18  The Silent Garden (en, 3.10, 2025)
   34.10  The Silent Garden (en, 3.80, 2018)

Sinyalsiz sorguda aynı başlıklı beş kitap eşit skorla, index'teki sıraya göre geliyor. Sinyallerle yüksek puanlı ve yeni olanlar öne çıkıyor; 2005 tarihli 3,0 puanlı kitap ilk beşten düştü. Satış sayısı gibi bir alan eklenirse aynı field_value_factor kalıbıyla, yine log1p gibi bir sıkıştırmayla eklenmeli; ham satış sayısıyla çarpmak çok satan bir kitabı her aramanın başına taşır.

rescore: Pahalı Sinyali Yalnız İlk Sonuçlara Uygulamak

Phrase eşleşmesini her document için hesaplamak, büyük index'lerde pahalı olabilir. rescore, ana sorgunun ilk N sonucunu (shard başına window_size) ikinci bir sorguyla yeniden puanlıyor. Aynı "the hobbit" sorununu Dev Tools'ta böyle çözdük:

GET /books/_search?filter_path=hits.hits._score,hits.hits._source.title,hits.hits._source.language
{
  "size": 3,
  "_source": ["title", "language"],
  "query": { "multi_match": { "query": "the hobbit", "fields": ["title^3", "title.tr^2", "title.folded^2", "title.en", "description", "description.tr", "description.folded", "description.en"] } },
  "rescore": {
    "window_size": 50,
    "query": {
      "rescore_query": { "match_phrase": { "title.folded": { "query": "the hobbit", "slop": 1 } } },
      "query_weight": 1,
      "rescore_query_weight": 2
    }
  }
}
{"hits":{"hits":[{"_score":60.25035,"_source":{"title":"The Hobbit","language":"en"}},{"_score":39.341927,"_source":{"title":"Hobbit","language":"tr"}},{"_score":7.362587,"_source":{"title":"Wuthering Heights","language":"en"}}]}}

# rescore olmadan, aynı sorgu
{"hits":{"hits":[{"_score":39.341927,"_source":{"title":"Hobbit","language":"tr"}},{"_score":36.15021,"_source":{"title":"The Hobbit","language":"en"}},{"_score":7.362587,"_source":{"title":"Wuthering Heights","language":"en"}}]}}

Biz should clause'unu seçtik çünkü _rank_eval isteğine yalnız bir sorgu verilebiliyor ve 20 bin kitapta maliyet önemsiz. Milyonlarca document'ta, ya da rescore sorgusu bir script veya pahalı bir phrase sorgusu olduğunda rescore doğru araç. Yalnız window_size içindeki sonuçların değiştiğini unutma: pencerenin dışında kalan bir kitap, rescore ne kadar puan verirse versin öne gelemez.

Highlight ve Güvenli Render

Sonuç sayfasında eşleşen kelimeyi işaretlemek için highlight kullanıyoruz. Sorgumuz başlığın folded ve tr alt alanlarında eşleşiyor ama kullanıcıya gösterilen metin title; matched_fields alt alanlardaki eşleşmeleri ana alanın metnine taşıyor (9.5'te varsayılan unified highlighter ile çalıştı):

private static Highlight BuildHighlight() => new()
{
    Encoder = HighlighterEncoder.Html,
    PreTags = ["<mark>"],
    PostTags = ["</mark>"],
    Fields = new Dictionary<Field, HighlightField>
    {
        // Sorgu title.folded / title.tr'de de eşleşiyor; matched_fields bu eşleşmeleri asıl başlığa taşır.
        ["title"] = new HighlightField { NumberOfFragments = 0, MatchedFields = Fields.FromStrings(["title", "title.folded", "title.tr"]) },
        ["description"] = new HighlightField { NumberOfFragments = 1, FragmentSize = 160, MatchedFields = Fields.FromStrings(["description", "description.folded", "description.tr"]) }
    }
};
curl "http://localhost:5080/books/search?q=raif%20berlin&pageSize=1"
{"total":2,"page":1,"pageSize":1,"items":[{"isbn":"9781000000030","title":"Kürk Mantolu Madonna","authors":["Sabahattin Ali"],"language":"tr","price":199.00,"score":19.795294,"titleHighlight":null,"descriptionHighlight":"<mark>Raif</mark> Efendi&#x27;nin <mark>Berlin&#x27;de</mark> bir sergide gördüğü portre ve Maria Puder ile yaşadığı aşkın romanı."}],"didYouMean":null}

Kesme işaretinin &#x27; olarak gelmesi encoder: html'in işi: highlight metni HTML olarak döndüğü için Elasticsearch metindeki özel karakterleri escape ediyor, yalnız kendi <mark> tag'lerini ham bırakıyor. Bu neden önemli? Highlight metni istemcide innerHTML ile DOM'a (Document Object Model; sayfanın tarayıcıdaki nesne ağacı) basılıyor ve document'ın içeriği her zaman güvenilir değil: back office'te bir açıklama alanına yapıştırılmış HTML ya da kötü niyetli bir girdi. Ayrı bir books-xss index'inde denedim:

PUT /books-xss/_doc/9789999999984?refresh=wait_for
{ "isbn": "9789999999984", "title": "Draft", "description": "A new edition <img src=x onerror=alert(1)> with notes" }

"highlight": { "pre_tags": ["<mark>"], "post_tags": ["</mark>"], "fields": { "description": {} } }
{"hits":{"hits":[{"highlight":{"description":["A new <mark>edition</mark> <img src=x onerror=alert(1)> with notes"]}}]}}

"highlight": { "encoder": "html", "pre_tags": ["<mark>"], "post_tags": ["</mark>"], "fields": { "description": {} } }
{"hits":{"hits":[{"highlight":{"description":["A new <mark>edition</mark> &lt;img src=x onerror=alert(1)&gt; with notes"]}}]}}

Encoder'sız cevabı innerHTML'e vermek bir stored XSS (cross-site scripting; sayfaya başkasının script'inin enjekte edilmesi) açığı. Encoder ile metin zararsız hâle geliyor. Angular tarafında [innerHTML] binding'i ayrıca sanitize ediyor, ama ikinci savunma hattına güvenip birinciyi kapatma; istemci her zaman Angular olmayabilir. Frontend Güvenliği serisinin "Angular'da DOM XSS Sink Envanteri: ElementRef, Renderer2, Dinamik URL'ler, HTML Üreten Kütüphaneler ve Code Review Listesi" yazısı innerHTML gibi sink'leri tek tek inceliyor.

"Bunu mu Demek İstediniz?": Term ve Phrase Suggester

Suggester'lar arama sonucuna ek olarak, sorgudaki kelimeler için index'teki term'lerden düzeltme öneriyor. term suggester her kelimeye ayrı bakıyor, phrase suggester ise bütün ifade için tek bir düzeltilmiş cümle üretiyor:

GET /books/_search?filter_path=suggest
{ "size": 0, "suggest": { "text": "kurk mantlu madona", "title-terms": { "term": { "field": "title.folded", "suggest_mode": "always" } } } }
{"suggest":{"title-terms":[{"text":"kurk","offset":0,"length":4,"options":[]},{"text":"mantlu","offset":5,"length":6,"options":[{"text":"mantolu","score":0.8333333,"freq":2}]},{"text":"madona","offset":12,"length":6,"options":[{"text":"madonna","score":0.8333333,"freq":2}]}]}}

GET /books/_search?filter_path=suggest
{
  "size": 0,
  "suggest": {
    "text": "saatleri ayarlma enstitusu",
    "did-you-mean": {
      "phrase": {
        "field": "title.folded", "size": 1,
        "direct_generator": [ { "field": "title.folded", "suggest_mode": "always" } ],
        "collate": { "query": { "source": { "match": { "title.folded": { "query": "{{suggestion}}", "operator": "and" } } } }, "prune": true }
      }
    }
  }
}
{"suggest":{"did-you-mean":[{"text":"saatleri ayarlama enstitusu","offset":0,"length":26,"options":[{"text":"saatleri ayarlama enstitusu","score":4.9556263E-7,"collate_match":true}]}]}}

collate, önerilen ifadeyi gerçek bir sorguyla deniyor; prune: false ile sonuç bulmayan öneriler hiç dönmüyor. Önerinin folded alanın term'lerinden, yani küçük harfli ve Türkçe karaktersiz geldiğine dikkat et ("enstitusu"). Kullanıcıya bir bağlantı olarak göstermek ve tıklandığında aynı aramayı çalıştırmak için bu yeterli, çünkü sorgu folded alanda da eşleşiyor. Önerinin Türkçe karakterli olması gerekiyorsa title alanını kullan; o zaman "kurk" gibi yazımlar düzeltilmez.

Endpoint'te suggester'ı aramayla aynı istekte gönderiyoruz. Fuzziness yazım hatalarının çoğunu zaten bulduğu için sıfır sonuç artık nadir; öneriyi, kullanıcının yazdığından farklıysa sonuç olsa da döndürüyoruz:

curl "http://localhost:5080/books/search?q=kurk%20madona&pageSize=1"
{"total":2,"page":1,"pageSize":1,"items":[{"isbn":"9781000000030","title":"Kürk Mantolu Madonna","authors":["Sabahattin Ali"],"language":"tr","price":199.00,"score":55.550564,"titleHighlight":"<mark>Kürk</mark> Mantolu <mark>Madonna</mark>","descriptionHighlight":null}],"didYouMean":"kurk madonna"}

Arayüz "kurk madonna için sonuçlar gösteriliyor" ya da "Bunu mu demek istediniz: kurk madonna" diyebilir. "madona"nın fuzzy ile Madonna'ya eşleştiğini ve highlight'ın da bunu işaretlediğini görüyorsun.

Üretimde Dikkat

Relevance setini CI'a koy. rank-eval komutu bir eşik değerin altına düştüğünde hata dönecek şekilde pipeline'da çalışabilir; analyzer, synonym ya da boost değişikliği içeren her pull request ölçülür. Setin kendisi de yaşayan bir dosya: sıfır sonuçlu aramalar ve şikâyetler her ay eklenmeli.

Fuzzy'nin maliyetini izle. Her fuzzy term, index'teki benzer term'lerden (varsayılan en fazla 50) bir genişleme üretiyor; uzun sorgularda ve büyük term sözlüklerinde bu, sorgu süresine yansır. prefix_length ve max_expansions ilk ayarlanacak iki değer.

Highlight'ın bedelini sınırla. number_of_fragments ve fragment_size ile dönen metni küçük tut; uzun açıklamalarda highlight, sorgu süresinin belirgin bir kısmı olabilir. Highlight'a ihtiyaç yalnız sonuç listesinde var, detay sayfası için yok.

Skoru kullanıcıya gösterme, karar vermek için de kullanma. Elasticsearch skoru aynı sorgu içinde sıralama içindir; sorgular arasında karşılaştırılamaz ve "skor 10'un altındaysa gösterme" gibi kurallar index büyüdükçe bozulur.

Sık Düşülen Tuzaklar

Boost'ları tahminle ayarlamak. Bu yazıdaki iki "mantıklı" değişiklik (tie_breaker ve most_fields) ölçünce puanı düşürdü. Ölçmeden yapılan ayar, bir sorguyu düzeltip başka birini bozar ve kimse fark etmez.

Fuzzy'yi her alana açmak. Kısa kelimelerde bir harf farkı tamamen başka bir kelime demek; kategori, dil, kod gibi alanlar fuzzy aranmamalı. Türkçe karakter sorunlarını fuzzy ile değil folding ile çöz.

Popülerlik sinyalini ham değerle çarpmak. Sıkıştırmadan (log1p, düşük weight) eklenen bir sinyal metin eşleşmesini ezer; ilk denememiz bunu ölçülebilir biçimde gösterdi.

Highlight metnini encoder'sız innerHTML'e basmak. Index'teki her metin, kaynağı ne olursa olsun, güvenilmez veri sayılmalı.

Relevance setini değişiklikten sonra yazmak. Set, değişiklikten önce yazılmalı; yoksa farkında olmadan değişikliğin iyi göründüğü sorgular seçilir.

Küçük Bir Deneme

★☆☆ judgments.json'a "kurk mantolu madonna sabahattin" ve "middlemarch eliot" sorgularını ekle, yargılarını ver ve rank-eval --tuned çalıştır. Puan değişti mi?

★★☆ bookstore-synonyms set'ine "suç ve ceza, crime and punishment" kuralını ekle (4. yazıdaki gibi, Dev Tools'ta). rank-eval --tuned yeniden ölç: iki dil arası sorgular düzeldi mi, başka bir sorgu bozuldu mu?

★★★ Endpoint'e rescore ekle (phrase clause'u yerine) ve iki yaklaşımı aynı sorgu listesiyle karşılaştır. _rank_eval isteğine rescore verilemediği için ölçümü nasıl yaparsın? İpucu: endpoint'i sorgu listesiyle çağırıp nDCG'yi kendin hesaplayan küçük bir komut.

Bir sonraki yazıda arama kutusunun kendisine geçiyoruz: kullanıcı yazarken öneri vermek. search_as_you_type, edge_ngram ve completion suggester'ı aynı index'te yan yana kuracak, "kür" yazınca Kürk Mantolu Madonna'yı getirecek ve her yaklaşımın index boyutuna maliyetini ölçeceğiz.