IA Engineering

Supabase Vector et pgvector : recherche sémantique et RAG dans Postgres

Activer pgvector dans Supabase, choisir le modèle d'embeddings et la taille de colonne, créer un index HNSW, écrire la fonction de recherche appelée par rpc() et construire un RAG. Code vérifié sur la documentation Supabase, pgvector et OpenAI.

Supabase Vector et pgvector

L’essentiel en 30 secondes

  • La recherche sémantique Supabase repose sur l'extension pgvector, une colonne vector(n) à la taille exacte du modèle, un index HNSW et une fonction SQL appelée avec rpc().
  • Utilisez le même modèle pour tous les vecteurs ; text-embedding-ada-002 est un modèle de génération précédente, préférez text-embedding-3-small (1 536 dimensions) ou gte-small (384).
  • Un index HNSW accepte jusqu'à 2 000 dimensions en vector et 4 000 en halfvec : au-delà, convertissez la colonne.
  • Mettez les filtres dans la fonction SQL, pas après rpc(), sinon la recherche peut renvoyer moins de résultats que demandé.

Pour faire de la recherche sémantique dans Supabase, il suffit de quatre éléments : l'extension Postgres pgvector, une colonne vector(n) dont la taille correspond exactement à votre modèle d'embeddings, un index HNSW, et une fonction SQL appelée depuis le client avec rpc(). Tout reste dans votre base Postgres, à côté des données métier et sous les mêmes règles RLS1.

Règle à ne jamais oublier : tous les vecteurs comparés doivent venir du même modèle. Comparer des embeddings produits par deux modèles différents donne des résultats sans signification1. Pour le contexte général, voir notre présentation complète de la plateforme Supabase.

Qu'est-ce qu'un embedding et quel modèle choisir ?

Un embedding est une liste de nombres qui représente le sens d'un texte : deux textes proches par le sens donnent des vecteurs proches. La taille du vecteur dépend du modèle1 :

ModèleDimensionsOù il tourne
gte-small384Intégré aux Edge Functions Supabase2
OpenAI text-embedding-3-small1 536 par défautAPI OpenAI3
OpenAI text-embedding-3-large3 072 par défautAPI OpenAI3

L'ancienne version de cet article utilisait text-embedding-ada-002 : OpenAI le présente désormais comme un modèle de génération précédente, au profit de la famille text-embedding-3. Les modèles text-embedding-3 acceptent un paramètre dimensions pour raccourcir les vecteurs3, et Supabase observe que moins de dimensions donne en général de meilleures performances2.

Comment activer pgvector et créer la table ?

Supabase installe l'extension dans le schéma extensions1 :

create extension if not exists vector with schema extensions;

create table public.documents (
  id bigint primary key generated always as identity,
  content text not null,
  metadata jsonb,
  embedding extensions.vector(1536), -- text-embedding-3-small
  created_at timestamptz default now()
);

alter table public.documents enable row level security;

-- Index HNSW pour la distance cosinus (m et ef_construction = valeurs par défaut de pgvector)
create index on public.documents
  using hnsw (embedding extensions.vector_cosine_ops)
  with (m = 16, ef_construction = 64);

HNSW est l'index à choisir par défaut : il sacrifie un peu d'exactitude contre beaucoup de débit. Avec pgvector 0.7.0 ou plus récent, un index accepte jusqu'à 2 000 dimensions en vector et 4 000 en halfvec ; pour text-embedding-3-large (3 072 dimensions), indexez donc la colonne convertie en halfvec(3072)4. Les paramètres m (16) et ef_construction (64) sont ceux par défaut de pgvector5.

Comment générer et stocker les embeddings ?

Côté serveur (route API, job, Edge Function), jamais dans le navigateur puisque la clé OpenAI et la clé secrète Supabase y seraient exposées :

import OpenAI from 'openai'
import { createClient } from '@supabase/supabase-js'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!)

export async function embed(text: string): Promise<number[]> {
  const res = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  })
  return res.data[0].embedding
}

export async function indexDocument(content: string, metadata = {}) {
  const embedding = await embed(content)
  return supabase.from('documents').insert({ content, metadata, embedding }).select('id')
}

Pour un texte long, découpez-le en passages avant de l'indexer : chaque passage devient une ligne avec son propre vecteur. Pour aller plus loin sur la structure des tables, voir structurer ses tables dans Supabase.

Comment écrire la fonction de recherche ?

pgvector propose trois opérateurs : <-> (distance euclidienne), <#> (produit scalaire négatif) et <=> (distance cosinus). La distance cosinus est le choix sûr ; si vos vecteurs sont normalisés, ce qui est le cas de ceux d'OpenAI, le produit scalaire est plus rapide et donne le même classement13.

create or replace function public.match_documents (
  query_embedding extensions.vector(1536),
  match_threshold float,
  match_count int
)
returns setof public.documents
language sql stable
as $$
  select *
  from public.documents
  where documents.embedding <=> query_embedding < 1 - match_threshold
  order by documents.embedding <=> query_embedding asc
  limit least(match_count, 200);
$$;
const queryEmbedding = await embed('Comment réinitialiser mon mot de passe ?')

const { data: documents, error } = await supabase.rpc('match_documents', {
  query_embedding: queryEmbedding,
  match_threshold: 0.78, // à calibrer sur vos données
  match_count: 10,
})

