Vector Search: 8 Farklı Motoru Kıyaslamak

11 minute read

Published:

Bu yazıda uzun zamandır aklımda olan “hangi vektör arama motorunu ne zaman kullanmalı” sorusunu yanıtlamaya çalıştım (yanıtlayamadım).


Giriş

--- ⚠ gereksiz teorik bilgi başlangıcı ---

Vektör araması (vector search) kavramı ilk olarak aslında benzerlik araması terimi ile birlikte ortaya çıkıyor. Buradaki benzerlik tanımı gözlemler arasındaki mesafeyi ölçmek için kullanılan bir matematiksel fonksiyon/hesaplama (ör: kosinüs uzaklığı) ile ilgili. Bu fonksiyon iki gözlem arasındaki benzerliği (veya benzemezliği) sayısal olarak ifade ediyor. Kendime atıf verdiğim (evet utanmadan) akademik tanımı aşağıya bıraktım;

Benzerlik araması (similarity search), bilgi erişiminde (information retrieval) temel bir yöntem olup bir sorguyla (query) en çok örtüşen veri örneklerinin belirlenmesine odaklanır. Metin, görüntü ve video gibi çeşitli alanları kapsayan bu yöntem öneri sistemleri, bilgi yönetimi ve kişiselleştirilmiş arama motorları (search engine) gibi uygulamalarda kritik bir rol oynar. Bu uygulamalarda benzerlik araması, ürünlerin özellikleri veya kullanıcı sorguları ile ürünler arasındaki semantik yakınlığı ölçerek arama sonuçlarının alaka düzeyini artırmayı hedefler. Benzerlik araması yaklaşımları genel olarak tam doğruluklu (exact) ve yaklaşık (approximate) yöntemler olarak ikiye ayrılır. Tam doğruluklu yöntemler arama uzayındaki (search space) tüm öğelerle karşılaştırma yaparak en yakın komşuları bulur ancak büyük veri setlerinde hesaplama maliyeti yüksektir. Bu nedenle özellikle moda ürünleri gibi milyonlarca ürünün bulunduğu büyük ölçekli veri kümelerinde yaklaşık en yakın komşu (approximate nearest neighbor - ANN) yöntemleri tercih edilmektedir. ANN yöntemleri yüksek boyutlu özellik vektörleri arasında hızlı ve verimli benzerlik araması yaparak gerçek zamanlı uygulamalarda kullanılabilirlik sağlar. 1

Benzerlik aramasına geri dönecek olursak da burada etken faktörler başta benzerlik ölçütü. Kosinüs (cosine), öklid (euclidean), Manhattan, Hamming gibi farklı metrikler var. Bunlar arasındaki farklar ve hangi metrik hangi veri tipine daha uygun soruları da ayrı bir yazı konusu olmakla birlikte karşılaştırma yapılırken göz önünde bulundurulması gereken bir parametre. Bir diğer etken ise seçilecek algoritma. HNSW, IVF, PQ, LSH gibi farklı algoritmalar var ve bunlar da farklı veri tipleri ve boyutları için farklı performanslar sergiliyor. Bununla birlikte komşu sayısı (k) ve arama parametreleri de performansı etkileyen diğer faktörler arasında. Tüm bu etkenler göz önünde bulundurulduğunda vektör arama motorlarının performansını kıyaslamak için standart bir yaklaşım belirlemek zorlaşıyor.

Vektör araması (vector search) ise özellikle son yıllarda özellikle doğal dil işleme (NLP) ve LLM alanında bir hayli popüler olmuş bir konu. Benzerlik aramasını temel alan süreç olarak tanımlamak mümkün. Vektörlerin elde edilmesinden en yakın (benzer) komşuların bulunmasına kadar olan süreci kapsıyor. Bu süreçte vektörlerin elde edilmesi için embedding modelleri kullanılıyor. Bu modeller, metin, görüntü veya diğer veri türlerini yüksek boyutlu vektörlere dönüştürerek benzerlik aramasının temelini oluşturuyor. Vektör araması özellikle büyük veri kümelerinde hızlı ve etkili bir şekilde benzer öğeleri bulmak için optimize edilmiş algoritmalar ve veri yapıları kullanıyor. Bu sayede kullanıcılar sorgularına en uygun sonuçları hızlı bir şekilde elde edebiliyor.

--- ⚠ gereksiz teorik bilgi bitti ---

Ben de kendi tarafımda ilk başta sorduğum soruyu yanıtlamaya çalıştım. Çaya çorbaya limon der gibi Annoy/Faiss kullanmalı mıyız yoksa amaca özel bir vektör veritabanı mı (Qdrant, Weaviate) tercih etmeliyiz.

