pgvector + Supabase
Recherche vectorielle dans Postgres : ACID, RLS, jointures SQL, un seul service à opérer.
Vue d'ensemble
pgvector est une extension Postgres open source (licence PostgreSQL, la même licence permissive de style BSD que Postgres lui-même) qui ajoute un type de colonne vectorielle, des opérateurs de distance, et deux types d'index : HNSW et IVFFlat. Les embeddings vivent dans la même base que l'état de l'application. Une écriture qui insère une ligne de document et son embedding forme une seule transaction : elle valide ou s'annule d'un seul bloc. Source : github.com/pgvector/pgvector.
L'avantage structurel sur un store vectoriel dédié, c'est la jointure. Une requête nearest-neighbor dans pgvector est un SELECT SQL : elle peut filtrer sur tenant_id, document_type ou n'importe quelle colonne relationnelle dans le même WHERE, et joindre les résultats vectoriels avec une table access_control dans le même plan de requête. Les politiques Row Level Security (RLS) s'appliquent automatiquement aux recherches vectorielles : la base garantit l'isolation par tenant, même si le code applicatif omet le filtre. Aucun saut réseau entre la couche vectorielle et la couche relationnelle.
La v0.5.0 (août 2023) a introduit HNSW, désormais l'index de référence en production. La v0.7.0 (avril 2024) a ajouté halfvec et la quantification binaire : comme HNSW et IVFFlat plafonnent le type vector à 2 000 dimensions, c'est halfvec (demi-précision) qui rend possible l'indexation jusqu'à 4 000 dimensions. La v0.8.0 (octobre 2024) a introduit les scans d'index itératifs (hnsw.iterative_scan) qui évitent le surbalayage quand les clauses WHERE sont très sélectives. La version courante est la 0.8.5. Supabase regroupe Postgres managé + pgvector + RLS + Auth dans un seul service avec régions EU, supprimant la charge DBA pour les équipes sans opérateur de base de données en interne. Sources : postgresql.org news, supabase.com.
Architecture
Le modèle mental est une table Postgres standard avec une colonne supplémentaire de type vector(n). Les requêtes de distance utilisent des opérateurs infixes : <=> (cosinus), <-> (L2 / euclidienne) et <#> (produit intérieur négatif), chacun associé à une opclass d'index correspondante (vector_cosine_ops, vector_l2_ops, vector_ip_ops). L'index HNSW réécrit le plan d'exécution en un scan nearest-neighbor approché. Toute la machinerie Postgres (transactions, WAL, RLS, partitionnement) s'applique à cette colonne sans modification.
Concepts clés
- vector(n)
- Le type de colonne. Stocke un tableau de n valeurs float32 de longueur fixe. n doit correspondre à la dimension de sortie du modèle d'embedding (ex. 1536 pour OpenAI text-embedding-3-small). HNSW et IVFFlat n'indexent un vector que jusqu'à 2 000 dimensions ; au-delà, on indexe un halfvec (float16, indexable jusqu'à 4 000 dims). Également disponibles : sparsevec (jusqu'à 1 000 dims nonzero), bit (binaire).
- HNSW
- Hierarchical Navigable Small World. L'index de production : meilleur compromis vitesse-rappel qu'IVFFlat, construction plus lente, mémoire plus importante. Paramètres : m (connexions par couche, défaut 16) et ef_construction (qualité de construction, défaut 64). Le rappel à la requête se règle via SET hnsw.ef_search = N (défaut 40 ; plus élevé = meilleur rappel, plus lent). Peut être construit sur une table vide.
- IVFFlat
- Inverted File Flat. Plus léger qu'HNSW : construction plus rapide, moins de mémoire. Divise les vecteurs en listes ; à la requête, cherche un sous-ensemble de listes. Nécessite des données représentatives à la création (construire sur une table vide produit des centroïdes sans sens). Paramètres clés : lists (nombre de partitions ; la recommandation officielle est lignes/1000 jusqu'à 1M de lignes, puis sqrt(lignes) au-delà de 1M, et non sqrt(lignes) partout) et ivfflat.probes (listes à interroger à la requête).
- ef_search
- Paramètre HNSW à la requête. Contrôle la taille de la liste de candidats dynamique pendant la recherche : ef_search plus élevé = meilleur rappel, requête plus lente. Se règle par session (SET hnsw.ef_search = 100) ou par transaction (SET LOCAL hnsw.ef_search = 100). Défaut : 40.
- Scan itératif
- hnsw.iterative_scan (ajouté en v0.8.0) : quand une clause WHERE est très sélective, un seul scan HNSW peut renvoyer trop peu de lignes qualifiantes. Le scan itératif ré-entre dans l'index plusieurs fois jusqu'à trouver suffisamment de résultats. Atténue le surbalayage au prix de traversées d'index supplémentaires. Activer avec SET hnsw.iterative_scan = relaxed_order.
- Row Level Security (RLS)
- Contrôle d'accès Postgres natif au niveau de la ligne. Avec pgvector, les politiques RLS s'appliquent aux requêtes de similarité exactement comme à n'importe quel SELECT : la base filtre les lignes que le rôle courant n'est pas autorisé à voir avant de renvoyer les résultats. Modèle standard : colonne tenant_id + USING (tenant_id = current_setting('app.tenant_id')::UUID). Sur Supabase, auth.uid() est directement disponible dans les politiques.
- Réglage de la construction d'index
- La construction d'un index HNSW sur un gros corpus est limitée par le CPU et la mémoire : c'est le point de friction opérationnel n°1 à grande échelle. Augmenter maintenance_work_mem pour que la construction reste en mémoire plutôt que de déborder sur disque, et augmenter max_parallel_maintenance_workers (défaut 2) pour la paralléliser : sur des tables de plusieurs millions de lignes, c'est la différence entre des minutes et des heures. Les constructions HNSW parallèles ont été introduites en v0.5.0.
Quand l'utiliser
Cas adaptés
- Les écritures d'embedding doivent être atomiques avec les écritures relationnelles : pgvector vit dans Postgres, donc l'insertion d'une ligne de document et de son embedding valide ou s'annule ensemble. Pas de problème de synchronisation en double écriture.
- RAG multi-tenant avec isolation par tenant : une seule table regroupant tous les embeddings avec une politique RLS sur tenant_id isole chaque tenant au niveau de la base. La base impose le filtre même si le code applicatif l'omet. Source : supabase.com/docs/guides/ai/rag-with-permissions.
- Les résultats vectoriels doivent être joints à des données relationnelles : résultats nearest-neighbor enrichis de métadonnées, enregistrements de contrôle d'accès ou préférences utilisateur dans une seule requête SQL. Aucun saut réseau vers un second service.
- Corpus inférieur à environ 1-5M de vecteurs sous charge concurrente modérée : HNSW atteint un p50 en millisecondes à cette échelle sans réglage mémoire ni partitionnement. Source : benchmarks vecstore.app.
- Équipes déjà sur Postgres voulant zéro surface d'ops supplémentaire : une seule chaîne de connexion, une seule sauvegarde, un seul tableau de bord de monitoring. Supabase (régions EU disponibles) supprime entièrement la charge DBA.
Anti-patterns
- Filtres de métadonnées très sélectifs à grande échelle : pgvector post-filtre l'ensemble de candidats HNSW après le scan nearest-neighbor approché. Quand une clause WHERE ne correspond qu'à ~10% ou moins du corpus, l'ANN doit surbalayer pour trouver suffisamment de lignes qualifiantes. Les bases vectorielles dédiées comme Qdrant filtrent à l'intérieur du parcours de graphe, ce qui est sensiblement plus rapide pour les requêtes sélectives. Le scan itératif de v0.8.0 (hnsw.iterative_scan) atténue le pire cas mais ne l'élimine pas. Source : tigerdata.com/blog/pgvector-vs-qdrant.
- Corpus très volumineux sous charge concurrente avec SLA p99 : au-delà d'environ 1-5M de vecteurs sous requêtes de similarité concurrentes soutenues, les bases vectorielles dédiées (qui maintiennent l'intégralité de l'index en mémoire, conçues à cet effet) garantissent un p99 plus stable. Une dégradation du p99 mesurée sur du trafic réel est un signal de basculement. Source : vecstore.app/blog/vector-database-performance-compared.
- Pression mémoire sur l'hôte Postgres : l'index HNSW est maintenu en mémoire lors des requêtes. Faire le calcul avant de dimensionner : 1,5M de vecteurs à 1536 dims représentent déjà ~9,2 Go de vecteurs float32 bruts (1,5M x 1536 x 4 octets), et l'index HNSW ajoute les liens du graphe par-dessus, environ 1,5x, soit ~13-14 Go à prévoir. Sur une instance sous-dimensionnée, l'éviction de l'index provoque des pics de latence. Dimensionner l'hôte Postgres pour garder l'index HNSW dans shared_buffers, utiliser halfvec pour le réduire de moitié environ, ou traiter la pression mémoire comme un signal de basculement.
- IVFFlat construit sur une table vide : IVFFlat nécessite des données représentatives à la création pour calculer des centroïdes pertinents. Construire sur une table vide produit des centroïdes aléatoires, ce qui donne un rappel désastreux. Toujours construire IVFFlat après avoir inséré un échantillon représentatif. HNSW n'a pas cette contrainte.
Exemples de code
Installer pgvector : extension, table, index HNSW
-- Enable the extension (once per database). On self-hosted Postgres this
-- needs superuser or rds_superuser. On Supabase, enable it from the dashboard
-- (Database > Extensions) instead, since you do not have a superuser role.
CREATE EXTENSION IF NOT EXISTS vector;
-- Documents table: relational columns alongside the embedding column
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL,
content TEXT NOT NULL,
embedding vector(1536), -- matches OpenAI text-embedding-3-small output
created_at TIMESTAMPTZ DEFAULT now()
);
-- On a large corpus, speed up the build first: keep it in memory and
-- parallelize it (this is the #1 operational pain point at scale).
SET maintenance_work_mem = '2GB';
SET max_parallel_maintenance_workers = 4; -- default is 2
-- HNSW index for cosine distance (production default)
-- m=16 and ef_construction=64 are solid starting defaults
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Tune recall at query time (session-level, default is 40)
-- Higher ef_search = better recall, slower query
SET hnsw.ef_search = 100;HNSW peut être construit sur une table vide (aucune étape d'entraînement). IVFFlat non : il requiert des données représentatives pour des centroïdes pertinents. Commencer par HNSW. La dimension vector(1536) doit correspondre exactement à la sortie du modèle d'embedding.
Requête de similarité avec filtre par tenant (SQL + Python)
-- Top-5 nearest neighbours filtered by tenant, ordered by cosine distance.
-- The SQL below uses positional placeholders ($1 = query embedding, $2 = tenant UUID);
-- the Python version uses psycopg named parameters (%(q)s, %(tenant)s).
-- SQL (prepared statement; $1 = embedding, $2 = tenant UUID):
SELECT id,
content,
1 - (embedding <=> $1) AS similarity
FROM documents
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 5;
-- Python equivalent using psycopg v3 + pgvector-python:
-- pip install "psycopg[binary]>=3.1" "pgvector>=0.3"
import psycopg
from pgvector.psycopg import register_vector
with psycopg.connect("postgresql://user:pass@host/db") as conn:
register_vector(conn)
query_emb = get_embedding(user_question) # your embedding call
rows = conn.execute(
"""
SELECT id, content, 1 - (embedding <=> %(q)s) AS similarity
FROM documents
WHERE tenant_id = %(tenant)s
ORDER BY embedding <=> %(q)s
LIMIT 5
""",
{"q": query_emb, "tenant": tenant_id},
).fetchall()L'opérateur <=> est la distance cosinus, qui va de [0, 2] (0 = identique, 1 = orthogonal, 2 = opposé). Donc 1 - (embedding <=> $1) est la similarité cosinus dans [-1, 1], et non [0, 1]. En pratique, les embeddings unitaires normalisés que produisent la plupart des modèles tombent dans [0, 1], mais ne comptez pas sur cette borne. Le filtre tenant_id est une clause WHERE standard : le scan d'index HNSW et le filtre relationnel s'exécutent dans le même plan de requête Postgres.
Row Level Security pour l'isolation multi-tenant
-- Step 1: add tenant_id column (already in schema above)
-- Step 2: enable RLS on the table
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Step 3a: Supabase pattern. auth.uid() is the USER id, not the tenant id.
-- tenant_id = auth.uid() only holds if one user maps to exactly one tenant.
-- For real B2B multi-tenant, resolve the user's tenants via a membership table.
CREATE POLICY tenant_isolation ON documents
AS PERMISSIVE FOR ALL
TO authenticated
USING (tenant_id IN (
SELECT tenant_id FROM memberships WHERE user_id = auth.uid()
));
-- Step 3b: self-hosted pattern: policy reads from a session variable
-- The application sets the variable before each query.
CREATE POLICY tenant_isolation ON documents
AS PERMISSIVE FOR ALL
TO app_role
USING (tenant_id = current_setting('app.tenant_id', TRUE)::UUID);
-- Application code (self-hosted): set the context before querying
BEGIN;
SET LOCAL app.tenant_id = '018e4567-e89b-12d3-a456-426614174000';
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
ORDER BY embedding <=> $1
LIMIT 5;
-- tenant_isolation policy enforces the filter automatically
COMMIT;Les politiques RLS s'appliquent automatiquement aux requêtes de similarité vectorielle, comme à tout SELECT. La base impose le filtre par tenant au niveau de la ligne, indépendamment du code applicatif. Sur Supabase, auth.uid() (l'identifiant de l'utilisateur authentifié) est disponible dans les politiques : aucun SET LOCAL n'est nécessaire, mais reliez-le à un tenant via une table de membership plutôt que de traiter l'identifiant utilisateur comme l'identifiant de tenant.
Comparatif
vs Qdrant
Performance de recherche filtrée, index optimisé mémoire, opérations vectorielles dédiées
pgvector post-filtre l'ensemble de candidats HNSW après le scan nearest-neighbor approché. Qdrant filtre à l'intérieur du parcours de graphe HNSW. Sur un corpus de 5M de vecteurs avec un filtre de sélectivité à 10%, Qdrant est sensiblement plus rapide. Le signal de basculement de pgvector vers Qdrant, c'est une dégradation du p99 mesurée sous des filtres sélectifs ou une charge concurrente soutenue au-delà de 1-5M de vecteurs. Démarrer avec pgvector ; ajouter Qdrant quand les chiffres réels montrent le croisement. Source : tigerdata.com/blog/pgvector-vs-qdrant, vecstore.app/blog/vector-database-performance-compared.
vs Pinecone
Simplicité managée face au contrôle et au coût
Pinecone est une base vectorielle entièrement managée : zéro infrastructure à gérer, mais les paramètres HNSW ne sont pas configurables (la documentation le confirme : "Pinecone does not support the ability to tune your index to control the accuracy performance trade-off"). Sa tarification managée au vecteur grimpe avec la taille du corpus et le volume de requêtes. pgvector sur Postgres auto-hébergé ou Supabase conserve le contrôle total du réglage HNSW et intègre le store vectoriel dans une base que vous opérez et payez déjà : pas de seconde facture, pas de synchronisation en double écriture. Choisir Pinecone pour le prototypage zéro-ops à petite échelle ; basculer vers pgvector quand la prévisibilité des coûts ou le réglage de l'index devient important.
vs Weaviate
Schéma orienté objet, API GraphQL, multi-modal
Weaviate propose l'inférence automatique de schéma, une API GraphQL et la recherche multi-modale (texte + image) d'emblée. Son système de modules gère la vectorisation, supprimant la nécessité de gérer un pipeline d'embedding séparément. La contrepartie : un nouveau service à opérer, un nouveau langage de requête à apprendre, et pas de jointures relationnelles. pgvector convient quand les données sont déjà dans Postgres et que la sémantique SQL est requise. Weaviate convient quand l'interface principale est la recherche vectorielle ou multi-modale et qu'on souhaite que l'écosystème de modules gère la vectorisation.
Ressources
- repopgvector GitHub repository (PostgreSQL License, releases, CHANGELOG)
- repopgvector releases (current 0.8.5; 0.8.2 fixed CVE-2026-3172 in parallel HNSW builds)
- docspgvector 0.8.0 release notes (iterative scan, improved ANN estimation)
- docspgvector 0.7.0 release notes (halfvec, sparsevec, binary quantization)
- docsRAG with permissions: pgvector + RLS on Supabase
- docspgvector on Neon: setup, index types, distance operators
- blogHNSW vs IVFFlat in pgvector: DBA guide (updated March 2026)
- blogpgvector vs Qdrant: filtered search benchmark
- blogVector database performance compared: pgvector vs Pinecone vs Qdrant (2026)
FAQ
- Quand utiliser pgvector plutôt qu'une base vectorielle dédiée ?
- pgvector est le bon choix par défaut pour la plupart des systèmes RAG et agentiques : il ajoute la recherche vectorielle à une instance Postgres existante sans service supplémentaire. Passer à une base vectorielle dédiée quand on dispose de preuves mesurées de dégradation : pics de latence p99 sous des filtres de métadonnées sélectifs (sélectivité ~10% ou moins), ou requêtes de similarité concurrentes soutenues au-delà de 1-5M de vecteurs. Mesurer d'abord, basculer sur les données, pas sur la capacité projetée. Source : vecstore.app/blog/vector-database-performance-compared.
- Quel index utiliser : HNSW ou IVFFlat ?
- Commencer par HNSW. Il offre un meilleur compromis vitesse-rappel, peut être construit sur une table vide (aucune étape d'entraînement) et se dégrade plus gracieusement sous charge. IVFFlat est plus léger en temps de construction et en mémoire, ce qui compte pour les très grands corpus statiques rarement réindexés. Si on choisit IVFFlat, le construire après avoir inséré des données représentatives : construire sur une table vide produit des centroïdes sans sens et un rappel désastreux. Dimensionner les listes IVFFlat (recommandation officielle) : lignes/1000 jusqu'à 1M de lignes, puis sqrt(lignes) au-delà de 1M, et non sqrt(lignes) à toutes les échelles. Source : dbi-services.com/blog/pgvector-a-guide-for-dba-part-2-indexes-update-march-2026.
- Le Row Level Security fonctionne-t-il avec les requêtes de similarité vectorielle ?
- Oui. Les politiques RLS s'appliquent à tout SELECT sur la table, y compris les recherches de similarité via <=>. La colonne vectorielle n'est qu'un type de colonne : Postgres filtre les lignes que le rôle courant n'est pas autorisé à voir avant de renvoyer les résultats. Sur Supabase, auth.uid() est directement disponible dans les politiques RLS. Sur Postgres auto-hébergé, le modèle standard consiste à exécuter SET LOCAL app.tenant_id = '...' avant la requête avec une politique USING (tenant_id = current_setting('app.tenant_id', TRUE)::UUID). Source : supabase.com/docs/guides/ai/rag-with-permissions.
- À quoi sert ef_search et quand le régler ?
- ef_search contrôle la taille de la liste de candidats dynamique lors d'une recherche HNSW. ef_search plus élevé = meilleur rappel, au prix de requêtes plus lentes. La valeur par défaut est 40. L'augmenter (ex. SET hnsw.ef_search = 100) quand on observe une dégradation du rappel sur le jeu d'évaluation. Le diminuer (ex. 20) quand on accepte un rappel plus faible et qu'on cherche un débit maximal. Se règle par session ou par transaction (SET LOCAL) pour varier selon le cas d'usage sans modifier l'index.
- Peut-on utiliser pgvector avec Supabase pour une résidence des données en UE ?
- Oui. Supabase propose des régions EU (Francfort, Irlande) et fournit un Postgres managé avec pgvector, RLS, Auth et le pooling de connexions dans un seul service. Pour des exigences de souveraineté plus strictes (secteur public français, HDS), auto-héberger Postgres + pgvector sur Scaleway fr-par ou Hetzner DE offre un contrôle de juridiction complet sans dépendance éditeur. pgvector est sous licence PostgreSQL (permissive, de style BSD) : aucune dépendance fournisseur sur l'extension elle-même.