-- Crearea extensiei și a tabelei de documente
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
tsv_content TSVECTOR,
embedding VECTOR(1536) -- Dimensiune standard OpenAI
);
-- Creăm indecșii: GIN pentru text clasic, HNSW pentru vectori
CREATE INDEX idx_documents_tsv ON documents USING gin(tsv_content);
CREATE INDEX idx_documents_hnsw ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
-- Query hibrid: Combinăm similaritatea vectorială cu text-search (BM25-like)
WITH vector_search AS (
SELECT id, 1 - (embedding <=> $1) AS vector_score
FROM documents
ORDER BY embedding <=> $1
LIMIT 20
),
text_search AS (
SELECT id, ts_rank_cd(tsv_content, to_tsquery('romanian', $2)) AS text_score
FROM documents
WHERE tsv_content @@ to_tsquery('romanian', $2)
LIMIT 20
)
SELECT
d.id,
d.content,
COALESCE(v.vector_score, 0) * 0.7 + COALESCE(t.text_score, 0) * 0.3 AS hybrid_score
FROM documents d
LEFT JOIN vector_search v ON d.id = v.id
LEFT JOIN text_search t ON d.id = t.id
WHERE v.id IS NOT NULL OR t.id IS NOT NULL
ORDER BY hybrid_score DESC
LIMIT 5;Am migrat recent un proiect cu vreo 120.000 de documente tehnice de pe o instanță scumpă de Pinecone înapoi în Postgres, folosind pgvector și căutare hibridă. Dacă vrei RAG performant fără să adaugi încă o bază de date NoSQL în stack-ul tău și să-ți bați capul cu sincronizarea datelor, Postgres e de ajuns. Am economisit cam 300 de dolari pe lună la infrastructură și am redus latența query-urilor fiindcă am eliminat apelurile de rețea externe.
Totuși, să faci RAG ca la carte în Postgres nu înseamnă doar să arunci niște vectori într-o coloană și să dai un query cu <=>. Dacă vrei rezultate bune, ai nevoie de indexare corectă și căutare hibridă.
De ce vectorii simpli nu sunt suficienți (Problema exact-match)
Căutarea semantică (prin embeddings) e genială pentru concepte și sinonime. Dacă userul caută „cum resetez parola”, embeddings vor găsi documente despre „recuperare cont”. Dar dacă userul caută un cod de eroare exact, cum ar fi „ERR-404-X”, embeddings s-ar putea să dea complet pe lângă.
Aici intervine căutarea hibridă: combinăm similaritatea vectorială cu căutarea clasică de tip text (BM25 sau tsvector în Postgres).
Pentru a face asta eficient, rulăm ambele căutări în paralel și le combinăm scorurile folosind o formulă simplă de ponderare sau Reciprocal Rank Fusion (RRF). Scorul final decide ce fragmente trimitem către LLM.
Indexarea HNSW: Unde se rupe filmul
Implicit, dacă dai query pe o tabelă cu vectori fără index, Postgres va face o scanare completă a tabelei (sequential scan). La 10k rânduri e ok, dar la 100k+ baza de date o ia în freză.
Soluția este să folosești un index HNSW (Hierarchical Navigable Small World). Spre deosebire de IVFFlat, HNSW nu are nevoie de o fază de antrenare și oferă o acuratețe mult mai bună a căutării (recall), chiar dacă ocupă mai multă memorie RAM.
Regula de aur pentru HNSW în Postgres: indexul trebuie să încapă complet în memoria RAM dedicată (shared buffers). Dacă Postgres începe să citească indexul de pe disc, latența sare de la 5ms la 500ms instant. La vectori de 1536 de dimensiuni (cum sunt cei de la OpenAI), fiecare nod din index consumă destul de mult.
Trade-offs sincere: Postgres vs Vector DB-uri dedicate
Să fim realiști. Postgres cu pgvector este ideal pentru proiecte medii (până în 5-10 milioane de vectori).
- Avantaje: ACID complet, ai toate datele utilizatorilor în același loc (poți face ușor filtre de tipul
WHERE user_id = Xîn același query de similaritate), backup-urile se fac la pachet. - Dezavantaje: Scalarea pe verticală devine scumpă când ai zeci de milioane de vectori de dimensiune mare, deoarece ai nevoie de instanțe cu foarte mult RAM pentru indecși. În plus, construirea unui index HNSW pe o tabelă mare poate bloca CPU-ul minute bune dacă nu configurezi corect parametrii de build.
Tu ce folosești pentru RAG în producție? Ai rămas pe Postgres sau ai trecut la ceva dedicat precum Qdrant sau Milvus?