WITH semantic_search AS (
SELECT id, row_number() OVER (ORDER BY embedding <=> $1) as rank
FROM documents
ORDER BY embedding <=> $1
LIMIT 50
),
keyword_search AS (
SELECT id, row_number() OVER (ORDER BY ts_rank_cd(text_search_vector, to_tsquery('romanian', $2)) DESC) as rank
FROM documents
WHERE text_search_vector @@ to_tsquery('romanian', $2)
LIMIT 50
)
SELECT
coalesce(s.id, k.id) as document_id,
coalesce(1.0 / (60 + s.rank), 0.0) + coalesce(1.0 / (60 + k.rank), 0.0) as rrf_score
FROM semantic_search s
FULL OUTER JOIN keyword_search k ON s.id = k.id
ORDER BY rrf_score DESC
LIMIT 10;Salutare. Am văzut că multă lume sare direct pe Pinecone sau Milvus când aude de RAG, de parcă Postgres ar fi doar pentru tabele de utilizatori. Eu zic să vă opriți puțin și să vă uitați în propria curte: pgvector e incredibil de capabil și îți salvează nopți întregi de debugging pe sincronizarea datelor.
De ce să nu complici stack-ul inutil
Am avut acum câteva luni un proiect cu vreo 120.000 de documente tehnice (fișe de service, manuale, coduri de eroare). Clientul voia un chatbot intern inteligent. Prima tentație a echipei a fost să bage Qdrant.
Am zis „stați așa”. Dacă pui o bază de date vectorială separată, ai imediat două mari probleme: consistența datelor și costurile de infrastructură. Când un document se șterge sau se modifică în DB-ul principal, trebuie să rulezi un worker care să actualizeze vector store-ul. Dacă workerul crapă din cauza unui timeout de rețea? Rămâi cu ghost embeddings și răspunsuri halucinate. Cu pgvector, totul se întâmplă într-o singură tranzacție ACID. Simplu și curat.
Indexarea: HNSW vs IVFFlat și costul în RAM
Aici e primul trade-off sincer pe care trebuie să-l înțelegi înainte să trântești asta în producție. pgvector îți dă două tipuri de indecși aproximativi (ANN): IVFFlat și HNSW.
IVFFlat e rapid de construit și consumă puțină memorie, dar acuratețea (recall) scade masiv dacă nu recalculezi listele des pe măsură ce adaugi date noi. HNSW (Hierarchical Navigable Small World) e regele: are un recall excelent și e foarte rapid la query-uri, dar indexarea durează mult și mănâncă RAM de rupe.
La cei 120.000 de vectori ai noștri (dimensiune 1536, generați cu text-embedding-3-small de la OpenAI), indexul HNSW a ocupat în jur de 1.2 GB de RAM. Sună puțin, dar dacă sari la 10 milioane de vectori, ai nevoie de zeci de GB doar pentru ca indexul să stea în memorie. Dacă nu ai buget de servere grase, Postgres o să înceapă să facă swap pe disc, iar latența o să sară de la milisecunde la secunde bune.
Magia căutării hibride (BM25 + Embeddings)
Căutarea pur semantică (pe bază de embeddings) e genială pentru concepte generale, dar e praf când userul caută un cod exact de piesă, cum ar fi „AX-9042”. Embeddings-urile pur și simplu nu înțeleg bine potrivirile exacte de caractere și numere.
Soluția pe care am aplicat-o a fost căutarea hibridă. Am combinat Full-Text Search-ul clasic din Postgres (folosind tsvector și tsquery, care acționează similar cu algoritmul BM25) cu căutarea semantică prin pgvector.
Folosim o tehnică numită Reciprocal Rank Fusion (RRF). Practic, rulăm ambele interogări în paralel într-un singur query SQL, le dăm un scor în funcție de poziția lor în rezultate și le combinăm. Am reușit să obținem un timp de răspuns de sub 45ms pe query, combinând precizia keyword-urilor cu flexibilitatea semantică a AI-ului.
Pentru 90% din cazurile de RAG din producție, Postgres e mai mult decât suficient. Câștigi simplitate operațională, tranzacții ACID și query-uri hibride într-un singur loc. Voi ați trecut prin coșmarul sincronizării între DB-ul principal și un vector store dedicat, sau ați mers direct pe varianta simplă?