CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Hybrid search logic
WITH semantic_search AS (
SELECT id, rank() OVER (ORDER BY embedding <=> '[...vector...]') as rank
FROM documents
ORDER BY embedding <=> '[...vector...]'
LIMIT 20
),
keyword_search AS (
SELECT id, rank() OVER (ORDER BY ts_rank_cd(text_search_vector, query) DESC) as rank
FROM documents, to_tsquery('english', 'E104') query
WHERE text_search_vector @@ query
LIMIT 20
)
SELECT
COALESCE(s.id, k.id) as document_id,
(1.0 / (60 + s.rank) + 1.0 / (60 + k.rank)) as combined_score
FROM semantic_search s
FULL OUTER JOIN keyword_search k ON s.id = k.id
ORDER BY combined_score DESC;Acum un an, când toată lumea sărea în barca AI, am făcut și eu greșeala clasică: am ales o bază de date vectorială dedicată pentru un proiect de RAG. Aveam vreo 50.000 de documente tehnice și totul părea brici pe hârtie, dar în producție m-am lovit de coșmarul sincronizării datelor între baza noastră principală și vector store-ul extern.
După două luni de injecții eșuate și inconsistențe la ștergerea datelor, am zis „stop”. Am mutat totul în Postgres folosind extensia pgvector. Rezultatul? Am tăiat costurile cu infrastructura cu aproape 40% și am scăpat de latența de rețea între servicii. Dar nu e totul roz și există niște capcane de care m-am lovit și vreau să le știi înainte să te apuci de treabă.
HNSW vs IVFFlat: Ce alegi când ai volum
Cea mai mare dilemă în pgvector este indexarea. La început am mers pe ivfflat pentru că se construia repede, dar precizia la căutare scădea drastic dacă nu făceam re-indexare după fiecare 10k de rânduri noi. La un moment dat, căutările returnau rezultate care n-aveau nicio treabă cu contextul.
Am trecut pe hnsw (Hierarchical Navigable Small World). Da, ocupă mai multă memorie RAM — am observat un consum cu vreo 2GB mai mare pe un nod de RDS m6g.large — și durează de 10 ori mai mult să construiești indexul inițial. Totuși, query-urile sunt mult mai rapide (sub 50ms la noi) și precizia rămâne ridicată fără mentenanță manuală constantă. Dacă ai sub 1 milion de vectori, HNSW e alegerea corectă 99% din timp.
Problema cu embeddings pure și de ce ai nevoie de BM25
Vectorii sunt grozavi pentru semantică („cum repar o țeavă”), dar sunt praf la cuvinte cheie specifice sau coduri de eroare. Dacă utilizatorul caută „eroare E104”, un model de embedding s-ar putea să-i dea rezultate despre „depanare generală” pentru că sunt apropiate în spațiul vectorial, ignorând fix codul specific.
Soluția pe care am implementat-o a fost Hybrid Search. Folosim tsvector din Postgres pentru căutarea clasică (BM25 style) și combinăm scorurile cu cele de la cosine similarity folosind Reciprocal Rank Fusion (RRF). Practic, facem două interogări într-una singură. Această abordare ne-a crescut relevanța răspunsurilor cu peste 25% conform feedback-ului de la useri.
Trade-off-uri și resurse
Nu te lăsa păcălit de ideea că Postgres e „gratis”. Vectorii de 1536 dimensiuni (cum sunt cei de la OpenAI text-embedding-3-small) mănâncă spațiu serios pe disc. La 50k documente, indexul HNSW poate ajunge ușor la câțiva GB. De asemenea, dacă vrei performanță, trebuie să te asiguri că indexul încape în shared_buffers.
Un alt aspect e mentenanța. Când faci update la un document, trebuie să recalculezi embedding-ul pe backend și să faci update în DB. Dacă ai multe scrieri concurente, pgvector poate deveni un bottleneck pentru că indexarea HNSW e intensivă pentru CPU. Noi am rezolvat asta printr-o coadă de procesare asincronă în BullMQ, deci baza de date nu stă niciodată blocată în calcule de vectori în timpul unui request de API.
Tu cum gestionezi consistența datelor în RAG? Rămâi pe Postgres sau preferi ceva gen Weaviate/Qdrant pentru scalabilitate extremă?