-- Presupunem că avem tabelul document_chunks cu o coloană embedding (vector(1536))
-- și o coloană fts_document (tsvector) pentru full-text search.
WITH semantic_search AS (
SELECT id, 1 - (embedding <=> $1) AS similarity_score
FROM document_chunks
ORDER BY embedding <=> $1
LIMIT 20
),
text_search AS (
SELECT id, ts_rank_cd(fts_document, to_tsquery('romanian', $2)) AS text_score
FROM document_chunks
WHERE fts_document @@ to_tsquery('romanian', $2)
ORDER BY text_score DESC
LIMIT 20
)
SELECT
coalesce(s.id, t.id) AS id,
(coalesce(s.similarity_score, 0) * 0.6) + (coalesce(t.text_score, 0) * 0.4) AS hybrid_score
FROM semantic_search s
FULL OUTER JOIN text_search t ON s.id = t.id
ORDER BY hybrid_score DESC
LIMIT 5;Am trecut prin febra bazelor de date vectoriale dedicate acum un an, când toată lumea urla că ai nevoie de Pinecone sau Milvus ca să faci RAG. La un proiect cu vreo 50.000 de documente de suport tehnic și în jur de 10.000 de utilizatori activi, am decis să simplificăm arhitectura și să aducem totul în Postgres folosind extensia pgvector. Am salvat vreo 400 de dolari pe lună și am redus latența cu 40ms pentru că am eliminat un hop de rețea inutil.
Dacă ai deja Postgres în stack, să adaugi o altă bază de date doar pentru embeddings e, de cele mai multe ori, o decizie greșită la început.
Când merită pgvector și unde se împiedică
Avantajul uriaș e că ai tranzacții ACID și poți să faci join-uri clasice între vectori și datele relaționale. Vrei să cauți doar în documentele create de userul X în ultimele 30 de zile? Într-o bază vectorială dedicată e un coșmar de filtrare. În Postgres e doar un simplu WHERE user_id = X AND created_at > NOW() - INTERVAL '30 days'.
Dar există și un compromis major. HNSW (Hierarchical Navigable Small World), cel mai bun index pentru căutări rapide în pgvector, se încarcă complet în RAM. La dimensiunea de 1536 a modelelor de la OpenAI, dacă ai milioane de rânduri, memoria RAM de pe instanța ta de Postgres o să plângă. Am pățit ca un simplu REINDEX să blocheze baza de date timp de 12 minute pentru că nu alocasem destul maintenance_work_mem în config.
Hybrid Search: Combinăm semantica cu keyword matching
Căutarea pur semantică (embeddings) e excelentă pentru context general, dar e praf când userul caută un cod de eroare exact sau un ID de produs (ex: "ERR_9021"). Aici intervine Hybrid Search. Combinăm tsvector (full-text search-ul nativ din Postgres) cu distanța de cosine din pgvector.
Ca să facem asta curat, rulăm ambele interogări, normalizăm scorurile și le adunăm cu ponderi. Eu folosesc de obicei o pondere de 0.6 pentru semantic și 0.4 pentru text search, dar depinde mult de datele tale. În exemplul de mai jos folosim o metodă simplă de combinare direct în SQL.
Indexarea care nu te lasă în drum
Dacă ai sub 10.000 de vectori, nu te obosi cu indecși. Căutarea secvențială (exactă) e extrem de rapidă în Postgres pe seturi mici de date. Când treci de pragul ăsta, ai de ales între IVFFlat și HNSW.
Sfatul meu: mergi direct pe HNSW. IVFFlat e mai rapid de construit și consumă mai puțin RAM, dar acuratețea (recall) scade masiv dacă baza de date suferă modificări dese și nu îi dai REINDEX constant. Pentru HNSW, folosesc de obicei m = 16 și ef_construction = 64 la crearea indexului. Dacă ai nevoie de acuratețe mai mare la căutare, poți crește ef_search la nivel de sesiune înainte de query, dar asta îți va mări latența. E genul de reglaj fin pe care îl faci doar după ce testezi cu date reale, nu în stadiul de mockup.
Voi ce folosiți pentru RAG în producție? Ați rămas pe Postgres sau chiar ați avut nevoie de scalabilitatea unui vector DB dedicat?