Elasticsearch ile ürün araması: 40ms'nin altına inmek
Binlerce baskı ürünü varyantında facet'li arama ve otomatik tamamlama mimarisi.
MatbaaStore'da katalog basit bir liste değil. Bir "kartvizit" tek bir ürün gibi görünse de arkasında ebat, kağıt türü, gramaj, kaplama ve son işlem kombinasyonlarından doğan yüzlerce varyant var. Kullanıcı arama kutusuna "kraft etiket" yazdığında, bu varyant denizinden doğru ürünleri milisaniyeler içinde önüne getirmemiz gerekiyor — üstelik sol taraftaki filtrelerin (kağıt türü, gramaj, renk) her birinin yanında kaç sonuç olduğunu da anında göstererek. Bu, klasik bir "LIKE '%kelime%'" sorgusunun kaldırabileceği bir yük değildi.
Bu yazıda ürün aramasını PostgreSQL'den Elasticsearch'e taşıma sürecimizi ve p95 arama gecikmesini 40 milisaniyenin altına nasıl indirdiğimizi anlatacağız. Türkçe'nin arama için çıkardığı zorlukları, yazarken öneri (autocomplete) mimarisini, filtreli aramanın (facet) kalbindeki toplama sorgularını ve gecikmeyi düşüren indeks tasarımı kararlarını tek tek ele alacağız. Amaç yalnızca doğru sonucu bulmak değil, kullanıcı harfi yazdığı hızda bulmaktı.
Neden PostgreSQL yetmedi?
Arama ilk sürümde PostgreSQL üzerinde çalışıyordu. Düşük katalog boyutunda "ILIKE" ve ardından tam metin arama makul sonuç veriyordu. Sorun ölçekte değil, aramanın doğasında ortaya çıktı. Kullanıcı "kuşe" yerine "kuse" yazdığında, ya da "davetiye" ararken "davetıye" yazdığında sonuç boş dönüyordu. Türkçe'nin çökme (ç, ş, ğ, ı) ve ek yapısı, kelime köküne inmeden yapılan aramayı kırılgan hâle getiriyordu.
İkinci sorun filtrelerdi. Her filtre seçeneğinin yanında canlı sonuç sayısı göstermek istiyorduk; bu, her arama için onlarca ayrı "GROUP BY" sorgusu demekti ve gecikme hızla büyüdü. Elasticsearch bu iki problemi de tek çatı altında çözüyordu: dile duyarlı analiz zinciri ve tek istekte hem sonuçları hem de tüm filtre sayımlarını dönen toplama (aggregation) altyapısı. Karar, hız ihtiyacından çok bu iki yeteneğin birleşmesiyle netleşti.
Arama bir veritabanı sorgusu değil, bir dil problemidir. Kullanıcı ne demek istediğini yaklaşık yazar; işiniz o yaklaşımı doğru sonuca çevirmektir. Türkçe'de bu, aramanın en zor ve en görünmez kısmıdır.
Türkçe'ye göre analiz zinciri
Elasticsearch'te aramanın kalitesi büyük ölçüde metni indekslerken uyguladığınız analiz zincirinden gelir. Amacımız hem çökme farklarını (kuşe/kuse) hem de yazım varyasyonlarını yakalamaktı. Bunun için Türkçe'ye özel bir analizör kurduk: küçük harfe indirme, aksan ve çökme karakterlerini sadeleştiren asciifolding ve Türkçe kök bulma adımlarını birlikte kullandık. Böylece "kuşeler", "kuşe" ve "kuse" indeks üzerinde aynı köke düşüyor.
{
"analysis": {
"analyzer": {
"tr_metin": {
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding", // ç, ş, ğ, ı sadeleşir
"turkish_stemmer" // kelime köküne iner
]
},
"tr_oneri": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "edge_ngram_2_15"]
}
}
}
}
Dikkat edilecek bir ayrıntı: asciifolding'i hem indeksleme hem de arama tarafında aynı şekilde uygulamak gerekiyor. Kullanıcının yazdığı sorgu da aynı zincirden geçmezse, indekste "kuse" olarak duran belge, "kuşe" sorgusuyla eşleşmez. Analizörü indeks ve sorgu için simetrik tutmak, arama kalitesindeki en sık görünmeyen hataların önüne geçti.
Bir de tek bir alanı birden çok biçimde indeksledik. Ürün adını hem analiz edilmiş metin olarak (arama için), hem de dokunulmamış "keyword" olarak (filtreler ve tam eşleşme için), hem de yazarken öneri için ayrı bir alan olarak tuttuk. Aynı veriyi farklı amaçlara göre birden çok kez indekslemek, Elasticsearch'te disk maliyetini kabul edip sorgu esnekliğini kazanmanın standart yolu.
Yazarken öneri ve facet'ler
Arama kutusunun altında beliren anlık öneriler için edge_ngram yaklaşımı kullandık. Bu yöntem indeksleme sırasında "kartvizit" kelimesini "ka", "kar", "kart", "kartv"... gibi artan öneklere böler. Kullanıcı "kart" yazdığında bu önek doğrudan indekste hazır bekliyor; sorgu anında hesap yapılmadığı için öneriler tek haneli milisaniyelerde dönüyor. Öneri kalitesini artırmak için ürünleri popülerliğe göre puanladık, böylece en çok satın alınan varyantlar listenin başında çıkıyor.
Facet'ler, yani sol taraftaki filtre grupları, Elasticsearch'in toplama altyapısıyla geliyor. Tek bir istekte hem eşleşen ürünleri hem de her filtre değerinin kaç sonuç ürettiğini alıyoruz. İşin püf noktası, filtreleri puanlama yapmayan "filter" bağlamında çalıştırmak. Filter bağlamı hem daha hızlıdır hem de Elasticsearch tarafından önbelleğe alınır; aynı filtre kombinasyonu tekrar geldiğinde sonuç neredeyse bedava döner.
- terms toplaması: Kağıt türü, gramaj ve renk gibi kategorik filtreler için sonuç sayımlarını üretir. Bunları "keyword" alt alanları üzerinden çalıştırmak şart; analiz edilmiş metin üzerinde toplama yapmak hem yavaş hem yanıltıcıdır.
- filter bağlamı: Seçili filtreleri puanlama dışı bırakır. Sıralamayı bozmaz, yalnızca eleme yapar ve önbelleğe girer.
- post_filter inceliği: Kullanıcının bir filtreyi seçmesi, o filtre grubunun diğer seçeneklerinin sayımını gizlememeli. Seçilen grubu toplamadan hariç tutarak "8 mat, 5 parlak" gibi sayımları doğru gösteriyoruz.
Eş anlamlılar, yazım hataları ve alaka düzeyi
Doğru sonucu bulmak analiz zinciriyle biter sanılır; oysa asıl incelik alaka düzeyinde. Kullanıcı "poster" yazdığında biz katalogda o ürünü "afiş" diye tutuyor olabiliriz. Bu tür eşleşmeleri bir eş anlamlılar sözlüğüyle çözdük: "poster" ile "afiş", "broşür" ile "katalog" gibi sektörel karşılıkları indekse tanıttık. Böylece kullanıcının hangi kelimeyi tercih ettiği önemini yitirdi; ikisi de aynı sonuca ulaşıyor.
Yazım hataları için ölçülü bir tolerans tanımladık. Elasticsearch, aranan kelimeye bir ya da iki harf uzaklıktaki kelimeleri de eşleştirebiliyor; "kartviit" yazan kullanıcı yine de "kartvizit" sonucunu görüyor. Ama bu toleransı sınırsız açmak tehlikeli — çok gevşek bir eşleşme, alakasız sonuçları da içeri sokar. Kısa kelimelerde toleransı kısıp uzun kelimelerde biraz gevşeterek dengeyi kurduk.
Son olarak sıralama. İki ürün de sorguyla eşleşiyorsa hangisi üstte çıkmalı? Yalnızca metin benzerliğine güvenmek yetmiyor; ürünün popülerliğini, stok durumunu ve kampanyalı olup olmadığını da sıralamaya kattık. Kullanıcının aradığı çoğu zaman "en çok eşleşen" değil, "en çok işine yarayan" sonuçtur. Alaka düzeyini bu iş değeriyle harmanlamak, aramanın kullanıcıya faydasını en çok artıran adım oldu.
40 milisaniyenin altına inmek
Hedefimiz p95 arama gecikmesini 40 milisaniyenin altında tutmaktı. Analiz zinciri doğru kurulduğunda Elasticsearch zaten hızlıdır; asıl kazanımlar birkaç somut karardan geldi. Birincisi indeks tasarımı: sık filtrelenen alanları "keyword", nadiren aranan uzun açıklama alanlarını ise gereksiz analiz yükünden arındırdık. Her alanı her şey için hazırlamak yerine, sorgu desenimize göre biçimlendirdik.
İkincisi, arama ile katalog verisini ayırmak. Elasticsearch bizim için gerçeğin kaynağı değil, aramaya optimize edilmiş bir görünüm. Ürün ve fiyat verisi PostgreSQL'de durur; katalog değiştiğinde ilgili belgeleri Elasticsearch'e asenkron olarak yeniden indeksleriz. Böylece arama tarafı, yazma yükünden ve karmaşık ilişkilerden tamamen kurtulur; tek işi hızlı okumak olur.
Üçüncüsü ölçmek. Hangi sorguların yavaş olduğunu tahmin etmek yerine yavaş sorgu kaydını açtık ve gerçek trafikteki en yavaş yüzde beşi izledik. Beklenmedik darboğaz, çok sayıda facet dönen geniş sorgulardaydı; gereksiz toplama alanlarını kıstığımızda p95 belirgin şekilde düştü. Sonuçta popüler sorgularda önbellek isabetiyle birlikte gecikmeyi rahatça 40 milisaniyenin altında, çoğu istekte tek haneli milisaniyelerde tutuyoruz.
Sonuç
Ürün aramasını hızlandıran tek bir sihirli ayar yoktu; birkaç sıradan kararın toplamıydı. Türkçe'ye uygun, indeks ve sorgu tarafında simetrik bir analiz zinciri kurmak; öneriyi arama anında değil indeksleme anında hazırlamak; facet'leri önbelleğe giren filter bağlamında çalıştırmak; ve Elasticsearch'i gerçeğin kaynağı değil, aramaya adanmış bir görünüm olarak konumlandırmak. Aramanın zor kısmı çoğu zaman altyapı değil, dildir. Kullanıcının yaklaşık yazdığını doğru sonuca çevirmeyi başarırsanız, kalan hız problemi çözülebilir bir mühendislik işine dönüşür.