Le seuil de similarité n'a pas de valeur universelle : testez-le sur un échantillon de vos propres questions. Pour filtrer (par catégorie, par utilisateur), ajoutez le filtre comme paramètre de la fonction SQL plutôt que d'enchaîner .eq() après rpc() : un filtre appliqué après coup s'exécute sur les résultats déjà limités et peut en renvoyer moins que prévu1. Depuis pgvector 0.8.0, les parcours d'index itératifs (hnsw.iterative_scan, désactivé par défaut) compensent ce problème avec HNSW4. Pour les fonctions SQL en général, voir triggers, fonctions et procédures stockées Supabase.

Comment construire un RAG avec ces résultats ?

Un RAG (génération augmentée par récupération) enchaîne trois étapes : retrouver les passages pertinents, les injecter dans le prompt, demander au modèle de répondre à partir de ce contexte seulement. Le squelette, indépendant du fournisseur de LLM :

async function answer(question: string) {
  // 1. Retrouver les passages
  const { data: docs } = await supabase.rpc('match_documents', {
    query_embedding: await embed(question),
    match_threshold: 0.78,
    match_count: 5,
  })

  // 2. Construire le contexte
  const context = (docs ?? []).map((d) => d.content).join('\n\n---\n\n')

  // 3. Appeler le modèle de votre choix (fonction à écrire selon votre fournisseur)
  const reply = await callLLM({
    system: 'Réponds uniquement à partir du contexte fourni. Si la réponse n\'y est pas, dis-le.',
    user: `Contexte :\n${context}\n\nQuestion : ${question}`,
  })

  return { reply, sources: (docs ?? []).map((d) => d.metadata) }
}

Renvoyer les métadonnées des passages utilisés permet d'afficher les sources à l'utilisateur et de vérifier que la réponse s'appuie bien sur vos documents.

Et en Python ?

Supabase oriente vers deux voies : le client vecs pour les usages de data science ou ponctuels (il suffit d'une chaîne de connexion, copiée depuis l'option Shared pooler du bouton Connect), et, pour une application de production avec des migrations versionnées, les bibliothèques pgvector-python qui ajoutent le type vector à psycopg, asyncpg, SQLAlchemy, Django et consorts6. Notez que la dernière version publiée de vecs (0.4.5) date de décembre 20247.

# pip install vecs
import vecs

vx = vecs.create_client("postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:5432/postgres")
docs = vx.get_or_create_collection(name="documents", dimension=1536)

docs.upsert(records=[
    ("doc-1", embedding_1, {"title": "Introduction"}),
    ("doc-2", embedding_2, {"title": "Guide avancé"}),
])
docs.create_index()

results = docs.query(data=query_embedding, limit=5, include_metadata=True)

Pour concevoir un moteur de recherche ou un assistant sur vos propres documents, voir notre offre d'intégration de l'IA dans vos outils.

Sources

  1. Semantic search (documentation Supabase)
  2. Vector columns (documentation Supabase)
  3. Embeddings guide (documentation OpenAI)
  4. HNSW indexes (documentation Supabase)
  5. pgvector README (GitHub pgvector)
  6. Choosing a Client : Python (documentation Supabase)
  7. vecs, historique des versions (PyPI)

Écrit par

CTO & Chief Digital Strategist chez AdSim, Liège

Georges est CTO et Chief Digital Strategist d’AdSim.

  • Campaign Manager Brand Controls Basics
  • Bid Manager Brand Controls Basics
  • AdWords Video Brand Controls Basics

Questions fréquentes

Vos questions sur Supabase Vector

Faut-il un service de base vectorielle séparé à côté de Supabase ?

Pas nécessairement : pgvector stocke et indexe les vecteurs dans la même base Postgres que vos données, ce qui permet de filtrer, joindre et sécuriser par RLS dans une seule requête. Le choix d'une base dédiée se justifie surtout à très grand volume, à mesurer sur votre cas.

Peut-on changer de modèle d'embeddings en cours de route ?

Oui, mais il faut recalculer tous les vecteurs avec le nouveau modèle et, si la taille change, modifier la colonne et reconstruire l'index. Mélanger des vecteurs de deux modèles rend les comparaisons sans valeur.

Quel seuil de similarité utiliser ?

Il n'existe pas de valeur universelle. Partez de la valeur de l'exemple de Supabase (0,78) et ajustez-la en testant un échantillon de vraies questions de vos utilisateurs.

Commentaires

Chaque commentaire est relu avant publication, en général sous 24 h ouvrées. Les liens promotionnels ne sont pas publiés.

Aucun commentaire pour l’instant. Une question sur l’article ? Posez-la ci-dessous.

Laisser un commentaire

Jamais publié. Sert à vous prévenir d’une réponse.

Votre commentaire sera publié après relecture. Un lien au plus, pas de message promotionnel.

Point de départ

On applique cette méthode à votre compte ?

L’audit gratuit part de vos données, pas d’un exemple. Vous recevez le diagnostic sous 48 h ouvrées.

« Chez AdSim, c’est un vrai expert du digital qui lit votre demande et vous répond sous 48 h ouvrées. »

Valérie Matrige, CEO & co-fondatrice

Réponse sous 48 h ouvrées · Diagnostic 100 % gratuit · Sans engagement · Zéro revente de vos données