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èle | Dimensions | Où il tourne |
|---|---|---|
| gte-small | 384 | Intégré aux Edge Functions Supabase2 |
| OpenAI text-embedding-3-small | 1 536 par défaut | API OpenAI3 |
| OpenAI text-embedding-3-large | 3 072 par défaut | API 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.






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.