Base de données vectorielle Python : avec PostgreSQL et pgvector, vous pouvez stocker des embeddings et rechercher des textes proches par leur sens. L’enjeu est de vérifier que cette proximité retrouve les bons documents pour vos utilisateurs.
Une recherche sur « récupérer un accès perdu » devrait pouvoir retrouver une procédure intitulée « réinitialiser son mot de passe ». Un embedding peut aider à rapprocher ces formulations. Il ne prouve pas que la procédure convient à la situation, ni que l’utilisateur a le droit de la consulter.
Commencez par un corpus limité et des questions dont vous connaissez les réponses. L’exemple ci-dessous couvre la vectorisation des données et la recherche depuis Python. Ajoutez ensuite un index seulement lorsque les mesures le justifient.
Embeddings : que signifie vectoriser des données ?
Un embedding représente un texte par une suite de nombres produite par un modèle. La recherche compare le vecteur de la question à ceux des documents. La pertinence dépend notamment du modèle, de la langue et du découpage des textes ; elle n’est pas garantie par le stockage.
Pour cet exemple, text-embedding-3-small produit par défaut des vecteurs de 1 536 dimensions. Le guide officiel des embeddings documente cette dimension et le paramètre permettant de la réduire. La colonne PostgreSQL et les vecteurs envoyés doivent avoir la même taille.
Utilisez le même modèle et les mêmes paramètres pour les documents et les requêtes. Deux modèles peuvent produire des vecteurs de même dimension sans partager un espace comparable. Lors d’un changement, prévoyez une réindexation et une nouvelle évaluation.
Sur un document long, conservez des passages avec leur titre, leur identifiant source et leur version. Testez le découpage sur vos questions : un fragment trop court perd son contexte ; un fragment trop large mélange plusieurs réponses possibles.
Pourquoi PostgreSQL avec pgvector ?
Si votre application utilise déjà PostgreSQL, pgvector permet de conserver vecteurs et données métier dans la même base. Ce choix réduit le nombre de systèmes à exploiter, mais les calculs et les index consomment toujours des ressources. Il faut mesurer leur effet sur les autres requêtes.
PostgreSQL ne se limite pas aux correspondances exactes : sa recherche plein texte analyse les mots et permet de classer les résultats. Elle peut compléter la recherche sémantique, notamment pour des noms de produits ou des références précises. Un filtre SQL reste préférable pour une contrainte comme un statut ou un identifiant client.
Pour cadrer un produit, comparez d’abord la recherche existante à la variante vectorielle. Une base spécialisée devient une option si vos exigences de charge ou d’exploitation le justifient. Ce choix s’inscrit dans l’architecture du SaaS à développer, pas seulement dans le choix d’une bibliothèque Python.
Exemple Python : embeddings et recherche PostgreSQL
Il vous faut une base de développement PostgreSQL où l’extension pgvector est installée et activée. Le paquet Python pgvector fournit l’adaptation des types ; il n’installe pas l’extension PostgreSQL. Faites activer celle-ci avec un compte disposant des droits nécessaires :
CREATE EXTENSION IF NOT EXISTS vector;
Installez les bibliothèques dans votre environnement Python :
python -m pip install openai "psycopg[binary]" pgvector
Configurez OPENAI_API_KEY et DATABASE_URL via votre environnement ou gestionnaire de secrets. Le script appelle l’API d’embeddings : les textes lui sont transmis et ces appels peuvent être facturés. Les trois documents sont fictifs et servent uniquement à illustrer le parcours.
import os
import psycopg
from openai import OpenAI
from pgvector import Vector
from pgvector.psycopg import register_vector
MODEL = 'text-embedding-3-small'
DIMENSIONS = 1536
client = OpenAI()
def create_embedding(text: str) -> Vector:
response = client.embeddings.create(
model=MODEL,
input=text,
dimensions=DIMENSIONS,
)
values = response.data[0].embedding
if len(values) != DIMENSIONS:
raise ValueError('Unexpected embedding dimensions')
return Vector(values)
documents = [
('password-reset', 'Pour réinitialiser votre mot de passe, ouvrez la page de connexion.'),
('invoice-export', 'Les factures sont téléchargeables depuis la rubrique facturation.'),
('semantic-search', 'pgvector permet de rechercher des vecteurs proches dans PostgreSQL.'),
]
embedded_documents = [
(document_id, content, create_embedding(content))
for document_id, content in documents
]
query_vector = create_embedding('Comment récupérer mon accès au compte ?')
with psycopg.connect(os.environ['DATABASE_URL']) as connection:
register_vector(connection)
connection.execute(
'''
CREATE TABLE IF NOT EXISTS vector_search_demo (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(1536) NOT NULL
)
'''
)
for document_id, content, embedding in embedded_documents:
connection.execute(
'''
INSERT INTO vector_search_demo (id, content, embedding)
VALUES (%s, %s, %s)
ON CONFLICT (id) DO UPDATE
SET content = EXCLUDED.content, embedding = EXCLUDED.embedding
''',
(document_id, content, embedding),
)
results = connection.execute(
'''
SELECT id, content, 1 - (embedding <=> %s) AS similarity
FROM vector_search_demo
ORDER BY embedding <=> %s
LIMIT 3
''',
(query_vector, query_vector),
).fetchall()
for document_id, content, similarity in results:
print(f'{document_id}: {similarity:.3f} — {content}')
L’adaptateur register_vector suit l’intégration officielle pgvector pour Psycopg 3. Les valeurs passent séparément de la requête, avec les paramètres %s de Psycopg. N’interpolez pas le texte utilisateur dans le SQL.
Le script met à jour les documents par identifiant stable et recalcule leurs embeddings à chaque exécution. C’est un choix de démonstration ; en production, détectez les modifications avant de recalculer. Aucun score attendu n’est figé : vérifiez le classement obtenu, puis testez des formulations différentes et des questions hors sujet.
Distance et index : séparer pertinence et vitesse
L’opérateur <=> mesure la distance cosinus ; 1 - distance donne la similarité. Ce score n’est pas une probabilité de réponse correcte. Choisissez un éventuel seuil à partir de requêtes annotées, jamais d’une valeur supposée universelle.
| Comparaison | Opérateur pgvector | Classe d’index |
|---|---|---|
| Distance cosinus | <=> | vector_cosine_ops |
| Distance euclidienne L2 | <-> | vector_l2_ops |
| Produit scalaire négatif | <#> | vector_ip_ops |
Sans index approximatif, la recherche est exacte pour la distance choisie. Cela ne garantit pas une pertinence métier parfaite. HNSW et IVFFlat échangent une partie du rappel contre de la vitesse. HNSW utilise davantage de mémoire et se construit généralement plus lentement qu’IVFFlat ; IVFFlat doit être construit sur des données représentatives. Ces compromis sont décrits dans la documentation officielle pgvector.
Après une mesure sur un corpus réaliste, vous pouvez tester HNSW :
CREATE INDEX IF NOT EXISTS vector_search_demo_embedding_hnsw
ON vector_search_demo USING hnsw (embedding vector_cosine_ops);
Gardez le tri direct par distance avec LIMIT, comme dans le script. Vérifiez le plan avec EXPLAIN (ANALYZE, BUFFERS) : sur trois lignes, PostgreSQL peut préférer un parcours séquentiel. Un index créé n’est pas nécessairement utilisé.
Évaluer la recherche avant de la brancher à un LLM
Créez un jeu de questions avec les documents pertinents identifiés manuellement. Incluez synonymes, références exactes, fautes de frappe et demandes sans réponse dans le corpus. Voici les mesures que je recommande de distinguer :
- Pertinence métier : proportion de documents pertinents retrouvés parmi les premiers résultats, et rang du premier résultat utile.
- Rappel de l’index : proportion des voisins exacts retrouvés par la recherche approximative, à nombre de résultats identique.
- Latence : temps de vectorisation de la question, temps SQL et délai total, avec les filtres réellement utilisés.
- Coût : appels d’embeddings, stockage, mémoire de l’index et recalcul après modification des documents.
- Accès : absence de résultats interdits et comportement lorsque le corpus autorisé ne contient pas de réponse.
Pour une comparaison reproductible, fixez le même nombre k de résultats pour toutes les variantes. Le rappel métier rapporte les documents pertinents retrouvés aux documents pertinents connus. Le rappel de l’index compare, lui, les voisins approximatifs aux voisins exacts : ne confondez pas ces deux diagnostics.
Mesurez d’abord sans index approximatif, puis avec HNSW sur le même corpus. Si les résultats exacts sont déjà mauvais, changer d’index ne corrigera pas le modèle d’embeddings ou le découpage. Comparez aussi une recherche plein texte et une combinaison des deux approches.
Avec des filtres, un index approximatif peut renvoyer moins de résultats que prévu. pgvector propose des parcours itératifs depuis la version 0.8.0 pour explorer davantage de candidats, dans des limites configurables. Testez cette situation avec les droits d’accès réels, sans supprimer les filtres pour gagner du rappel.
En production, conservez la version du modèle, l’empreinte du contenu et la provenance de chaque passage. Prévoyez aussi la suppression des embeddings lorsqu’une source disparaît. Ces opérations de synchronisation font partie du coût du moteur de recherche.
Une fois la récupération validée, un LLM peut exploiter les passages pour rédiger une réponse. Il faudra alors évaluer aussi ses citations et sa fidélité aux sources. Mon accompagnement en ingénierie IA et LLM couvre ce passage de la recherche au service utilisable.
Ce qu'il faut retenir
Une base de données vectorielle avec Python ne se résume pas à insérer des listes de nombres. Le modèle d’embeddings, le découpage et les permissions déterminent ce que la recherche peut retrouver. PostgreSQL et pgvector permettent de tester cette approche dans une base relationnelle.
Commencez par mesurer la pertinence avec une recherche exacte. Ajoutez un index approximatif lorsque la latence le justifie, puis vérifiez le rappel et le coût avec les filtres de votre application.
Questions fréquentes
Une base vectorielle crée-t-elle elle-même les embeddings ?
Dans cet exemple, non : Python appelle un modèle d’embeddings, puis PostgreSQL stocke les vecteurs. Certaines solutions intègrent cette étape, mais le choix du modèle et l’évaluation restent nécessaires.
Peut-on utiliser un modèle local ?
Oui, à condition de produire des vecteurs compatibles pour les documents et les questions. Adaptez la dimension de la colonne, vérifiez les limites du type et de l’index choisis, puis réindexez le corpus avec ce modèle.
Faut-il utiliser HNSW dès le prototype ?
Non. Avec peu de documents, commencez sans index approximatif. Mesurez le temps SQL avant d’ajouter HNSW, puis comparez ses résultats à la recherche exacte sur les mêmes questions.
Conclusion
Pour votre première base de données vectorielle Python, choisissez un corpus limité et un besoin de recherche précis. Le prototype devient utile lorsque vous savez expliquer ses résultats, ses erreurs et le coût de sa mise à jour.
Vous voulez valider cette recherche dans votre produit ? Réservez un échange de 30 minutes pour cadrer les données et les critères de réussite.