Bu soruya kimse net bir cevap veremiyor gördüğüm kadarıyla. Bunun en temel sebebi herkesin kıyaslaması farklı veri seti, farklı donanım, farklı metrik üzerinden yapılmış olması. Kimi sadece QPS’e bakıyor, kimi sadece recall’e, kimi de “bende çalıştı” seviyesinde kalıyor. Bu belirsizliği ortadan kaldırmamak için “bir de ben yanıt veremeyeyim” dedim ve 8 ayrı vektör arama motorunu2 aynı donanım üzerinde aynı embedding’ler ve aynı metriklerle kıyasladım. Kodun mimarisi bir oturuşta ortaya çıktı, asıl öğretici kısım motorların birbirinden nerede ayrıştığını görmekti.

Her zaman olduğu gibi yine deney ortamı özelinde önce birkaç net kısıt koydum.

  • Embedding üretimi GPU’da, arama CPU’da: Ollama üzerinden nomic-embed-text (768 boyut) ile embedding’leri bir kez üretip diske yazıyorum. Asıl kıyaslanan arama motorlarının hiçbirinde GPU yok, hepsi CPU üzerinde çalışıyor. Amaç aynı donanım üzerinde kıyas yapmak.
  • Küçük ama etiketli bir veri seti: AG News‘ten 2.000’er örnek x 4 sınıf, yani 8.000 dokümanlık bir corpus (train), artı 500 sorgu (test). Etiket olması önemliydi, çünkü sadece brute force’a göre recall ölçmek yetmiyor. Aynı sınıftan dokümanları alakalı sayıp precision/nDCG gibi klasik bilgi erişimi metriklerini de hesaplamak istedim.
  • Tek dev docker-compose.yml yok: Her motorun kendi compose dosyası var, sırayla ayağa kalkıp iniyor. Hem zaman hem kaynak açısından mantıklı. Çünkü sekiz motoru aynı anda ayakta tutmak hem makineyi zorluyor hem de debug etmeyi zorlaştırıyor.
  • Adil kıyas: Aynı 768 boyutlu, L2 normalize edilmiş embedding’ler her motora aynı şekilde besleniyor. Her container aynı cpus/mem_limit sınırına tabi. Mesafe metriği hepsinde cosine. HNSW (Hierarchical Navigable Small World) kullanan motorlarda M/ef_construction benzer değerlerde tutuluyor. Ve en önemlisi, tek bir noktada ölçüm yapmak yerine her motorun kendi arama anı parametresini (ef_search, num_candidates, search_k, ne diyorsa) 5 farklı noktada tarayıp gerçek bir recall-latency eğrisi çıkarıyorum.

Kıyaslanan 8 motor şunlar:

Mimari

Klasör yapısı aşağıdaki gibi 👇🏻

embeds/
  data/            
  embeddings/      
  ground_truth/    
  engines/
    pgvector/  redis/  elasticsearch/  qdrant/  weaviate/  chroma/
      docker-compose.yml
      adapter.py    
    faiss/  annoy/
      adapter.py    
  benchmark/
    common/         
    configs/        
  results/          

Her framework için ortaklaştırabilmek adına BaseAdapter diye bir arayüz. Her motor bunu uyguluyor:

  • connect(): ayakta olan container’a bağlan.
  • build_index(ids, vectors, index_params): koleksiyonu/tabloyu/index’i oluştur, tüm vektörleri yükle, geçen süreyi döndür.
  • set_search_params(params): arama anı parametresini uygula (ef_search, num_candidates…).
  • query(vector, k): tek bir top-k sorgu çalıştır, gecikmeyle beraber sonucu döndür.
  • index_size_bytes(): index’in disk üzerindeki boyutu.
  • close(): bağlantıyı kapat.

Ortak runner.py bunu kullanarak her motor için aynı akışı işletiyor. Container’ı ayağa kaldır, health check’i bekle, index’i kur, warmup sorguları at, sweep (tarama, yani arama anı parametresini birden fazla değerde deneyip her birinde ayrı ayrı ölçüm alma işlemi. Burada her motor için 5 noktada yapılıyor) boyunca recall@10, precision@10, nDCG@10, p50/p95/p99 gecikmesi, tek thread ve 8 thread QPS (queries per second, saniyede işlenen sorgu sayısı) ölç, sonunda container’ı durdur. Yani pgvector’ı test eden kod ile Weaviate’i test eden kod satır satır aynı iskeleti paylaşıyor, değişen sadece engines/<framework>/adapter.py.

Referans bir “ground truth” da lazım. Brute force ile top-k bulup diğer motorların recall’unu buna göre ölçmek için gerekli. Burada hızlı ve basit olması için FAISS’un IndexFlatIP‘ini kullandım. 8.000 vektör üzerinde top-100’ü çıkarmak birkaç saniye sürüyor, bu da tüm motorların recall’unu karşılaştırdığım sabit referans.

