You are currently viewing pgvector : la recherche vectorielle dans PostgreSQL pour un RAG sans brique supplémentaire

pgvector : la recherche vectorielle dans PostgreSQL pour un RAG sans brique supplémentaire

pgvector : la recherche vectorielle dans PostgreSQL pour un RAG sans brique supplémentaire

Quand on construit un pipeline RAG (Retrieval-Augmented Generation), la question du stockage des embeddings se pose rapidement. On a vu dans un précédent article comment déployer Qdrant, une base vectorielle dédiée. Mais si votre infrastructure repose déjà sur PostgreSQL, ce qui est le cas pour CKAN, Keycloak, Nextcloud, Gitea, Drupal et tant d’autres applications, il existe une alternative élégante : pgvector, une extension open source qui ajoute nativement le support des vecteurs et de la recherche par similarité directement dans PostgreSQL. Pas de service supplémentaire à déployer, pas de synchronisation à maintenir, pas de port réseau à ouvrir.

Pourquoi pgvector plutôt qu’une base vectorielle dédiée

Les bases vectorielles spécialisées (Qdrant, Milvus, Weaviate, Pinecone) offrent des performances remarquables sur de très grands volumes : des centaines de millions de vecteurs. Mais la majorité des cas d’usage en production ne nécessitent pas cette échelle. Pour un portail open data avec quelques dizaines de milliers de documents, un assistant interne sur une base documentaire de 50 000 pages, ou un moteur de recherche sémantique sur les métadonnées CKAN, pgvector suffit largement et présente des avantages décisifs.

Premier avantage : la simplicité opérationnelle. pgvector vit dans votre PostgreSQL existant. Pas de conteneur supplémentaire, pas de backup séparé, pas de monitoring dédié. Vos procédures de sauvegarde (BorgBackup), de tuning et de haute disponibilité (réplication, patroni) couvrent automatiquement les données vectorielles. Deuxième avantage : la cohérence transactionnelle. Vos embeddings et vos métadonnées structurées vivent dans la même base, la même transaction. Pas de risque de désynchronisation entre un document mis à jour dans PostgreSQL et son embedding encore présent dans une base vectorielle externe.
Troisième avantage : la possibilité de combiner recherche vectorielle et filtres SQL classiques dans une seule requête, par exemple, chercher les documents les plus proches sémantiquement parmi ceux publiés après une certaine date et appartenant à une organisation donnée.

Installation et configuration

pgvector s’installe comme n’importe quelle extension PostgreSQL. Sur une instance Docker, il suffit d’utiliser l’image officielle pgvector/pgvector:pg16 (ou pg15, pg17 selon votre version). Si vous gérez PostgreSQL directement sur le serveur, l’extension se compile depuis les sources en quelques commandes, ou s’installe via le gestionnaire de paquets sur les distributions courantes.

Une fois l’extension chargée, la configuration se fait en quelques lignes SQL :

-- Activer l'extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Créer une table avec une colonne vectorielle
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT,
    source_url TEXT,
    organization TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    embedding vector(1536)  -- dimension selon le modèle d'embedding
);

-- Créer un index HNSW pour la recherche rapide
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

Le type vector(1536) stocke des vecteurs de dimension 1536 -, la dimension standard des embeddings d’OpenAI text-embedding-3-small ou de modèles open source comme nomic-embed-text servi par Ollama. pgvector supporte des dimensions allant jusqu’à 2000 (et au-delà avec le type halfvec en demi-précision).

Types d’index : HNSW vs IVFFlat

pgvector propose deux algorithmes d’indexation. HNSW (Hierarchical Navigable Small World) est le choix par défaut recommandé : il offre les meilleures performances en lecture avec un temps de construction raisonnable. Le paramètre m contrôle le nombre de connexions par nœud (16 est un bon défaut), et ef_construction la qualité de construction de l’index (200 offre un bon compromis). IVFFlat est plus rapide à construire et consomme moins de mémoire, mais nécessite un entraînement préalable sur les données existantes, il convient mieux aux jeux de données statiques de grande taille.

Pour la plupart des cas d’usage, HNSW avec les paramètres par défaut offre un excellent rappel (>95 %) avec des temps de requête inférieurs à 10 ms sur des collections de quelques centaines de milliers de vecteurs.

Intégration dans un pipeline RAG avec Ollama

L’intégration de pgvector dans un pipeline RAG existant est directe. Voici le flux typique avec une stack entièrement open source :

