Aller au contenu principal
Leçon 39 sur 67 · Claude sur Amazon Bedrock

Recherche lexicale BM25

Leçon 39 sur 67 Mis à jour le 5 novembre 2026 Développeur
Sur cette page
  1. Quand la recherche sémantique rate
  2. La recherche hybride
  3. Comment fonctionne BM25
  4. Une implémentation compacte
  5. L’essai
  6. À vous de jouer
  7. Points clés à retenir

En construisant un pipeline RAG, on découvre vite que la recherche sémantique seule ne suffit pas toujours. Il faut parfois une correspondance exacte de mots, qu’elle peut manquer. La solution : la combiner avec une recherche lexicale, l’algorithme BM25.

Quand la recherche sémantique rate

Cherchez un identifiant d’incident précis, comme « INC-2023-Q4-011 ». Le terme apparaît plusieurs fois dans les sections pertinentes, et pourtant la recherche sémantique peut remonter des sections au sens voisin qui ne le contiennent pas. Elle compare des sens, pas des chaînes de caractères. Quand il faut un mot exact, il faut une autre méthode.

La recherche hybride

L’idée : lancer les deux recherches et fusionner leurs résultats.

  • La recherche sémantique trouve les passages proches par le sens, grâce aux embeddings.
  • La recherche lexicale trouve les passages qui contiennent les mots exacts.
  • La fusion combine les deux classements, ce que fera la leçon 40.

Comment fonctionne BM25

Comment BM25 note un morceau Quatre étapes : la question découpée en mots, un poids plus fort pour les mots rares, un score qui combine présence, rareté et longueur du morceau, puis un classement par score décroissant. RECHERCHE LEXICALE Comment BM25 note un morceau 1 Mots la question découpée 2 Rareté les mots rares pèsent plus 3 Score présence, rareté, longueur 4 Classement le meilleur score d'abord « INC-2023-Q4-011 » pèse bien plus lourd que « le » ou « avec ».
D'après la leçon « BM25 lexical search » (Claude Academy, Claude with Amazon Bedrock).

BM25 (Best Match 25) est un algorithme classique de recherche lexicale. Pour une question :

  1. il découpe la question en mots ;
  2. il compte combien de documents contiennent chaque mot ;
  3. il donne plus de poids aux mots rares ;
  4. il note chaque morceau selon la présence des mots, pondérée par leur rareté et corrigée par la longueur du morceau.

Un terme rare comme « INC-2023-Q4-011 » pèse donc bien plus lourd que « le » ou « avec ».

Une implémentation compacte

Le cours officiel fournit une classe BM25Index dans son notebook. En voici une version complète, avec la même interface que VectorIndex :

import math
import re
from collections import Counter


def tokenize(text):
    return re.findall(r"\w+(?:-\w+)*", text.lower())


class BM25Index:
    def __init__(self, k1=1.5, b=0.75):
        self.k1, self.b = k1, b
        self.documents = []
        self.term_counts = []
        self.doc_freq = Counter()

    def add_document(self, document):
        counts = Counter(tokenize(document["content"]))
        self.documents.append(document)
        self.term_counts.append(counts)
        self.doc_freq.update(counts.keys())

    def search(self, query, k=1):
        n = len(self.documents)
        avg_len = sum(sum(c.values()) for c in self.term_counts) / n
        results = []
        for doc, counts in zip(self.documents, self.term_counts):
            length = sum(counts.values())
            score = 0.0
            for term in set(tokenize(query)):
                tf = counts.get(term, 0)
                if tf == 0:
                    continue
                df = self.doc_freq[term]
                idf = math.log(1 + (n - df + 0.5) / (df + 0.5))
                score += idf * tf * (self.k1 + 1) / (tf + self.k1 * (1 - self.b + self.b * length / avg_len))
            if score > 0:
                results.append((doc, score))
        results.sort(key=lambda pair: pair[1], reverse=True)   # score décroissant : le meilleur d'abord
        return results[:k]

Trois détails comptent. tokenize garde les tirets à l’intérieur des mots : « inc-2023-q4-011 » reste un seul terme. idf mesure la rareté d’un terme : plus il est rare, plus il pèse. Et les morceaux qui ne contiennent aucun mot de la question sont écartés : un score nul n’est pas un résultat.

L’essai

store = BM25Index()
for chunk in chunk_by_section(text):
    store.add_document({"content": chunk})

results = store.search("Que s'est-il passé avec INC-2023-Q4-011 ?", 3)
for doc, score in results:
    print(round(score, 2), doc["content"][:200], "\n----\n")

Dans la démonstration du cours officiel, BM25 remonte les sections d’ingénierie logicielle et de cybersécurité, qui parlent réellement de l’incident, là où la recherche sémantique proposait une analyse financière. Notez une différence d’échelle : BM25 renvoie un score, plus grand est meilleur, alors que VectorIndex renvoie une distance, plus petite est meilleure. La leçon suivante contourne cette différence.

À vous de jouer

Le mot exact. Sur votre document de la leçon 38, choisissez un terme rare (un code, un nom propre, une référence) et cherchez-le avec VectorIndex puis avec BM25Index. Faites de même avec une question formulée sans aucun mot du document. Laquelle des deux méthodes gagne dans chaque cas ?

Sources officielles : BM25 lexical search (Claude Academy) · Contextual Retrieval (Anthropic : BM25 dans un pipeline RAG)

Points clés à retenir

  • La recherche sémantique compare des sens ; elle peut manquer un terme exact comme un identifiant.
  • BM25 note les morceaux selon les mots de la question, pondérés par leur rareté.
  • Même interface que VectorIndex : add_document et search.
  • BM25 renvoie un score (plus grand est meilleur), l’index vectoriel une distance (plus petite est meilleure).