Aller au contenu principal

Recherche sémantique vs mots-clés

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 (type TF-IDF, BM25, Elasticsearch) repose sur la correspondance lexicale : elle cherche les mots exacts de la requête dans les documents.

Problèmes courants

# 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"

La recherche par mots-clés échoue face aux synonymes, aux reformulations et aux ambiguïtés.

La recherche sémantique

La recherche sémantique compare le sens des textes, pas leurs mots. Elle utilise les embeddings pour trouver les documents dont la signification est la plus proche de la requête.

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

La recherche sémantique retrouve les documents pertinents même avec des mots différents, et relègue le faux positif en bas du classement.

Comparaison structurée

CritèreMots-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 ?

Recherche par mots-clés

  • Recherche de codes produits, références, noms propres
  • Filtres exacts (dates, catégories)
  • Corpus très volumineux sans budget embedding

Recherche sémantique

  • FAQ, support client, documentation
  • Recherche en langage naturel
  • Corpus multilingue
  • Requêtes vagues ou exploratoires

Recherche hybride (le meilleur des deux)

Combinez 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

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']}")

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