· kaynak dev.to (home feed)
pgvector HNSW filtrelemesi 400 RAG sorgusunun 52'sinde sıfır satır döndürdü
Bir dev.to mühendislik yazısı, pgvector'ın HNSW index'inin tenant filtrelerini aday aramasından sonra uygulayarak çok kiracılı RAG retrieval'i sessizce felç ettiğini ve yinelemeli taramaların bunu nasıl çözdüğünü gösteriyor.

Bir retrieval arızasını gizleyen grounded reddetme
Bir geliştirici, dev.to'daki yazısında, dokümantasyon asistanının bir müşteriye API key rotasyonu ile ilgili hiçbir şeyin müşterinin dokümanlarında yer almadığını kendinden emin bir şekilde söylediğini, oysa cevabın ikinci sayfada olduğunu anlatıyor. İlgili chunk veritabanında mevcuttu ve embedding'i sağlamdı; Postgres onu hiç döndürmedi. Sorumlusu, sıradan bir tenant filtresinin pgvector'ın HNSW index'iyle birleşmesiydi ve bu, 400 değerlendirme sorgusunun 52'sinde hatasız, uyarısız ve dashboard'larda görünür hiçbir iz bırakmadan sıfır satırlık sonuç üretti.
Kurulum ve sessiz arıza
Sistem, 40 müşteri çalışma alanına hizmet veriyordu; 1,2 milyon satırlık bir chunks tablosunda vector(1024) embedding sütunu, cosine distance kullanan bir HNSW index ve tenant_id üzerinde düz bir btree index vardı. En büyük tenant tablonun yaklaşık %34'ü olan 410.000 chunk'a sahipti, en küçüğünde ise 600 chunk vardı.
Retrieval, her pgvector eğitimindeki sorgu kalıbını kullanıyordu: tenant'a göre filtrele, embedding mesafesine göre sırala, 10 satır al. Sonuçlar, yalnızca verilen bağlamdan yanıtlaması talimatıyla Claude'a gidiyordu. Bu talimat, hatanın haftalarca hayatta kalmasını sağlayan şeydi, çünkü boş bir bağlam dürüst bir "bilmiyorum" üretiyordu ve bu, cevap kalitesi metriklerinde doğru grounded davranış olarak okunuyordu.
Filtre HNSW retrieval'ini neden bozuyor
Yazıya göre HNSW taraması önce hnsw.ef_search adayını (varsayılan 40) tablonun tamamından topluyor ve Postgres WHERE koşulunu sonradan uyguluyor. Döndürülen satırların beklenen sayısı kabaca ef_search ile filtre seçiciliğinin çarpımıdır: satırların %2'sine sahip bir tenant ortalama yaklaşık 0,8 satır elde eder, 10 değil. Yazarın pratik kuralı, varsayılan ayarlar ve LIMIT 10 ile tablonun %25'inden azıyla eşleşen herhangi bir filtre eksik dönebilir.
Chunk'ların dengesiz dağılımı durumu kötüleştirdi. Şifre sıfırlama gibi yaygın sorular, grafın büyük tenant'ın auth dokümantasyonuyla dolu bölgelerine denk düşüyor; böylece küçük tenantlar, komşu yarışını kazanan ve sonra filtrelenerek atılan başka bir çalışma alanının neredeyse aynı chunk'larıyla rekabet ediyordu.
Hasarı ölçmek
Yazar, yalnızca retrieval'e odaklanan bir değerlendirme kurdu: 400 sorgu, tenant başına 10 tanesi, her biri var olduğu bilinen bir gold chunk ile eşleştirilmiş, ayrıca satır sayısı limitin altına düştüğünde bir uyarı kaydı. Varsayılanlarla 253 sorgu tam 10 satır döndürdü, 147'si daha az döndürdü, 52'si hiç döndürmedi. Sıfır satırlı her sorgu, tablonun %3'ünden azına sahip bir tenant'tan geldi. Bu 52 boş bağlam için Claude 49 kez reddetti; üç kez ise talimatlarını ihlal ederek genel bilgiden yanıt verdi ve var olmayan bir ayarlar sayfası uydurdu.
ef_search'ü yükseltmek kısmi ve maliyetli bir çözüm
Daha büyük bir aday listesi yardımcı oluyor: ef_search 100'deyken eksik sorgular 81'e düştü; 400'deyken dört sıfır satırlık sonuçla 19'a düştü, ancak p95 gecikme 18ms'den 61ms'ye çıktı. Maksimum değer olan 1000'de bile dokuz sorgu hâlâ eksik döndü. Yazar ikinci bir tuzağa da düştü: ef_search'ü session seviyesinde ayarlamak hiçbir şeyi değiştirmedi, çünkü transaction modundaki PgBouncer SET komutunu bir backend'de, sorguyu ise başka bir backend'de çalıştırdı. İşleyen yaklaşım, SET LOCAL'i sorguyla aynı transaction içinde her seferinde kullanmak.
Yinelemeli taramalar, sonra minik tenantlar için exact taramalar
pgvector 0.8.0'ın hnsw.iterative_scan özelliği, filtreleme çok az satır bıraktığında HNSW grafında yürümeye devam ediyor; LIMIT karşılanana veya hnsw.max_scan_tuples'a (varsayılan 20.000) ulaşılana kadar sürüyor. relaxed_order ile ve ef_search 40'a geri döndürüldüğünde eksik sorgular 147'den 3'e, sıfır satırlık sorgular sıfıra düştü ve p95 gecikme yalnızca 18ms'den 24ms'ye çıktı. Gevşek sıralama satırları mesafe sırasının biraz dışında döndürebildiği için yazar, materialized CTE üzerinden yeniden sıralıyor; strict_order tam sıralamayı koruyor ama daha yavaş. Büyük tenant sorguları yavaşlama yaşamadı, çünkü ilk geçişte 10 satırı dolduruyorlar ve asla yinelemiyorlar.
Son üç eksik sorgu 600 satırlık tenant'tan geldi: bu seçicilikte 20.000'lik tarama sınırı ortalama kabaca 10 eşleşme veriyor, yani limit yaklaşık yarısı zaman kaçırılıyor. 5.000 satır ve altındaki tenantlar artık enable_indexscan devre dışı bırakılarak vector index'i tamamen atlıyor; tenant_id btree'si bir bitmap taramayı yönetiyor ve ardından EXPLAIN ANALYZE ile doğrulanmış exact bir sıralama geliyor. 600 satır üzerinden exact tarama, tam recall ile 4ms sürdü.
Neden önemli
Bu arıza modu tasarımdan dolayı görünmez: hata yok, yavaş sorgu yok, temiz dashboard'lar ve grounded reddetmeleri sistemin doğru çalıştığı gibi göründüğü bir LLM var. Tenant boyutları dengesiz ve approximate vector search sonradan filtrelenen her çok kiracılı RAG dağıtımı buna maruz. Ucuz savunmalar şunlar: cevap metriklerinin yanında retrieval sayılarını kaydetmek, retrieval'i cevaplardan ayrı değerlendirmek, pgvector 0.8 veya sonrasında yinelemeli taramaları etkinleştirmek ve çok küçük tenantlar için exact taramalara dönmek. PgBouncer ayrıntısı başlı başına bir ders: session seviyesindeki ayarlama transaction pooling arkasında sessizce başarısız olabilir.
- #pgvector
- #postgres
- #rag
- #vector-search
- #retrieval