"Arama" dendiğinde akla hazır servisler gelir; ama bir blogun ya da doküman sitesinin aramasını yazmak, bir öğleden sonralık işe yakındır ve ne olduğunu öğretmesi bakımından hazır servislerden iyidir. Uçtan uca üç aşama vardır: metni parçalara ayırmak, parçaları hangi belgelerin içerdiğini hatırlayan bir yapıda saklamak ve sonuçları sıralamak. Her aşamada vereceğiniz küçük kararlar, kullanıcının gördüğü sonuç kalitesini belirler.
Bu yazıda üç aşamayı da en yalın haliyle kuracağız; amaç, arama kutusunun ardındaki makineyi görünür kılmak. Kullanıcı için arama tek bir kutudur; sizin için ise token listesi, dizin ve sıralama fonksiyonudur. Üçünün ayrımını gördükten sonra hazır servislerin hangi doldurunları sattığını da daha net görürsünüz.
Aşama 1: Tokenizasyon ve normalleştirme
Kullanıcı "Kağıt" yazdığında "kağıt" içeren belgeyi bulmak istiyordur; motor ise ikisinin aynı olduğunu bilmek zorunda. Bu, tokenizasyon sırasında yapılan basit normalleştirmedir: küçük harfe çevir, noktalama at, çoğul sıkıştırmayı dene. Türkçe'de ek işleri zorlaştırır ama "kitap" ile "kitabı"nın kökü ortaktır; basit bir sonek kırpma listesi bile çoğu senaryoyu kurtarır.
function tokenize(metin: string): string[] {
return metin
.toLocaleLowerCase('tr')
.replace(/[.,;:!?()"'«»]/g, ' ')
.split(/\s+/)
.filter((t) => t.length > 1);
}
Aşama 2: Ters dizin kurmak
Motorun kalbi, her terimi hangi belgelerin içerdiğini saklayan ters dizindir. Bellek içinde bile basit bir harita, on binlerce belge için yeterlidir:
// terim -> belge kimliği listesi
const tersDizin = new Map<string, number[]>();
function belgeEkle(id: number, metin: string) {
for (const terim of tokenize(metin)) {
const belgeler = tersDizin.get(terim) ?? [];
if (belgeler[belgeler.length - 1] !== id) belgeler.push(id);
tersDizin.set(terim, belgeler);
}
}
Sorgu aşamasında kullanıcı terimleri ayrıştırılır, her terimin belge listesi alınır ve kesişim kümesi hesaplanır. Kesişim için en kısa listeden başlamak, pratikte en hızlı yoldur; klasik bilgisayar bilimi burada gerçek hayattan hızlıdır.
Aşama 3: Sıralamak
Kesişim kümesi çok uzun olabilir; sıralama bunu kullanıcıya sunar. Basit başlangıç, terimin belgede kaç kez geçtiği saymaktır; ama uzun belgeler haksız avantaj kazanır. BM25 tam bunu dengeler: sıklık arttıkça katkıyı azaltır, belge uzunluğuna normalleştirir ve nadir terimlere öncelik verir. Türkçe'de kullanıcı yazım hataları için de pay bırakın: iki karaktere kadar uzaklıkta olan terimleri öneri olarak göstermek, arama deneyimini büyük ölçüde iyileştirir.
Sıralamanın en altına iki küçük kural daha ekleyin. Birincisi tazelik: aynı terimi içeren iki belgeden yenisi hafif bir çarpanla öne geçsin; haber ve blog içeriğinde bu tek kural, kullanıcı memnuniyetini belirler. İkincisi başlık ağırlığı: başlıkta geçen terim, gövdede geçen terimden daha değerlidir; en basit uygulaması, başlıktan gelen frekansı iki ya da üçle çarpan bir katsayıdır. Karmaşık sıralama algoritmaları bu iki kuralın üzerine eklenir; ikisi olmadan kurulan sıralama ise hep aynı yarım düzine belgeyi öne çıkaran düz bir çöl gibi hissettirir.
Kendi motorunuzu yazmak, hazır bir servisi değiştirecek ölçekte olmasa bile iki şey kazandırır: aramanın sihir değil mühendislik olduğunu görmek ve hazır serviste hangi doldurunları sipariş ettiğinizi bilmek. Tokenizer'ınızı yazdıktan sonra, "niye bu sonucu bulamıyor" sorusunun cevabı artık sihirli kutunun ardında değil, sizin kodunuzdadır.
Okur Yorumları (0)