Cherchez “chiot” dans un système qui ne compare que des mots-clés et un document sur l’éducation des chiens ne ressortira pas, même si n’importe qui lisant les deux les jugerait étroitement liés. La recherche par mots-clés compare des chaînes de caractères. Elle ne sait pas que “chiot” et “chien” veulent dire presque la même chose. Les embeddings règlent exactement ce problème. Ils transforment un texte en une liste de nombres, un vecteur, placé de sorte que des textes de sens proche se retrouvent près d’autres textes de sens proche, quels que soient les mots employés.
Comment un modèle d’embedding transforme le texte en nombres
Un modèle d’embedding est un réseau de neurones entraîné à placer du texte en un point d’un espace à haute dimension : 384, 768 ou 1536 dimensions sont des tailles courantes, selon le modèle. Il ne consulte pas un dictionnaire. À partir d’énormes quantités de texte, il a appris quels mots et expressions apparaissent dans des contextes similaires, et il encode cela dans la position vers laquelle pointe le vecteur.
La distance dans cet espace suit la proximité de sens. “Roi” et “reine” se retrouvent proches. “Roi” et “golden retriever” se retrouvent loin. Personne n’a dit au modèle de comparer ces mots ; c’est l’entraînement qui a produit cette géométrie, mot par mot.
Vous pouvez observer la même forme avec des nombres choisis à la main, avant même qu’un vrai modèle entre en jeu. Donnez à chaque mot un vecteur de 3 nombres (un vrai modèle en utilise des centaines) :
| |
king et queen s’accordent étroitement sur les deux premiers nombres. Pas dog. Pour transformer cet “accord étroit” en un score unique, il faut une métrique de similarité.
Cosine similarity, dot product et Euclidean distance comparés
Trois métriques reviennent constamment dans le travail avec les embeddings, chacune répondant à une question légèrement différente :
| Métrique | Ce qu’elle mesure | Quand l’utiliser |
|---|---|---|
| Cosine similarity | L’angle entre deux vecteurs, en ignorant leur longueur | Par défaut pour les embeddings de texte, car la plupart des modèles sont entraînés pour cela |
| Dot product | Angle et longueur ensemble | Le modèle a été explicitement entraîné pour un ranking par dot product (certains modèles de retrieval le sont) |
| Euclidean distance | Distance en ligne droite entre les deux points | Données spatiales ou basées sur des comptages ; moins courant pour le texte |
La cosine similarity est celle que vous utiliserez presque toujours. Beaucoup de modèles de text embedding normalisent leur sortie à une longueur de 1, donc seule la direction varie, et sur des vecteurs normalisés, cosine et dot product donnent le même classement. Le score va de -1 (opposé) à 1 (identique), 0 signifiant aucun lien. Voici le calcul sur les vecteurs choisis à la main :
| |
0,801 contre 0,375. king se retrouve bien plus proche de queen que de dog. Un vrai modèle produit le même type d’écart, réparti sur des centaines de dimensions apprises à partir de données plutôt que trois choisies à la main.
Comment générer des embeddings en Python
sentence-transformers est la bibliothèque open source de référence pour cela. Elle fonctionne en local, sans clé API :
| |
| |
all-MiniLM-L6-v2 transforme chaque phrase en un vecteur de 384 nombres, et model.similarity calcule la cosine similarity pour chaque paire. Les deux phrases sur le beau temps ne partagent presque aucun mot, et pourtant elles obtiennent un score élevé entre elles et faible face à celle sur le stade. Même schéma que l’exemple jouet, sauf que les nombres viennent maintenant d’un modèle qui a appris la langue.
Les API hébergées renvoient la même chose. Les endpoints text-embedding-3-small d’OpenAI et embed de Cohere donnent tous deux une liste de nombres décimaux par texte. Si vous faites déjà tourner des modèles en local, Ollama sert aussi des embeddings : ollama pull nomic-embed-text, puis appelez son endpoint /api/embed (la configuration est traitée dans Comment exécuter un LLM en local avec Ollama). La bibliothèque change. Le vecteur, et ce que vous en faites, non.
Comment la recherche de similarité vectorielle trouve la correspondance la plus proche
Calculer l’embedding d’un seul texte est la partie facile. Le vrai travail est presque toujours ailleurs : étant donné un vecteur de requête, trouver les correspondances les plus proches parmi des milliers ou des millions de vecteurs stockés. C’est la recherche de similarité vectorielle, et sa version directe est la force brute : noter la requête contre chaque vecteur stocké et garder les meilleurs résultats.
| |
Sur une machine ordinaire, cela affiche quelque chose proche de :
| |
Le temps croît linéairement avec le nombre de vecteurs, O(n) selon les termes de la notation Big O, mais NumPy rend chaque étape si peu coûteuse qu’un million de vecteurs prend largement moins de 100 ms. Cela cesse de suffire avec un trafic concurrent important, des dizaines de millions de vecteurs, ou les deux. Il faut alors un index approximate nearest neighbor (ANN), qui fait pour les vecteurs ce qu’un B-tree fait pour une clause WHERE dans une base de données (voir Comment Fonctionnent les Index de Base de Données). Deux types reviennent partout :
- HNSW (Hierarchical Navigable Small World) construit un graphe multicouche reliant chaque vecteur à ses voisins, si bien qu’une recherche parcourt le graphe vers la réponse au lieu de toucher chaque vecteur. Il offre le meilleur recall par milliseconde parmi les options courantes, au prix d’une mémoire plus importante et de constructions d’index plus lentes.
- IVFFlat regroupe les vecteurs en clusters autour de centroïdes, puis ne cherche que dans les clusters les plus proches de la requête. Il se construit plus vite et consomme moins de mémoire que HNSW, mais nécessite des données représentatives déjà chargées avant sa création. Construit sur une table vide, il produit de mauvais clusters et un mauvais recall.
Dans Postgres avec l’extension pgvector, chacun se résume à une instruction :
| |
Les deux échangent de l’exactitude contre de la vitesse. Ils peuvent manquer le vecteur réellement le plus proche, ce qui ne pose pas de problème pour la recherche et les recommandations, mais en pose pour tout cas qui exige la réponse exacte à chaque fois.
Erreurs courantes dans la recherche par embeddings
- Interroger avec un modèle et stocker avec un autre. Les vecteurs de deux modèles différents vivent dans des espaces incompatibles. Si vous changez de modèle, recalculez l’embedding de chaque document stocké avec le nouveau. Un mélange d’anciens et de nouveaux vecteurs renvoie des correspondances inutiles sans la moindre erreur.
- Classer par dot product sur des vecteurs jamais normalisés. Le dot product inclut la longueur, donc un vecteur long peut dépasser dans le classement un vecteur plus court et plus pertinent. Normalisez à l’écriture, ou utilisez l’opérateur cosine, qui ignore la longueur.
- Confondre proximité et exactitude. Un score cosine élevé signifie “apparaît dans des contextes similaires”, pas “est la bonne réponse”. Une requête sur les effets secondaires d’un médicament peut renvoyer un extrait sur le dosage de ce même médicament : lié, et faux. C’est la principale cause d’échec dans les pipelines RAG. La similarité réduit les candidats. Elle ne les vérifie pas.
- Ajouter un index ANN avant que la force brute ne soit vraiment trop lente. Le benchmark ci-dessus traite un million de vecteurs en moins de 100 ms. HNSW ou IVFFlat ajoutent du temps de construction, des paramètres à régler et des résultats approximatifs. Cela vaut le coup au-delà de dizaines de millions de vecteurs ou sous forte charge concurrente, pas par défaut.
- Oublier de recalculer l’embedding quand la source change. Un embedding est un instantané du texte au moment où vous l’avez calculé. Modifiez le document sans le recalculer et la recherche continuera de renvoyer l’ancienne version en toute confiance.
Avez-vous besoin d’une base de données vectorielle ?
Pas pour commencer. Calculez l’embedding de quelques centaines de vos documents réels avec sentence-transformers, gardez les vecteurs dans un tableau NumPy et cherchez-les avec la boucle à force brute ci-dessus. Pas de base de données, pas d’index. Passez à pgvector ou à une base de données vectorielle gérée une fois que la force brute devient mesurablement trop lente pour votre volume de données et de requêtes. N’ajoutez un index ANN par-dessus que lorsque le scan de table est le goulot d’étranglement que vous avez réellement mesuré.