Architecture de Recherche Vectorielle : Construire des Systèmes de Recherche par Similarité Prêts pour la Production
Guide technique des systemes de recherche vectorielle. Decouvre les bases de donnees vectorielles, algorithmes d'indexation (HNSW, IVF) et scaling.
Pourquoi la Recherche Vectorielle est Importante Maintenant
Voici le problème avec la recherche traditionnelle : elle ne trouve que les correspondances exactes. Tu cherches "chaussures de course" et tu rates tous les résultats sur "baskets de jogging" parce que les mots ne correspondent pas. La recherche vectorielle résout cette limitation fondamentale en comprenant le sens, pas juste en matchant des mots-clés.
On a déployé des systèmes de recherche vectorielle pour la découverte de produits e-commerce, la recherche documentaire dans des pipelines RAG, et des moteurs de recommandation gérant des millions de requêtes par jour. La technologie a considérablement mûri ces deux dernières années, et l'outillage est enfin prêt pour la production.
La recherche vectorielle ne consiste pas seulement à trouver des éléments similaires. Il s'agit de construire des systèmes qui comprennent ce que les utilisateurs veulent vraiment dire, pas juste ce qu'ils tapent.
Laisse-moi te montrer comment construire ces systèmes correctement, du choix de la bonne base de données à la mise à l'échelle pour du vrai trafic.
Comment la Recherche Vectorielle Fonctionne Réellement
Avant de plonger dans les bases de données et les algorithmes, comprenons ce qu'on fait vraiment. La recherche vectorielle fonctionne en trois étapes :
Étape 1 : Créer les Embeddings
Tu prends tes données (texte, images, produits, peu importe) et tu les convertis en vecteurs avec un modèle d'embedding. Ces vecteurs sont des tableaux de nombres qui capturent le sens sémantique de ton contenu.
// Utiliser les embeddings OpenAI
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: "chaussures de course confortables pour marathons"
});
const vector = response.data[0].embedding;
// Retourne : [0.0023, -0.0142, 0.0089, ...] (1536 dimensions)
Étape 2 : Stocker et Indexer les Vecteurs
Tu stockes ces vecteurs dans une base de données spécialisée qui construit un index pour une récupération rapide. C'est là que la magie opère avec des algorithmes comme HNSW et IVF.
Étape 3 : Requêter par Similarité
Quand un utilisateur cherche, tu convertis sa requête en vecteur et tu trouves les voisins les plus proches dans ton index. La base de données retourne les éléments les plus similaires classés par distance.
| Composant | But | Exemple |
|---|---|---|
| Modèle d'Embedding | Convertit les données en vecteurs | OpenAI ada-002, Cohere embed-v3 |
| Base de Données Vectorielle | Stocke et indexe les vecteurs | Pinecone, Weaviate, Qdrant |
| Métrique de Distance | Mesure la similarité | Cosine, Euclidean, Dot Product |
| Algorithme ANN | Recherche approximative rapide | HNSW, IVF, PQ |
Choisir une Base de Données Vectorielle
Comparons les options principales. Je vais être honnête sur les compromis parce que chaque base de données excelle dans différents scénarios.
Pinecone : Simplicité Managée
Pinecone est le plus facile pour démarrer. Entièrement managé, scale automatiquement, et ça marche tout simplement. L'inconvénient ? Tu es enfermé dans leur infrastructure et les prix peuvent grimper à l'échelle.
import { Pinecone } from '@pinecone-database/pinecone';
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
const index = pinecone.index('products');
// Upsert des vecteurs
await index.upsert([
{
id: 'product-123',
values: embedding,
metadata: { category: 'chaussures', price: 129.99 }
}
]);
// Requête avec filtrage par métadonnées
const results = await index.query({
vector: queryEmbedding,
topK: 10,
filter: { category: { $eq: 'chaussures' } },
includeMetadata: true
});
Idéal pour : Les équipes qui veulent shipper vite sans gérer d'infrastructure. Les startups avec du financement. Les use cases sous 10M de vecteurs où le coût n'est pas la préoccupation principale.
Weaviate : Schema-First avec GraphQL
Weaviate prend une approche différente avec son design basé sur un schéma et son API GraphQL. Tu définis des classes d'objets avec des propriétés, et Weaviate gère la vectorisation automatiquement si tu le souhaites.
import weaviate from 'weaviate-ts-client';
const client = weaviate.client({
scheme: 'https',
host: 'your-cluster.weaviate.network'
});
// Définir le schéma
await client.schema.classCreator().withClass({
class: 'Product',
vectorizer: 'text2vec-openai',
properties: [
{ name: 'name', dataType: ['text'] },
{ name: 'description', dataType: ['text'] },
{ name: 'price', dataType: ['number'] }
]
}).do();
// Requête avec GraphQL
const result = await client.graphql
.get()
.withClassName('Product')
.withNearText({ concepts: ['chaussures de marathon'] })
.withLimit(10)
.withFields('name description price _additional { distance }')
.do();
Idéal pour : Les équipes qui construisent des knowledge graphs, les applications nécessitant une recherche hybride (mot-clé + vecteur), les projets où l'enforcement du schéma est important.
Qdrant : Orienté Performance et Open Source
Qdrant est écrit en Rust et optimisé pour la performance. C'est open source, peut être auto-hébergé, et a un excellent système de filtrage. On l'a vu gérer 50M+ vecteurs avec des latences sous 100ms sur du matériel modeste.
import { QdrantClient } from '@qdrant/js-client-rest';
const client = new QdrantClient({ url: 'http://localhost:6333' });
// Créer une collection avec des paramètres optimisés
await client.createCollection('products', {
vectors: {
size: 1536,
distance: 'Cosine'
},
optimizers_config: {
indexing_threshold: 20000
},
hnsw_config: {
m: 16,
ef_construct: 100
}
});
// Upsert avec payload
await client.upsert('products', {
points: [
{
id: 'product-123',
vector: embedding,
payload: { category: 'chaussures', price: 129.99, in_stock: true }
}
]
});
// Requête avec filtres complexes
const results = await client.search('products', {
vector: queryEmbedding,
limit: 10,
filter: {
must: [
{ key: 'category', match: { value: 'chaussures' } },
{ key: 'price', range: { lte: 150 } },
{ key: 'in_stock', match: { value: true } }
]
}
});
Idéal pour : Les déploiements auto-hébergés, les projets sensibles aux coûts à l'échelle, les applications nécessitant un filtrage complexe, les équipes qui veulent le contrôle sur l'infrastructure.
pgvector : Recherche Vectorielle dans PostgreSQL
Si tu fais déjà tourner PostgreSQL, pgvector te permet d'ajouter la recherche vectorielle sans une autre base de données. Ce n'est pas l'option la plus rapide, mais c'est souvent assez rapide et ça simplifie drastiquement ton architecture.
-- Activer l'extension
CREATE EXTENSION vector;
-- Créer une table avec colonne vectorielle
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT,
category TEXT,
price NUMERIC,
embedding VECTOR(1536)
);
-- Créer un index HNSW pour des requêtes plus rapides
CREATE INDEX ON products
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Requêter les plus proches voisins
SELECT id, name, price,
1 - (embedding <=> query_embedding) AS similarity
FROM products
WHERE category = 'chaussures' AND price < 150
ORDER BY embedding <=> query_embedding
LIMIT 10;
Idéal pour : Les équipes déjà sur PostgreSQL, les applications sous 5M de vecteurs, les use cases où tu as besoin de transactions ACID avec la recherche vectorielle, la réduction de la complexité opérationnelle.
Comparaison des Bases de Données Vectorielles
| Fonctionnalité | Pinecone | Weaviate | Qdrant | pgvector |
|---|---|---|---|---|
| Déploiement | Managé uniquement | Managé + Auto-hébergé | Managé + Auto-hébergé | Auto-hébergé |
| Échelle Max | Milliards | Centaines de millions | Centaines de millions | ~10 millions |
| Latence Requête | <50ms | <100ms | <50ms | <200ms |
| Filtrage | Bon | Excellent | Excellent | SQL natif |
| Courbe d'Apprentissage | Facile | Modérée | Facile | Minimale si tu connais SQL |
| Coût à l'Échelle | Élevé | Modéré | Faible (auto-hébergé) | Faible |
| Recherche Hybride | Limitée | Excellente | Bonne | Via recherche full-text |
Comprendre les Algorithmes d'Indexation
L'algorithme d'index détermine à quelle vitesse tu peux chercher dans des millions de vecteurs. Voici ce que tu dois savoir sur les deux approches les plus courantes.
HNSW : Hierarchical Navigable Small World
HNSW est l'algorithme dominant pour la recherche vectorielle. Il construit un graphe multi-couches où les couches supérieures ont moins de nœuds et des sauts plus grands, tandis que les couches inférieures ont plus de nœuds et des connexions plus courtes. La recherche commence en haut et descend.
| Paramètre | Ce qu'il contrôle | Compromis |
|---|---|---|
m | Nombre de connexions par nœud | Plus élevé = meilleur recall, plus de mémoire |
ef_construct | Largeur de recherche pendant la construction | Plus élevé = meilleure qualité, indexation plus lente |
ef_search | Largeur de recherche pendant les requêtes | Plus élevé = meilleur recall, requêtes plus lentes |
// Configuration HNSW Qdrant
const hnswConfig = {
m: 16, // 16 connexions par nœud (défaut)
ef_construct: 100, // Qualité de construction
full_scan_threshold: 10000 // Passer en brute force en dessous
};
// Pour des exigences de recall élevé
const highRecallConfig = {
m: 32,
ef_construct: 200
};
// Pour des environnements limités en mémoire
const lowMemoryConfig = {
m: 8,
ef_construct: 50
};
Avantages : Requêtes rapides, bon recall, fonctionne bien pour la plupart des use cases Inconvénients : Intensif en mémoire, mises à jour d'index lentes, pas idéal pour les données en streaming
IVF : Inverted File Index
IVF clusterise tes vecteurs et ne cherche que dans les clusters pertinents. C'est plus rapide à construire que HNSW et utilise moins de mémoire, mais a typiquement un recall plus faible.
| Paramètre | Ce qu'il contrôle | Compromis |
|---|---|---|
nlist | Nombre de clusters | Plus élevé = plus de précision, requêtes plus lentes |
nprobe | Clusters à chercher | Plus élevé = meilleur recall, requêtes plus lentes |
# Exemple Faiss IVF
import faiss
dimension = 1536
nlist = 100 # Nombre de clusters
# Créer un index IVF avec quantizer plat
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, nlist)
# Entraîner sur des données sample
index.train(training_vectors)
index.add(all_vectors)
# Chercher avec nprobe
index.nprobe = 10 # Chercher 10 clusters
distances, indices = index.search(query_vector, k=10)
Avantages : Efficace en mémoire, construction d'index rapide, bon pour les mises à jour en streaming Inconvénients : Recall plus faible que HNSW, nécessite une étape d'entraînement, plus de tuning nécessaire
Product Quantization : Compresser les Vecteurs
Quand la mémoire est limitée, la Product Quantization (PQ) compresse les vecteurs en les divisant en sous-vecteurs et en utilisant des codebooks. Tu échanges du recall contre des économies de mémoire drastiques.
# IVF + PQ pour une échelle massive
m = 8 # Nombre de sous-quantizers
bits = 8 # Bits par sous-vecteur
index = faiss.IndexIVFPQ(quantizer, dimension, nlist, m, bits)
# Réduit la mémoire ~32x comparé à un index plat
Utilise PQ quand : Tu as 100M+ vecteurs et tu ne peux pas te permettre le stockage en précision complète. Attends-toi à une perte de recall de 5-10%.
Métriques de Similarité : Choisir la Bonne Distance
La métrique de distance détermine comment la similarité est calculée. Le choix compte plus que tu ne le penses.
| Métrique | Formule | Idéal Pour |
|---|---|---|
| Cosine | 1 - (A.B / ||A|| ||B||) | Embeddings texte, vecteurs normalisés |
| Euclidean (L2) | sqrt(sum((A-B)^2)) | Embeddings image, features denses |
| Dot Product | A.B | Quand la magnitude compte, recommandations |
Règle générale : Si tes embeddings viennent d'un modèle texte (OpenAI, Cohere, etc.), utilise la similarité cosine. Les vecteurs sont déjà normalisés, et cosine les gère correctement. Pour les embeddings image ou les modèles custom, Euclidean fonctionne souvent mieux.
// La plupart des APIs d'embedding texte retournent des vecteurs normalisés
// Similarité cosine = dot product pour les vecteurs normalisés
const cosineSimilarity = (a, b) => {
return a.reduce((sum, val, i) => sum + val * b[i], 0);
};
// Distance euclidienne pour les vecteurs non normalisés
const euclideanDistance = (a, b) => {
return Math.sqrt(
a.reduce((sum, val, i) => sum + Math.pow(val - b[i], 2), 0)
);
};
Mettre à l'Échelle la Recherche Vectorielle pour la Production
C'est là que la théorie rencontre la réalité. Laisse-moi te partager ce qu'on a appris en scalant la recherche vectorielle pour gérer des millions de requêtes.
Stratégies de Sharding
Quand un seul nœud ne peut pas contenir tous tes vecteurs, tu dois sharder. Il y a deux approches principales :
Sharding Géographique : Diviser par région si les requêtes sont localisées
shard_fr: Produits France (5M vecteurs)
shard_eu: Produits UE (3M vecteurs)
shard_global: Produits Mondiaux (2M vecteurs)
Sharding par Hash : Distribuer uniformément entre les nœuds
const shardId = hash(productId) % numShards;
Sharding par Métadonnées : Diviser par catégorie ou attribut
shard_electronique: Produits Électroniques
shard_vetements: Produits Vêtements
shard_maison: Produits Maison & Jardin
Réplication pour la Disponibilité
Fais tourner au moins 3 réplicas pour les workloads de production. La recherche vectorielle est intensive en lecture, donc les réplicas aident aussi avec le débit.
# Configuration cluster Qdrant
cluster:
enabled: true
replication_factor: 3
shard_number: 6
# Pinecone (automatique)
# Sélectionne juste le tier "production" avec plusieurs pods
# Weaviate
replicationConfig:
factor: 3
Mettre en Cache les Requêtes Fréquentes
Certaines requêtes sont bien plus communes que d'autres. Cache-les.
import { Redis } from 'ioredis';
const redis = new Redis();
const CACHE_TTL = 3600; // 1 heure
async function searchWithCache(query, filters) {
const cacheKey = `vsearch:${hash(query)}:${hash(filters)}`;
// Vérifier le cache d'abord
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Requêter la base vectorielle
const embedding = await generateEmbedding(query);
const results = await vectorDB.search({
vector: embedding,
filter: filters,
limit: 20
});
// Cacher les résultats
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(results));
return results;
}
Batching et Traitement Asynchrone
Ne traite pas les vecteurs un par un. Batch tout.
// Mauvais : Traitement séquentiel
for (const doc of documents) {
const embedding = await generateEmbedding(doc.text);
await vectorDB.upsert({ id: doc.id, vector: embedding });
}
// Bon : Traitement par batch
const BATCH_SIZE = 100;
for (let i = 0; i < documents.length; i += BATCH_SIZE) {
const batch = documents.slice(i, i + BATCH_SIZE);
// Générer les embeddings en parallèle
const embeddings = await Promise.all(
batch.map(doc => generateEmbedding(doc.text))
);
// Upsert en batch
await vectorDB.upsert(
batch.map((doc, j) => ({
id: doc.id,
vector: embeddings[j],
metadata: doc.metadata
}))
);
}
Exemple d'Architecture de Production
Voici une vraie architecture qu'on utilise pour un système de recherche e-commerce gérant 10M+ produits et 1000+ requêtes par seconde :
┌─────────────────────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ API Gateway │
│ (Rate limiting, auth, routing) │
└─────────────────────┬───────────────────────────────────────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌────▼────┐
│ Cache │ │ Search │ │ Search │
│ (Redis) │ │ Node 1 │ │ Node 2 │
└───────────┘ └────┬────┘ └────┬────┘
│ │
┌───────▼───────────▼───────┐
│ Cluster Qdrant │
│ (3 shards, 3 réplicas) │
└───────────────────────────┘
│
┌───────────▼───────────────┐
│ Service Embedding │
│ (Accéléré GPU) │
└───────────────────────────┘
Composants Clés
Search Nodes : Services stateless qui gèrent le traitement des requêtes, la génération d'embeddings et le classement des résultats. On fait tourner 2-4 instances derrière un load balancer.
Cluster Qdrant : 3 shards pour la distribution des données, 3 réplicas pour la disponibilité. Chaque shard gère ~3.5M vecteurs. Mémoire totale : ~50GB sur le cluster.
Service Embedding : Service GPU dédié pour générer les embeddings. On utilise des modèles optimisés ONNX pour une inférence 10x plus rapide que les transformers vanilla.
Cache Redis : Cache les requêtes communes et les embeddings fréquents. Réduit la charge Qdrant de ~60%.
Pièges Courants et Comment les Éviter
Piège 1 : Ne Pas Normaliser les Vecteurs
Si tu utilises la similarité cosine mais que tes vecteurs ne sont pas normalisés, tu auras des résultats faux. La plupart des APIs d'embedding retournent des vecteurs normalisés, mais si tu utilises un modèle custom, normalise-les toi-même.
function normalize(vector) {
const magnitude = Math.sqrt(
vector.reduce((sum, val) => sum + val * val, 0)
);
return vector.map(val => val / magnitude);
}
Piège 2 : Ignorer le Modèle d'Embedding
Le modèle d'embedding compte plus que la base de données. Un bon modèle avec pgvector battra un mauvais modèle avec Pinecone. Prends le temps d'évaluer la qualité des embeddings avant d'optimiser l'infrastructure.
Piège 3 : Ne Pas Planifier les Mises à Jour
La plupart des bases vectorielles sont optimisées pour les lectures, pas les écritures. Si tu as besoin de mises à jour fréquentes, conçois en conséquence :
- Utilise des write-ahead logs
- Batch les mises à jour pendant les périodes de faible trafic
- Considère un index de staging pour les nouvelles données
Piège 4 : Trop Filtrer Avant la Recherche Vectorielle
Filtrer après la recherche vectorielle est généralement plus efficace que filtrer avant. Laisse l'index vectoriel faire son travail, puis filtre les résultats.
// Moins efficace : Filtrer d'abord, puis chercher dans les vecteurs restants
// Plus efficace : Chercher tout, puis filtrer les meilleurs résultats
const results = await vectorDB.search({
vector: queryEmbedding,
limit: 100 // Obtenir plus de résultats que nécessaire
});
const filtered = results.filter(r =>
r.metadata.price < maxPrice &&
r.metadata.in_stock
).slice(0, 10);
Monitoring et Observabilité
Tu ne peux pas améliorer ce que tu ne mesures pas. Surveille ces métriques :
| Métrique | Cible | Action si dépassée |
|---|---|---|
| Latence p50 | <50ms | Vérifier la configuration de l'index |
| Latence p99 | <200ms | Ajouter des réplicas ou shards |
| Recall@10 | >95% | Augmenter ef_search ou m |
| QPS par nœud | <1000 | Ajouter plus de nœuds |
| Utilisation mémoire | <80% | Sharder ou utiliser la compression PQ |
// Tracker la latence de recherche
const startTime = Date.now();
const results = await vectorDB.search(query);
const latency = Date.now() - startTime;
metrics.histogram('vector_search_latency_ms', latency, {
collection: 'products',
filter_count: Object.keys(query.filter || {}).length
});
// Tracker le recall (nécessite une ground truth)
const recall = calculateRecall(results, groundTruth);
metrics.gauge('vector_search_recall', recall);
Démarrer : Recommandations Pratiques
Si tu pars de zéro, voici ce que je recommanderais :
-
Moins de 1M vecteurs : Commence avec pgvector. C'est simple, probablement assez rapide, et tu connais déjà SQL.
-
1M-10M vecteurs : Qdrant auto-hébergé te donne le meilleur rapport performance/prix. Pinecone si tu ne veux pas gérer d'infrastructure.
-
10M-100M vecteurs : Qdrant ou Weaviate avec un sharding approprié. Considère le tier enterprise de Pinecone si le budget le permet.
-
100M+ vecteurs : Tu as besoin d'une architecture spécialisée. Considère Milvus, Qdrant multi-cluster, ou des solutions custom avec Faiss.
Commence avec des index HNSW (le défaut dans la plupart des bases). N'optimise que quand tu rencontres de vrais problèmes de performance. L'optimisation prématurée gaspille du temps sur des problèmes que tu n'as peut-être pas.
Conclusion
La recherche vectorielle est passée de curiosité de recherche à nécessité de production. Que tu construises de la recherche sémantique, des systèmes de recommandation ou des pipelines RAG, comprendre ces fondamentaux t'aidera à prendre de meilleures décisions architecturales.
La technologie est mature, l'outillage est bon, et la communauté a résolu la plupart des problèmes courants. Ce qui reste, c'est de choisir les bons outils pour ton use case spécifique et de les implémenter de manière réfléchie.
Si tu construis des systèmes de recherche vectorielle et que tu veux discuter architecture, on est toujours contents de partager ce qu'on a appris des déploiements en production.
Sujets couverts
Guides connexes
Guide Entreprise des Systèmes d'IA Agentiques
Guide technique des systemes d'IA agentiques en entreprise. Decouvre l'architecture, les capacites et les applications des agents IA autonomes.
Lire le guideCommerce Agentique : Comment laisser les agents IA acheter en toute securite
Comment concevoir un commerce agentique gouverne. Moteurs de politiques, portes d'approbation HITL, reçus HMAC, idempotence, isolation multi-tenant et le protocole Agentic Checkout complet.
Lire le guideLes 9 endroits où ton système IA laisse fuir des données (et comment colmater chacun)
Cartographie systématique de chaque point de fuite de données dans les systèmes IA. Prompts, embeddings, logs, appels d'outils, mémoire d'agent, messages d'erreur, cache, données de fine-tuning et transferts entre agents.
Lire le guidePrêt à construire des systèmes IA prêts pour la production ?
Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.
Démarrer une conversation