Benzerlikler & Benzemezlikler

Benzerlik aramasından bahsetmişken o kadar yaklaşımdaki farklardan bahsetmemek olmazdı. Sekiz motoru aynı BaseAdapter arayüzünün arkasına koyunca hangi noktalarda gerçekten aynı ölçüldüklerini hangi noktalarda farklı davrandıklarını görmek kolaylaştı.

Ortak olanlar

  • Mesafe metriği: Hepsi dolaylı cosine benzerliğine göre sıralama yapıyor. Annoy’un kullandığı “angular” metrik, vektörler L2 normalize olduğu için cosine ile birebir aynı sıralamayı üretiyor.
  • HNSW ağırlıklı: pgvector, Redis, Elasticsearch, Qdrant, Weaviate ve Chroma’nın hepsi HNSW tabanlı approximate index kullanıyor. FAISS de IndexHNSWFlat ile aynı aileden. Sadece Annoy random projection tabanlı bir ağaç ormanıyla (forest of trees) tamamen farklı bir yaklaşım izliyor.
  • Tek bir efor düğmesi: Her motorun recall ile latency arasındaki dengeyi kontrol eden tek bir arama anı parametresi var: ef_search, num_candidates, hnsw_ef, ef, search_ef, ef_runtime, search_k. İsimler farklı, işlev aynı: aramanın grafikte ne kadar geniş gezineceğini söylüyor.
  • Ölçütler: Build süresi, disk boyutu, Recall@10 & precision@10 & nDCG@10, p50/p95/p99 gecikme, tek thread ve 8 thread QPS, hepsinde aynı şekilde ve aynı runner.py üzerinden ölçülüyor.

Farklı olanlar

Çalışma şekli: pgvector, Redis, Elasticsearch, Qdrant, Weaviate ve Chroma birer client-server sistem. Her biri kendi container’ında kendi ağ protokolüyle konuşuyor. FAISS ve Annoy ise birer kütüphane. Bu tek fark bile, aşağıdaki tablodaki QPS farkının büyük kısmını açıklıyor.

Canlı parametre güncelleme: pgvector SET hnsw.ef_search ile, Redis EF_RUNTIME ile sorgu içinde, Elasticsearch num_candidates ile yine sorgu içinde, Qdrant search_params ile sorgu içinde, Weaviate ise collection.config.update(...) ile arama anı parametresini canlı olarak değiştirmeye izin veriyor, index’i yeniden kurmadan.

Kullanım amaçları: pgvector, Redis ve Elasticsearch aslında başka bir işin üzerine sonradan eklenmiş eklenti veya fonksiyonlar. Qdrant, Weaviate ve Chroma baştan bu amaca özel vektör veritabanı olarak tasarlanmış.

Ekosistem uyumu: pgvector’ün en büyük avantajı SQL uyumlu doğması. Vektör aramasını mevcut ilişkisel tablolarla aynı sorguda JOIN edebiliyorsun. Diğerlerinde bu uygulama katmanında ayrıca birleştirilmesi gereken iki ayrı sorgu.

Disk ayak izi. Aynı ~8.000 vektör için index boyutları 24MB ile 200MB arasında değişiyor (aşağıdaki tabloda görülüyor).

Bunların hiçbiri “şu motor daha iyi” demiyor. Asıl kıyas, aşağıdaki sayılarda.

Sonuçlar

results/summary.csv‘yi topladıktan sonra bu aralar favori görselleştirme kütüphanem olan plotnine ile altı ayrı grafik çıkardım (kodu scripts/06_visualize_results.py‘de). Tek bir kalabalık dashboard yerine her biri tek bir şeyi anlatan görseller tercih ettim.

Her motorun kendi 5 noktalık taramasının ortasını (sıra 3 - medyan-) alıp tablolaştırdım. Aşağıdaki tablo her motorun “ne çok düşük ne çok yüksek” bir efor ayarındaki hali. p50 (medyan gecikme) değerine göre sıralı, p99 sütunu da sorguların en yavaş %1’lik kısmının ne kadar sürdüğünü gösteriyor

MotorRecall@10p50 (ms)p99 (ms)QPS (1 client)QPS (8 client)Build (s)Index (MB)
FAISS0.9880.100.2713.97113.6250.3024.5
Annoy0.8230.440.802.31711.0620.4128.3
pgvector0.9920.781.101.2832.5794.6363.6
Redis0.9850.811.051.1902.2222.9926.8
Weaviate0.9861.261.676801.1691.9329.3
Chroma0.9863.485.942793432.4730.1
Qdrant0.99015.4228.49654266.27196.5
Elasticsearch0.99950.0560.48201517.3423.7

