Aller au contenu principal
~/GiwiSoft
Pgvector

RAG avec Ollama et PostgreSQL (pgvector)

Giwi 3 min de lecture

Dans mon précédent article sur le RAG, j’utilisais ChromaDB comme vector store. Mais PostgreSQL avec l’extension pgvector fait aussi bien, sans ajouter une brique à votre stack existante.

Pourquoi pgvector ?

  • Pas de service supplémentaire - votre base PostgreSQL suffit
  • Indexation HNSW - performances comparables à Pinecone/Weaviate
  • SQL standard - vos requêtes vectorielles s’intègrent aux requêtes métier
  • Filtres hybrides - combinez recherche vectorielle et filtres WHERE classiques

Installation

CREATE EXTENSION vector;
docker run -e POSTGRES_PASSWORD=pass -p 5432:5432 pgvector/pgvector:pg16

Création de la table

CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
embedding vector(768) -- dimension selon votre modèle
);

Index HNSW

CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

Le pipeline complet

graph LR A[Document] --> B[Chunker] B --> C[Ollama embeddings] C --> D[pgvector] D --> E[Recherche] F[Question] --> G[Ollama embeddings] G --> E E --> H[Contexte + question] H --> I[Ollama LLM] I --> J[Réponse]

Le code Node.js

import pg from 'pg';
import ollama from 'ollama';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

async function embed(text) {
const res = await ollama.embeddings({ model: 'nomic-embed-text', prompt: text });
return res.embedding;
}

async function search(query, limit = 5) {
const vec = await embed(query);
const result = await pool.query(
`SELECT content, 1 - (embedding <=> $1) AS score
FROM documents
ORDER BY embedding <=> $1
LIMIT $2`,
[JSON.stringify(vec), limit]
);
return result.rows;
}

async function ask(question) {
const docs = await search(question);
const context = docs.map(d => d.content).join('\n\n');
const res = await ollama.chat({
model: 'llama3',
messages: [
{ role: 'system', content: `Réponds en français en t'appuyant sur ce contexte :\n\n${context}` },
{ role: 'user', content: question },
],
});
return res.message.content;
}

Inserer des documents

import fs from 'fs';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';

const text = fs.readFileSync('documentation.md', 'utf-8');
const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 50 });
const chunks = await splitter.createDocuments([text]);

for (const chunk of chunks) {
const vec = await embed(chunk.pageContent);
await pool.query(
'INSERT INTO documents (content, metadata, embedding) VALUES ($1, $2, $3)',
[chunk.pageContent, JSON.stringify(chunk.metadata), JSON.stringify(vec)]
);
}

Recherche hybride (vecteur + filtre SQL)

async function searchByCategory(query, category) {
const vec = await embed(query);
return pool.query(
`SELECT content FROM documents
WHERE metadata->>'category' = $1
ORDER BY embedding <=> $2
LIMIT 10`,
[category, JSON.stringify(vec)]
);
}

Performances

VolumeIndexTemps de requête
10K vecteursHNSW~5 ms
100K vecteursHNSW~15 ms
1M vecteursHNSW~50 ms

Pour un blog technique ou une doc interne, 10–50 ms c’est plus que suffisant.

Pourquoi j’ai abandonné ChromaDB

  • pgvector tient dans PostgreSQL, ChromaDB est un service à part
  • pgvector permet les backups PostgreSQL standards, ChromaDB a son propre format
  • pgvector s’intègre aux ORM (Prisma, TypeORM), pas besoin de deux couches d’accès

Si vous avez déjà PostgreSQL, pgvector est un choix plus simple et plus maintenable qu’un vector store dédié.