Sparse Retrieval und BM25: Wenn die lexikalische Suche überlegen ist
Entdecken Sie Sparse Retrieval und BM25 für eine präzise lexikalische Suche. Anwendungsfälle, Implementierung und Vergleich mit dem dense retrieval.
Sparse Retrieval et BM25 : Quand la recherche lexicale surpasse
Le sparse retrieval, incarné par l'algorithme BM25, reste une force dominante dans la recherche d'information. Malgré l'engouement pour les embeddings et le dense retrieval, la recherche lexicale surpasse souvent les approches sémantiques dans des cas bien précis. Ce guide explore les mécanismes du sparse retrieval, ses forces, et quand le privilégier.
Qu'est-ce que le sparse retrieval ?
Le sparse retrieval représente les documents par des vecteurs "creux" (sparse) où chaque dimension correspond à un terme du vocabulaire. La plupart des valeurs sont nulles car un document ne contient qu'une fraction du vocabulaire total.
Du TF-IDF au BM25
L'évolution des algorithmes de sparse retrieval :
TF-IDF (1972) → BM25 (1994) → BM25+ (2011)
Score = TF × IDF Saturation du TF Correction bias
Normalisation longueur documents courts
Comment fonctionne BM25
BM25 (Best Matching 25) calcule un score de pertinence basé sur la fréquence des termes de la requête dans les documents, avec des ajustements intelligents.
La formule BM25
Score(D, Q) = Σ IDF(qi) × (f(qi, D) × (k1 + 1)) / (f(qi, D) + k1 × (1 - b + b × |D|/avgdl))
Où :
f(qi, D): fréquence du terme qi dans le document D|D|: longueur du documentavgdl: longueur moyenne des documentsk1: paramètre de saturation (typiquement 1.2-2.0)b: paramètre de normalisation de longueur (typiquement 0.75)
Intuition derrière BM25
DEVELOPERpythondef bm25_score_explained(query_terms, document, corpus_stats): """ Intuitive Erklärung der BM25-Bewertung """ score = 0 doc_length = len(document) avg_length = corpus_stats['average_length'] for term in query_terms: # 1. IDF: seltene Begriffe = wichtiger # "algorithme" in 5% der Docs > "le" in 95% idf = compute_idf(term, corpus_stats) # 2. TF mit Sättigung: vermeidet Übergewichtung von Wiederholungen # "machine learning machine learning machine" ≠ 5× besser tf = document.count(term) saturated_tf = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * doc_length / avg_length)) # 3. Längennormalisierung: kompensiert lange Dokumente # Ein 10-seitiges Dokument mit 5 Erwähnungen ≠ eine 2-zeilige FAQ mit 5 Erwähnungen score += idf * saturated_tf return score
Implémentation pratique
Avec rank_bm25
DEVELOPERpythonfrom rank_bm25 import BM25Okapi import nltk # Préparation des documents documents = [ "Comment retourner un produit défectueux", "Politique de remboursement sous 30 jours", "Délais de livraison en France métropolitaine", "Frais de retour et conditions", "Service client disponible 24h/24" ] # Tokenisation tokenized_docs = [nltk.word_tokenize(doc.lower()) for doc in documents] # Création de l'index BM25 bm25 = BM25Okapi(tokenized_docs) # Recherche query = "retour produit" tokenized_query = nltk.word_tokenize(query.lower()) scores = bm25.get_scores(tokenized_query) # Résultats triés results = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True) for doc, score in results[:3]: print(f"Score: {score:.3f} - {doc}")
Avec Elasticsearch
DEVELOPERpythonfrom elasticsearch import Elasticsearch es = Elasticsearch() # Créer un index avec BM25 es.indices.create( index="knowledge_base", body={ "settings": { "similarity": { "custom_bm25": { "type": "BM25", "k1": 1.2, "b": 0.75 } } }, "mappings": { "properties": { "content": { "type": "text", "similarity": "custom_bm25", "analyzer": "french" # Analyseur français }, "category": {"type": "keyword"} } } } ) # Indexer les documents for i, doc in enumerate(documents): es.index(index="knowledge_base", id=i, body={"content": doc}) # Rechercher results = es.search( index="knowledge_base", body={ "query": { "match": { "content": { "query": "retour produit", "operator": "or" } } } } )
Avec Qdrant (sparse vectors)
DEVELOPERpythonfrom qdrant_client import QdrantClient from qdrant_client.models import ( SparseVectorParams, PointStruct, SparseVector, NamedSparseVector ) from collections import Counter import math client = QdrantClient("localhost", port=6333) # Collection mit sparse vectors client.create_collection( collection_name="bm25_docs", sparse_vectors_config={ "text": SparseVectorParams() } ) def compute_sparse_vector(text: str, idf_dict: dict) -> SparseVector: """Konvertiert einen Text in einen sparse vector im BM25-Stil""" tokens = text.lower().split() tf = Counter(tokens) indices = [] values = [] for token, freq in tf.items(): if token in idf_dict: token_id = hash(token) % 1000000 # Simple hash # Score TF-IDF simplifié score = freq * idf_dict.get(token, 1.0) indices.append(token_id) values.append(score) return SparseVector(indices=indices, values=values) # Indexer for i, doc in enumerate(documents): sparse_vec = compute_sparse_vector(doc, idf_dict) client.upsert( collection_name="bm25_docs", points=[PointStruct( id=i, payload={"content": doc}, vector={"text": sparse_vec} )] )
Quand le sparse retrieval surpasse le dense
1. Correspondance exacte requise
Requête : "Erreur 503"
Dense retrieval : trouve "Problème de serveur", "Site indisponible"
Sparse retrieval : trouve exactement "Erreur 503 - Service unavailable"
Pour les codes d'erreur, numéros de série, références produit, le sparse est imbattable.
2. Termes techniques ou rares
Requête : "Tenseur de covariance Riemannien"
Dense retrieval : confus, trouve des articles sur les tenseurs en général
Sparse retrieval : match exact sur les documents contenant ces termes précis
Les modèles d'embedding ont rarement vu ces termes spécialisés dans leur entraînement.
3. Recherches combinatoires
Requête : "Python asyncio websocket"
Dense retrieval : comprend le sens global mais peut manquer des combinaisons exactes
Sparse retrieval : trouve les documents contenant les 3 termes
4. Noms propres et entités
Requête : "Jean-Pierre Dupont facture 2024"
Dense retrieval : perd le nom propre dans l'embedding
Sparse retrieval : match exact sur le nom
Tableau comparatif
| Cas d'usage | Dense | Sparse | Gagnant |
|---|---|---|---|
| Reformulation sémantique | Excellent | Faible | Dense |
| Correspondance exacte | Faible | Excellent | Sparse |
| Termes techniques | Moyen | Excellent | Sparse |
| Synonymes | Excellent | Faible | Dense |
| Noms propres | Moyen | Excellent | Sparse |
| Requêtes longues | Excellent | Moyen | Dense |
| Requêtes 1-2 mots | Moyen | Excellent | Sparse |
Optimiser BM25
Tuning des paramètres k1 et b
DEVELOPERpythondef grid_search_bm25_params(queries, relevant_docs, corpus): """Finde die besten Parameter k1 und b""" best_score = 0 best_params = {} for k1 in [0.5, 1.0, 1.2, 1.5, 2.0]: for b in [0.25, 0.5, 0.75, 1.0]: bm25 = BM25Okapi(corpus, k1=k1, b=b) # Bewerten total_recall = 0 for query, relevant in zip(queries, relevant_docs): scores = bm25.get_scores(query) top_k = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:10] hits = len(set(top_k) & set(relevant)) total_recall += hits / len(relevant) avg_recall = total_recall / len(queries) if avg_recall > best_score: best_score = avg_recall best_params = {"k1": k1, "b": b} return best_params
Règles générales :
- Documents longs →
bplus élevé (0.75-1.0) - Documents courts (FAQ) →
bplus bas (0.3-0.5) - Requêtes répétitives →
k1plus bas (0.5-1.0)
Analyse linguistique
La qualité du sparse retrieval dépend fortement de la tokenisation et du preprocessing :
DEVELOPERpythonimport spacy nlp = spacy.load("fr_core_news_md") def preprocess_french(text: str) -> list[str]: """Preprocessing optimiert für Französisch""" doc = nlp(text.lower()) tokens = [] for token in doc: # Interpunktion und Stopwörter ignorieren if token.is_punct or token.is_stop: continue # Lemmatisation : "retournés" → "retourner" lemma = token.lemma_ # Filtrer les tokens trop courts if len(lemma) > 2: tokens.append(lemma) return tokens # Exemple text = "Les produits retournés seront remboursés sous 15 jours" tokens = preprocess_french(text) # ['produit', 'retourner', 'rembourser', 'jour']
Expansion de requête
DEVELOPERpythondef expand_query_synonyms(query: str, synonyms_dict: dict) -> str: """Enrichir la requête avec des synonymes""" expanded_terms = [] for term in query.split(): expanded_terms.append(term) if term in synonyms_dict: expanded_terms.extend(synonyms_dict[term]) return " ".join(expanded_terms) synonyms = { "retour": ["remboursement", "renvoi", "échange"], "produit": ["article", "commande", "achat"], "problème": ["souci", "erreur", "bug", "incident"] } query = "retour produit" expanded = expand_query_synonyms(query, synonyms) # "retour remboursement renvoi échange produit article commande achat"
BM25F : pour les documents structurés
BM25F étend BM25 aux documents avec plusieurs champs :
DEVELOPERpython# Configuration Elasticsearch avec boost par champ es.search( index="products", body={ "query": { "multi_match": { "query": "smartphone samsung", "fields": [ "title^3", # Titre : poids x3 "description^1", # Description : poids x1 "category^2" # Catégorie : poids x2 ], "type": "best_fields" } } } )
DEVELOPERpython# Implémentation manuelle BM25F def bm25f_score(query, document_fields, field_weights): """ BM25F : scoring multi-champs document_fields = { "title": "Samsung Galaxy S24", "description": "Smartphone haut de gamme avec écran AMOLED...", "category": "Téléphones" } field_weights = {"title": 3, "description": 1, "category": 2} """ total_score = 0 for field_name, content in document_fields.items(): weight = field_weights.get(field_name, 1) field_score = bm25_score(query, content) total_score += weight * field_score return total_score
Limites du sparse retrieval
1. Vocabulary mismatch
Document : "Véhicule électrique à batterie lithium"
Requête : "voiture écologique"
→ Score BM25 = 0 (aucun terme commun)
2. Sensibilité aux typos
Requête : "remboursment" (typo)
→ Ne trouve pas "remboursement"
Solution : Fuzzy matching
DEVELOPERpythones.search( index="knowledge_base", body={ "query": { "match": { "content": { "query": "remboursment", "fuzziness": "AUTO" # Tolère 1-2 erreurs } } } } )
3. Pas de compréhension du contexte
Requête : "Apple"
→ Trouve autant la marque que les recettes de pommes
Le sparse ne désambiguïse pas sans contexte additionnel.
Intégration dans un pipeline RAG
DEVELOPERpythonclass SparseRetriever: def __init__(self, documents: list[str]): self.documents = documents self.tokenized_docs = [self._preprocess(d) for d in documents] self.bm25 = BM25Okapi(self.tokenized_docs) def _preprocess(self, text: str) -> list[str]: # Tokenisation et normalisation return preprocess_french(text) def search(self, query: str, top_k: int = 5) -> list[dict]: tokenized_query = self._preprocess(query) scores = self.bm25.get_scores(tokenized_query) # Indices des top_k documents top_indices = sorted( range(len(scores)), key=lambda i: scores[i], reverse=True )[:top_k] return [ { "content": self.documents[i], "score": scores[i], "method": "bm25" } for i in top_indices ]
Prochaines étapes
Le sparse retrieval excelle sur les correspondances exactes, mais manque de compréhension sémantique. La solution ? Combiner les deux approches.
- Fusion hybride - Combiner dense et sparse
- Dense Retrieval - Recherche sémantique avec embeddings
- Fondamentaux du Retrieval - Vue d'ensemble
FAQ
Sparse retrieval optimisé avec Ailog
Ailog combine automatiquement sparse et dense retrieval pour des résultats optimaux :
- BM25 français natif avec lemmatisation et analyse linguistique
- Fusion hybride automatique adaptée à vos contenus
- Tuning des paramètres basé sur vos retours utilisateurs
- Zéro configuration - tout fonctionne dès l'import
Essayez gratuitement et bénéficiez du meilleur des deux mondes.
Tags
Verwandte Artikel
Query Routing: Anfragen an die richtige Quelle weiterleiten
Implementieren Sie Query Routing, um jede Anfrage zur optimalen Datenquelle zu leiten. Klassifizierung, LLM-Routing und fortgeschrittene Strategien.
Hybride Fusion: Dense- und Sparse-Retrieval kombinieren
Meistern Sie die hybride Fusion zur Kombination von semantischer und lexikalischer Suche. RRF, weighted fusion und optimale Kombinationsstrategien.
Dense Retrieval: Semantische Suche mit Embeddings
Beherrschen Sie Dense Retrieval für eine leistungsfähige semantische Suche. Embeddings, Modelle, vektorielle Indexierung und fortgeschrittene Optimierungen.