Recherche sémantique vs mots-clés
Mis à jour le 29 juillet 2026
Objectifs
- Comprendre les limites de la recherche par mots-clés
- Découvrir comment la recherche sémantique résout ces problèmes
- Implémenter une recherche sémantique simple
Les limites de la recherche par mots-clés
La recherche classique — TF-IDF, BM25, Elasticsearch — repose sur la correspondance lexicale : elle cherche les mots exacts de la requête dans les documents. Cette mécanique est rapide, prévisible et parfaitement fiable tant que l’utilisateur emploie le vocabulaire des documents. Le problème, c’est qu’il ne le fait presque jamais.
Le petit corpus commenté ci-dessous montre les quatre situations typiques face à la requête « comment nourrir un chiot ».
# Recherche par mots-clés
requete = "comment nourrir un chiot"
# ✅ Trouvé : contient les mêmes mots
doc_a = "Comment nourrir un chiot de 3 mois"
# ❌ Pas trouvé : mots différents, même sens
doc_b = "L'alimentation du jeune chien"
# ❌ Pas trouvé : reformulation
doc_c = "Régime alimentaire pour les chiots nouveau-nés"
# ⚠️ Faux positif : mêmes mots, sens différent
doc_d = "Comment nourrir un projet de startup comme un chiot"
Seul le premier document est retrouvé, alors que le deuxième et le troisième répondent tout aussi bien à la question. Pire, le quatrième remonte en bonne place uniquement parce qu’il partage les mots de la requête, alors qu’il parle de gestion d’entreprise. Synonymes, reformulations et ambiguïtés : la recherche lexicale échoue sur les trois à la fois.
La recherche sémantique
La recherche sémantique compare le sens des textes, pas leurs mots. On encode le corpus une fois pour toutes, on encode la requête au moment où elle arrive, et on classe les documents par similarité.
from openai import OpenAI
import numpy as np
client = OpenAI()
def embed(texte: str) -> np.ndarray:
response = client.embeddings.create(
input=texte,
model="text-embedding-3-large"
)
return np.array(response.data[0].embedding)
# Corpus
documents = [
"Comment nourrir un chiot de 3 mois",
"L'alimentation du jeune chien",
"Régime alimentaire pour les chiots nouveau-nés",
"Comment nourrir un projet de startup comme un chiot",
"Les bases de la cuisine italienne",
]
# Indexer le corpus
corpus_embeddings = np.array([embed(doc) for doc in documents])
# Rechercher
requete = "comment nourrir un chiot"
query_embedding = embed(requete)
# Calculer les similarités
similarites = corpus_embeddings @ query_embedding
# Trier par pertinence
resultats = sorted(
enumerate(similarites), key=lambda x: x[1], reverse=True
)
for idx, score in resultats:
print(f"{score:.4f} | {documents[idx]}")
Résultat attendu :
0.9123 | Comment nourrir un chiot de 3 mois
0.8756 | Régime alimentaire pour les chiots nouveau-nés
0.8421 | L'alimentation du jeune chien
0.4532 | Comment nourrir un projet de startup comme un chiot
0.1245 | Les bases de la cuisine italienne
Les trois documents pertinents occupent les trois premières places malgré des formulations entièrement différentes, et le faux positif tombe à 0,45 — largement en dessous du seuil qui le ferait remonter. Le modèle a compris que « chiot » y sert de comparaison et non de sujet.
Comparaison structurée
| Critère | Mots-clés (BM25) | Sémantique (embeddings) |
|---|---|---|
| Synonymes | ❌ Échoue | ✅ Fonctionne |
| Reformulations | ❌ Échoue | ✅ Fonctionne |
| Multilingue | ❌ Impossible | ✅ Possible |
| Fautes d’orthographe | ❌ Échoue | ✅ Tolère |
| Correspondance exacte | ✅ Excellente | ⚠️ Parfois imprécise |
| Noms propres, codes | ✅ Excellente | ⚠️ Peut confondre |
| Vitesse (grand corpus) | ✅ Très rapide | ⚠️ Nécessite un index |
| Coût | ✅ Gratuit | ⚠️ Appels API |
Quand utiliser quoi ?
La lecture du tableau donne une règle simple : plus la requête est proche d’un identifiant, plus la recherche lexicale est indiquée. Une référence produit du type XR-4471-B, une date, une catégorie exacte ou un nom propre rare se cherchent au mot près, et un modèle d’embedding aura tendance à confondre cette référence avec ses voisines. C’est aussi le choix par défaut quand le corpus est massif et le budget d’embedding nul.
La recherche sémantique prend l’avantage dès que l’utilisateur s’exprime en langage naturel. Une FAQ, un support client, une documentation technique consultée par des non-spécialistes, un corpus multilingue où la question est posée en français et la réponse rédigée en anglais, ou encore une requête vague et exploratoire : dans tous ces cas, l’écart entre le vocabulaire de l’utilisateur et celui des documents est précisément ce que les embeddings savent franchir.
Ces deux profils étant complémentaires plutôt que concurrents, la meilleure réponse consiste souvent à combiner les deux approches pour couvrir tous les cas. C’est le sujet de la leçon 9.
Implémenter une recherche sémantique complète
En pratique, on encapsule l’indexation et l’interrogation dans une petite classe, ce qui évite de recalculer les vecteurs du corpus à chaque requête — l’erreur la plus coûteuse du débutant.
class RechercheSemantique:
def __init__(self, model: str = "text-embedding-3-large"):
self.client = OpenAI()
self.model = model
self.documents: list[dict] = []
self.embeddings: np.ndarray | None = None
def indexer(self, documents: list[dict]):
"""Indexe une liste de documents {id, texte, ...}."""
self.documents = documents
textes = [d["texte"] for d in documents]
response = self.client.embeddings.create(
input=textes,
model=self.model
)
vecteurs = sorted(response.data, key=lambda x: x.index)
self.embeddings = np.array(
[v.embedding for v in vecteurs], dtype=np.float32
)
def rechercher(self, requete: str, k: int = 5) -> list[dict]:
"""Recherche les k documents les plus pertinents."""
query_emb = self.client.embeddings.create(
input=requete,
model=self.model
).data[0].embedding
similarites = self.embeddings @ np.array(query_emb)
top_k = np.argsort(similarites)[-k:][::-1]
return [
{**self.documents[i], "score": float(similarites[i])}
for i in top_k
]
# Utilisation
moteur = RechercheSemantique()
moteur.indexer([
{"id": 1, "texte": "Comment installer Python sur Windows"},
{"id": 2, "texte": "Guide de configuration de l'environnement Python"},
{"id": 3, "texte": "Les bases du langage Python"},
])
resultats = moteur.rechercher("mettre en place Python sur mon PC")
for r in resultats:
print(f"[{r['score']:.3f}] {r['texte']}")
Testez ce moteur avec la requête « mettre en place Python sur mon PC » : aucun de ces cinq mots ne figure dans le document 1, qui parle d’« installer Python sur Windows ». C’est pourtant lui qui doit arriver en tête. Modifiez ensuite la requête pour parler de « configurer l’environnement » et observez le classement basculer vers le document 2 — vous tenez là un banc d’essai minimal pour vérifier le comportement de votre index avant de le remplir sérieusement.
Résumé
- La recherche par mots-clés échoue face aux synonymes et reformulations
- La recherche sémantique compare le sens via les embeddings
- Chaque approche a ses forces : la recherche hybride combine les deux
- Un moteur sémantique simple se construit en quelques dizaines de lignes