Tabloda göze çarpan ilk şey recall sütunu neredeyse hepsinde 0.98’in üzerinde (Annoy bu efor seviyesinde henüz oraya gelmemiş) ama p50 gecikmesi 0.10ms ile 50ms arasında, yani aynı recall bandında 500 kata yakın fark var. Elasticsearch’ün recall’u (0.999) burada da diğerlerinden belirgin biçimde yüksek. Bunun nedeni muhtemelen seçtiğim sweep aralığının (20-320) diğer motorlarınkinden (çoğu 10’dan başlıyor) daha yüksekten başlaması. Küçük ve tek shard’lık bir HNSW grafiğinde num_candidates=20 bile grafiğin büyük kısmını kapsayabiliyor, yani Elasticsearch’ün gerçekten zorlandığı düşük efor noktasını hiç test etmemiş olabilirim.

En basit olanı ilk grafik. Her motorun orta efor seviyesindeki recall’unu, p50 gecikmesini ve tek thread QPS’ini yan yana bar chart olarak gösteren bir görsel.

Ardından iki Pareto eğrisi geliyor. Biri p50 gecikmeye karşı recall@10, diğeri tek thread QPS’e karşı recall@10, her motorun 5 noktalık taramasını bir çizgi olarak çiziyor, böylece “hızlı ama düşük recall” ile “yavaş ama yüksek recall” ayarları arasındaki geçişi görebiliyoruz. Buna bir de ortak “efor seviyesi” ekseni ekledim çünkü her motorun kendi tarama parametresi farklı ölçekte (Annoy’da 2000 Qdrant’ta 160 aynı şey değil) ve ham değerleri doğrudan karşılaştıramıyorum. Bunun yerine her motorun kendi 5 noktalık taramasındaki sırasını (1’den 5’e) ortak eksen yapınca “en düşük eforlu ayar” ile “en yüksek eforlu ayar” tüm motorlarda karşılaştırılabilir oldu.




Son iki grafik de build süresini disk boyutuyla ve 1 ile 8 eşzamanlı client’taki QPS’i de birbiriyle kıyaslıyor.



Ham sayılara bakınca da birkaç şey öne çıkıyor. FAISS ve Annoy process içi çalıştıkları için build süresinde ve QPS’te açık ara önde. FAISS build’i 0.3 saniyede bitiyor, tek thread’de yaklaşık 19.000 QPS’e kadar çıkabiliyor. Ama bu adil bir avantaj değil mimari bir farktan geliyor. Çünkü ikisinde de ağ ve serileştirme (serialization) maliyeti yok. Qdrant ve Elasticsearch’te ise ilginç bir örüntü var. Gecikme hnsw_ef/num_candidates arttıkça neredeyse hiç değişmiyor ama recall net biçimde iyileşiyor. Qdrant’ta örneğin recall 0.915’ten 0.999’a çıkarken p50 gecikmesi 15.4 ile 15.7 milisaniye arasında sabit kalıyor, bu da bu ölçekte (8.000 vektör) network ve protokol maliyetinin asıl HNSW gezinme maliyetini gölgelediği anlamına geliyor. Qdrant’ın disk kullanımı da diğerlerine göre belirgin şekilde daha yüksek (~196MB, diğerleri 24-64MB aralığında).

Repo sekiz motorun tamamı için docker-compose.yml + adapter + config içeriyor, her biri ayrı ayrı, istediğin gün çalıştırılabilir. Grafikler ve tam sonuç tablosu results/ altında.

Repo: silverstone1903/ann-benchmark

Kodun geliştirilmesinde Claude Sonnet 5’ten yararlandım. Kodun mimarisini ve adapter’ları tasarlayıp benchmark runner’ı kodunu yazdırdım. Sonuçların yorumlanması ve görselleştirilmesi kısmı tamamen kendi gözlemlerim.

Kaynaklar

  1. Need to run docker run command inside python script
  2. pgvector
  3. Redis Stack, vector search
  4. Qdrant
  5. Weaviate
  6. Chroma
  7. plotnine


  1. Gümüştaş, E. (2025). Derin öğrenme tabanlı multimodal ürün arama sisteminin geliştirilmesi (Doctoral dissertation, Mimar Sinan Fine Arts University (Turkey)). 

  2. Arama motoru kavramından yola çıkarak vektör arama motoru (vector search engine) terimini kullandım. Aslında FAISS ve Annoy birer kütüphane, veritabanı değiller ama benchmark sürecinde hepsini aynı mantığın arkasına koyduğum için “motor” demek daha kapsayıcı oldu.