Phase d’ingestion : un script Python (ou un workflow n8n) récupère les documents depuis CKAN, Nextcloud ou un répertoire local. Chaque document est découpé en chunks (paragraphes ou sections), puis chaque chunk est envoyé à Ollama pour générer son embedding via un modèle comme nomic-embed-text ou mxbai-embed-large. Le chunk et son embedding sont insérés dans la table pgvector.

import ollama
import psycopg2

# Générer l'embedding via Ollama
response = ollama.embeddings(
    model="nomic-embed-text",
    prompt=chunk_text
)
embedding = response["embedding"]

# Insérer dans PostgreSQL/pgvector
cur.execute("""
    INSERT INTO documents (title, content, source_url, organization, embedding)
    VALUES (%s, %s, %s, %s, %s::vector)
""", (title, chunk_text, url, org, str(embedding)))

Phase de requête : quand un utilisateur pose une question, son texte est transformé en embedding par le même modèle, puis pgvector cherche les chunks les plus proches par distance cosinus. Les résultats sont injectés comme contexte dans le prompt envoyé au LLM (Ollama, vLLM).

-- Recherche des 5 documents les plus proches
-- avec filtre SQL classique sur l'organisation
SELECT title, content, source_url,
       1 - (embedding <=> query_embedding::vector) AS similarity
FROM documents
WHERE organization = 'ville-de-rennes'
ORDER BY embedding <=> query_embedding::vector
LIMIT 5;

L’opérateur <=> calcule la distance cosinus. pgvector supporte également la distance euclidienne (<->) et le produit scalaire (<#>). La combinaison avec des clauses WHERE SQL classiques est le point fort de cette approche : pas besoin de pré-filtrer côté applicatif.

Performances et limites

pgvector gère confortablement des collections de quelques centaines de milliers à quelques millions de vecteurs sur un serveur modeste (16 Go de RAM, SSD). Au-delà de 5 à 10 millions de vecteurs en dimension 1536, les performances commencent à se dégrader et une base vectorielle dédiée comme Qdrant devient pertinente. Les paramètres clés à surveiller sont le maintenance_work_mem (augmenter pour accélérer la construction des index HNSW), le shared_buffers (pour garder l’index en mémoire), et le ef_search (ajustable par session pour arbitrer entre vitesse et rappel).

-- Augmenter la qualité de recherche pour une requête précise
SET hnsw.ef_search = 200;

-- Revenir au défaut pour les requêtes courantes
SET hnsw.ef_search = 40;

Quand choisir pgvector, quand choisir Qdrant

Le choix entre pgvector et une base vectorielle dédiée se résume souvent à une question de volume et de complexité opérationnelle. pgvector est le bon choix quand vous avez déjà PostgreSQL dans votre stack, que votre collection ne dépasse pas quelques millions de vecteurs, et que vous valorisez la simplicité de maintenance et la cohérence transactionnelle. Qdrant (ou Milvus, Weaviate) devient pertinent quand vous gérez des dizaines de millions de vecteurs, que vous avez besoin de fonctionnalités avancées comme le multi-tenancy natif, le filtrage par payload optimisé, ou la réplication distribuée.

En pratique, pgvector est souvent le bon point de départ. Il est plus simple à déployer, à sauvegarder et à monitorer. Et si les besoins évoluent, la migration vers une base dédiée reste possible — les embeddings sont les mêmes, seul le stockage change.

Docker Compose : ajouter pgvector à sa stack existante

# docker-compose.yml — extrait
services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    deploy:
      resources:
        limits:
          memory: 4G

volumes:
  pgdata:

Il suffit de remplacer l’image PostgreSQL standard par pgvector/pgvector:pg16. L’image est basée sur l’image officielle PostgreSQL avec l’extension pré-compilée. Aucune autre modification n’est nécessaire, vos bases, utilisateurs et données existants restent intacts.

Conclusion

pgvector transforme PostgreSQL en base vectorielle sans ajouter de complexité à votre infrastructure. Pour la majorité des projets RAG auto-hébergés : assistant sur données ouvertes, moteur de recherche sémantique interne, indexation documentaire, c’est le chemin le plus court entre vos documents et un LLM capable de les exploiter. Combiné avec Ollama pour les embeddings et l’inférence, n8n pour l’orchestration et Langfuse pour l’observabilité, il complète une stack RAG 100 % open source qui tient dans un seul docker-compose.yml.