Recherche lexicale BM25
Sur cette page
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
BM25 (Best Match 25) est un algorithme classique de recherche lexicale. Pour une question :
- il découpe la question en mots ;
- il compte combien de documents contiennent chaque mot ;
- il donne plus de poids aux mots rares ;
- 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_documentetsearch. - BM25 renvoie un score (plus grand est meilleur), l’index vectoriel une distance (plus petite est meilleure).