80 Satır Python ile Yapılmış Arama Motoru
(alexmolas.com)microsearch, arama motorlarının iç işleyişini doğrudan anlamak için yapılmış oyuncak bir uygulama; çekirdek arama motoru sınıfı 80 satırdan az olsa da crawler, API ve HTML şablonları dahil edildiğinde proje daha büyük- Küçük web siteleri ve blogların büyük arama motorlarında iyi keşfedilememesi sorunundan yola çıkarak, 642 RSS feed’inden yazılar toplayıp arama verisi oluşturuyor
asynciotabanlı asenkron crawling ile toplama süresi 20 dakikadan 20 saniyeye düştü; temizlenmiş metinler Parquet verisi olarak saklanıyor- Arama, kelimeleri URL bazında görülme sayılarına bağlayan ters indeks üzerinde çalışıyor; sonuç sıralamada bağlantı tabanlı PageRank yerine içerik tabanlı BM25 kullanılıyor
- FastAPI UI ile arama kutusu ve sonuç sayfası sunuyor; ancak sorgu operatörleri, n-gram indeksleme, sorgu/belge genişletme ve crawling sırasında indeksleme gibi özellikler henüz yok
microsearch’ün amacı ve kapsamı
microsearch, GitHub deposunda yayımlanan bir Python arama motoru uygulamasıdır- Amaç, üretim kullanımı için bir arama motoru değil; arama motorlarının içeride nasıl çalıştığını gösteren kullanılabilir bir oyuncak örnek oluşturmaktır
- Arama kapsamı, Google SEO rekabetinde iyi keşfedilemeyen küçük web siteleri ve bloglara daha yakındır
- Çekirdek arama motoru uygulaması 80 satırdan azdır; ancak veri crawler’ı, API ve HTML şablonları gibi yardımcı kodlar dahil edildiğinde projenin tamamı daha büyüktür
- Uygulama, Solr ve Lucene ile çalışırken arama motorlarının işleyişini daha derinden anlama sürecinde ortaya çıkmıştır
RSS tabanlı crawler
- Aranacak veriyi oluşturmak için blog RSS feed’leri crawl edilir
- Kullanılan feed sayısı toplam 642 RSS feed’idir
- Yaklaşık 100’ü ML, veri bilimi, matematik gibi yazarın doğrudan okuduğu bloglardır
- Kalan yaklaşık 500’ü surprisetalk blogs.hn projesinden alınmıştır
- Crawling akışı, her RSS feed’inden yazı URL’lerini çıkarmak, yazının HTML’ini indirmek ve ardından gövde metnini temizlemek şeklindedir
- HTML temizleme,
BeautifulSoupilescriptvestyleöğelerini kaldırır; satır sonlarını ve boşlukları düzenleyerek metne dönüştürür aiohttpveasynciokullanılan asenkron crawling sayesinde çalışma süresi 20 dakikadan 20 saniyeye düşmüştür- Sonuçlar, URL ve temizlenmiş gövde metnini içeren bir
DataFramehaline getirilipoutput.parquetdosyasına kaydedilir
Ters indeks yapısı
- Arama motorunun ilk temel veri yapısı ters indekstir
- Ters indeks, anahtar kelimeleri belgelere eşleyerek belirli bir kelimenin hangi belgelerde geçtiğini hızlıca bulmayı sağlar
- Uygulama,
dict[str, dict[str, int]]biçiminde birdefaultdictkullanır- Dış anahtar kelimedir
- İç anahtar URL’dir
- İç değer, ilgili kelimenin o URL’deki belgede kaç kez geçtiğidir
SearchEnginesınıfının iki dahili sözlüğü vardır_index: kelime bazında URL görülme sayılarını saklar_documents: URL bazında orijinal içeriği saklar
index(url, content), içeriği normalize ettikten sonra boşluklara göre ayırır ve her kelimenin URL bazındaki görülme sayısını artırırbulk_index(), URL ve içerik listelerini alarak birden fazla belgeyi tek seferde indekslerget_urls(keyword), anahtar kelimeyi normalize ettikten sonra o kelimeyi içeren URL’leri ve görülme sayılarını döndürür
Dize normalizasyonu ve temel arama
- Dize normalizasyonu, noktalama işaretlerini boşlukla değiştirir, yinelenen boşlukları temizler ve metni küçük harfe çevirir
- Büyük/küçük harf farkını azaltmak için
Foovefooaynı anahtar kelime olarak işlenir - İki örnek belge indekslendiğinde
fooarama sonucu iki belgeyi de döndürürFoo:Hello, World! My name is Foo!Bar:Hello, World! My name is Bar, I'm not Foo!
- Bu aşamada yalnızca belgenin arama terimini içerip içermediği ve kaç kez içerdiği bilinebildiğinden, sonuç sırasını belirlemek için ayrı bir sıralama gerekir
BM25 sıralayıcı
- Arama sonuçlarını sıralamak için BM25 kullanılır
- PageRank belgeleri bağlantılara göre sıralarken, BM25 puanı belge içeriğine göre hesaplar
SearchEngine, BM25 hesaplaması için varsayılank1=1.5,b=0.75parametrelerine sahiptir- Sınıf, sıralama hesaplaması için gerekli özellikleri sağlar
posts: indekslenmiş URL listesinumber_of_documents: toplam belge sayısıavdl: ortalama belge uzunluğu
idf(kw), belirli bir anahtar kelimenin ters belge frekansını hesaplar- Toplam belge sayısı
N - İlgili anahtar kelimeyi içeren belge sayısı
n_kw log((N - n_kw + 0.5) / (n_kw + 0.5) + 1)formülünü kullanır
- Toplam belge sayısı
bm25(kw), ilgili anahtar kelimeyi içeren her URL için BM25 puanını hesaplarsearch(query), sorguyu normalize edip kelimelere ayırır; ardından her kelimenin BM25 puanını URL bazında toplayarak döndürür- Örnekte yalnızca
fooarandığındaFoobelgesinin puanıBar’dan yüksektir;foo bararandığında iseBarbelgesinin puanı daha yüksek olur
FastAPI arayüzü
- Arama motoru küçük bir FastAPI uygulaması olarak sunulur
- Uygulama bir
SearchEngineörneği oluşturur ve başlangıçta Parquet verisinden URL ile içerikleri okuyupbulk_index()ile indeksler - Başlıca üç route vardır
/: arama sayfasını render eder ve indekslenmiş yazı listesini aktarır/results/{query}: sorguyu arar ve en iyi 5 URL’yi sonuç sayfasında gösterir/about: tanıtım sayfasını render eder
- Sonuçlar puana göre azalan sırada sıralanır ve ardından yalnızca top-N URL seçilir
- UI ve UX’in geliştirilebilecek çok yanı vardır; ancak arama hızlı çalışır ve sonuçlar da fena değildir
Eksik özellikler ve sınırlamalar
- Uygulamada gerçek arama motorlarından beklenebilecek birçok özellik eksiktir
- Sorgu operatörleri yoktur
- Örneğin Google’daki
how to build a search engine -solrgibi belirli bir kelimeyi hariç tutan aramaları desteklemez
- Örneğin Google’daki
- n-gram indeksleme yoktur
"search engine"gibi iki kelimenin belirli bir sırayla geçtiği belgeleri bulma biçimi desteklenmez
- Sorgu veya belge genişletme yoktur
enginearandığındaenginesiçeren belgeler otomatik olarak bulunmaz
- Crawling ve indeksleme ayrıdır
- Belge alınır alınmaz indeksleme yapacak şekilde birleştirilebilir ve bu süreç de asenkron hale getirilebilir
Sonraki adımlar
- Proje sayesinde Solr’ın içeride nasıl çalıştığına dair daha fazla sezgi kazanılmıştır
- IO ağırlıklı işlerde asenkron kodun büyük etki yarattığı da görülmüştür
- Sonraki adım, arama motoruna semantik arama özelliği eklemektir
- Embedding modeli ve ANN ile denemeler yapılmıştır; bir sonraki iş bu özelliği
microsearche eklemektir
1 yorum
Hacker News yorumları
Bu gerçekten çok havalı. Yerelde test amaçlı, oldukça hızlı bir BM25 arama motorunu Pandas ile yapıyorum: https://github.com/softwaredoug/searcharray
Pandas kullanmamın nedeni, sadece BM25’in yeterli olmaması ve güncellik, popülerlik gibi diğer etkenleri pandas/numpy ile kolayca hesaplayıp birleştirmek istemem
Bu arada ifade araması zor kısım. İfade eşleştirmede çok fazla sınır durumu var ve slop gibi şeyleri de hesaba katmak gerekiyor. Konum bilgisini de mümkün olduğunca az bellekle sıkıştırmak lazım: https://github.com/softwaredoug/searcharray/blob/main/searcharray/utils/roaringish.py
Oldukça kapsamlı test ettiğimi düşünüyorum ama geri bildirim almak güzel olur. Konum bilgisini delta encoding ile kodladım ve base36 olarak kodladım
Doğru bir nokta. Aramadaki zor kısmın çoğu veri ölçeğini yönetmekte yatıyor. Mantığın kendisi şaşırtıcı derecede kolay ya da kolaylaştırılabilir
Elbette sonsuz derecede karmaşık hale getirilebilir ama bu proje gereksiz kısımları iyi budamış. Arama motorunu büyütme problemi olarak değil de veriyi fiziksel olarak küçültme veya sinyal-gürültü oranını artırma problemi olarak bakarsan oldukça ileri gidebilirsin
src/microsearch/engine.pyiçindeSearchEngine.__init__(self, k1: float = 1.5, b: float = 0.75)gibi kodlar var amak1ya dab’nin ne olduğunu hiç bilmiyorum ve dosyanın tamamında tek bir yorum bile yokBu günlerde yorumlar moda değil mi?
_documentsanahtarı URL, değeri de o URL’nin içeriği gibi görünüyor ama yanılıyor da olabilirim. Arama motorunun nasıl yapıldığını öğrenmek ve üstüne koymak için iyi bir kaynak olabilirdi ama belgelendirme olmayınca kod kalitesi hayal kırıklığı yaratıyorŞaka bir yana, normalde dokümantasyon ve kodun birlikte olmasının daha iyi olduğuna katılıyorum. Ama bu durumda bu bir eğitim projesi, bu yüzden kodla dokümantasyonu ayırıp blog yazısında kodu belgelemeyi seçtim
k1veb, TF-IDF veya BM25’te kullanılan standart ağırlık değerleri; burada da BM25 tarafındalarYorumlar faydalı olurdu ama bu probleme aşina biri için hemen tanınabilecek isimler bunlar
k1veb, BM25 sıralama fonksiyonunun ayar parametreleri. Orijinal yazının yazarının uydurduğu isimler değil; neredeyse tüm uygulama ve ders kitaplarında bu değişken adları kullanılırBilgi erişimi alanını bilen birinin anlaması için aslında onlara
k1vebdemek daha doğru: https://en.wikipedia.org/wiki/Okapi_BM25a: floatgibi örnekleri görünce hep Rich Hickey’nin “ihtiyacınız olan şey tipler değil, doğru isimlerdir” konuşması aklıma geliyorGo’dan gelmiş gibi duran, açıklamasız tek harfli değişken adları kullanma ve tip sistemini isimlendirmeye yardımcı bir araç gibi kötüye kullanma eğiliminden gerçekten nefret ediyorum. İsimler, programın ne yaptığına dair anlamsal bilgi taşıyabilir; o yüzden düzgün kullanılmaları gerekir
Dış bağımlılıklar kullanırken toplam
\r\nsayısı yerine kod satırı sayısıyla övünmenin ne anlamı var, bilmiyorumKod tabanını ölçmek için SI birimi yok ama bilişsel yükün bir şekilde ölçülmesi gerektiğini düşünüyorum
collections,math,stringiçe aktarılıyor ve hepsi standart kütüphaneAma daha doğrusu buna bir “arama motoru motoru” demek gerekebilir. Tarayıcı ve arayüz o 80 satıra dahil değil ama bir şekilde gerekli ve sunulan uygulama bunlar yüzünden satır sayısını ve kütüphane sayısını epey artırıyor. Yine de bu kütüphaneler arama motorunun kendisiyle ilgili değil.
pandasveyafastapigibi genel bağımlılıkları saymaya başlarsan, işletim sisteminin milyonlarca satırını, ağ kartı firmware’ini ve donanım karmaşıklığını da sayman gerekebilirHoşuma gitti. Arama motoruyla birlikte kullanılabilecek 20 satırdan kısa bir öneri motoru da mümkün. Tıklanan URL’lerin oturum günlüklerini tutarsan, her oturumda mevcut URL’den sonraki sliding window’a bakıp yakın bağlantılara daha yüksek ağırlık vererek bir öneri listesi oluşturabilirsin
Öneri sonuçlarını sıralayıp yalnızca en üstteki N tanesini bırakırsan, belirli bir URL için önerilen URL’ler listesi elde edersin. Biraz kurcalarsan, girilen arama sorgularını ve tıklanan URL’leri günlüğe karıştırıp yazım önerileri de çıkarabilirsin
Çok hoş ve öğretici. Yalnız bunu deploy etmeyin :-)
Eskiden benzer bir şeye ihtiyacım olmuştu ama ölçek biraz daha büyüktü; on binlerce belge vardı ve cevap her zamanki gibi sqlite olmuştu. Yapısal olarak buradakiyle aynı, ama ters indeks kalıcılık katmanını benim yerime başka biri yazmıştı
Google,
"search engine"gibi çift tırnakla arama yaptığınızda yalnızca bu iki kelimenin o sırayla geçtiği sonuçları gösterirEn azından bazı durumlarda öyle, ama ne yazık ki her zaman değil. İleri düzey kullanıcıların istediği şey “web için grep”, “Google'ın size göstermek istediğini söylemesi” değil
Google'ın sorguyu yorumlarken fazla özgür davranması doğru, ama herhangi bir arama motorunun hiç yapmamasındansa kesinlikle daha iyi olan birçok işlem var. Bugünkü Google aramanın sorunu, sonuçların neden öyle çıktığını kestirmenin zor olması; bunun da string karşılaştırmasında embedding'lere fazla dayanmasından kaynaklandığı anlaşılıyor.
"cat food"ifadesinin"dog restaurant"ile eşleşmesi gibi, embedding uzayında anlamsal olarak yakın ama insan muhakemesiyle örtüşmeyen sonuçlar çıktığında insanı gerçekten yoruyorDış kütüphaneler olan
feedparser,bs4vb. kullanırken buna 80 satır kod demek bence adil değilelasticsearchüstüne yapılmış olsaydı katılırdım, ama gerçek arama motoru kısmı o 80 satırda uygulanmışsa bunun adil olduğunu düşünüyorum. İçeri aktarılan kütüphaneler zaten genelde elle yeniden yazılmaması gereken türdenBazen “kendi arama motorumu yapmak” diye yazılan bir yazı aslında
searxngya dayacykurulum rehberi çıkıyor; bu ise öyle bir durum değilGüzel. Buraya bulanık arama özelliği eklemek de çok zor görünmüyor. Örneğin
"hackrnew"aramasının"hackernews"ile eşleşmesini sağlamak için, önek düzenleme mesafesi belli bir eşik altında olan sonuçları bulabilirsinizTemel fikir, bir ek ters indeks daha tutmak; burada anahtarlar belge koleksiyonundaki kelimelerin n-gramlarıdır (genellikle 3-gram), posting'ler ise bu n-gramın geçtiği kelimeler ya da kelime kimlikleridir.
PED(x, y) <= deltaise|N(x) ∩ N(y)| >= |N(x)| - n ∙ deltayardımcı teoremi kullanılabilir. Girdix'in n-gramlarını hesaplayıp her n-gram için posting'leri alır ve tekrarları birleştirirseniz, her aday kelimeyile paylaşılan n-gram sayısını elde edersiniz. Bu sayı eşikten büyükse ancak o zaman gerçek PED hesaplanır; küçükse atlanır, böylece pahalı hesaplama büyük ölçüde azaltılırSonra bu şekilde elde edilen kelime listesini mevcut indekste sorgularsınız. Eskiden https://dont.watch/ üzerinde istemci taraflı bir JS bulanık arama motoru yaparken bu yaklaşımı kullanmıştım. JS koduna bakarsanız ters indeksin ve sıkıştırılmış n-gram indeksinin doğrudan JS dosyası olarak gönderildiğini görebilirsiniz. Gerçek arama motoru, dış bağımlılık olmadan yaklaşık 300 satır JS'ten oluşuyor ve yalnızca arama sonuçlarını iyileştirmeye yönelik çok temel sezgiseller içeriyor