5. Retrieval

Sparse Retrieval und BM25: Wenn die lexikalische Suche überlegen ist

8. März 2026
Équipe Ailog

Entdecken Sie Sparse Retrieval und BM25 für eine präzise lexikalische Suche. Anwendungsfälle, Implementierung und Vergleich mit dem dense retrieval.

Sparse Retrieval und BM25: Wenn die lexikalische Suche überlegen ist

Sparse Retrieval, verkörpert durch den BM25-Algorithmus, bleibt eine dominierende Kraft in der Informationssuche. Trotz der Begeisterung für Embeddings und Dense Retrieval übertrifft die lexikalische Suche in bestimmten Fällen häufig die semantischen Ansätze. Dieser Leitfaden erkundet die Mechanismen des Sparse Retrieval, seine Stärken und wann es bevorzugt werden sollte.

Was ist Sparse Retrieval?

Sparse Retrieval stellt Dokumente durch "dünnbesetzte" (sparse) Vektoren dar, bei denen jede Dimension einem Begriff des Vokabulars entspricht. Die meisten Werte sind null, da ein Dokument nur einen Bruchteil des Gesamtvokabulars enthält.

Von TF-IDF zu BM25

Die Evolution der Sparse-Retrieval-Algorithmen:

TF-IDF (1972)           →    BM25 (1994)           →    BM25+ (2011)
Score = TF × IDF             Saturation du TF           Correction bias
                             Normalisation longueur      documents courts

Wie BM25 funktioniert

BM25 (Best Matching 25) berechnet einen Relevanzwert basierend auf der Häufigkeit der Suchbegriffe in den Dokumenten, mit intelligenten Anpassungen.

Die BM25-Formel

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 document
  • avgdl : longueur moyenne des documents
  • k1 : paramètre de saturation (typiquement 1.2-2.0)
  • b : paramètre de normalisation de longueur (typiquement 0.75)

Intuition hinter BM25

DEVELOPERpython
def 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

Praktische Implementierung

Mit rank_bm25

DEVELOPERpython
from 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}")

Mit Elasticsearch

DEVELOPERpython
from 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" } } } } )

Mit Qdrant (sparse vectors)

DEVELOPERpython
from 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} )] )

Wann Sparse Retrieval das Dense übertrifft

1. Exakte Übereinstimmung erforderlich

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. Technische oder seltene Begriffe

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. Kombinatorische Suchen

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. Eigennamen und Entitäten

Requête : "Jean-Pierre Dupont facture 2024"
Dense retrieval : perd le nom propre dans l'embedding
Sparse retrieval : match exact sur le nom

Vergleichstabelle

Cas d'usageDenseSparseGagnant
Reformulation sémantiqueExcellentFaibleDense
Correspondance exacteFaibleExcellentSparse
Termes techniquesMoyenExcellentSparse
SynonymesExcellentFaibleDense
Noms propresMoyenExcellentSparse
Requêtes longuesExcellentMoyenDense
Requêtes 1-2 motsMoyenExcellentSparse

BM25 optimieren

Tuning der Parameter k1 und b

DEVELOPERpython
def 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

Allgemeine Regeln:

  • Lange Dokumente → höheres b (0,75-1,0)
  • Kurze Dokumente (FAQ) → niedrigeres b (0,3-0,5)
  • Repetitive Anfragen → niedrigeres k1 (0,5-1,0)

Linguistische Analyse

Die Qualität des Sparse Retrieval hängt stark von Tokenisierung und Preprocessing ab:

DEVELOPERpython
import 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']

Query Expansion

DEVELOPERpython
def 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: für strukturierte Dokumente

BM25F erweitert BM25 auf Dokumente mit mehreren Feldern:

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

Grenzen des Sparse Retrieval

1. Vocabulary mismatch

Document : "Véhicule électrique à batterie lithium"
Requête : "voiture écologique"
→ Score BM25 = 0 (aucun terme commun)

2. Empfindlichkeit gegenüber Tippfehlern

Requête : "remboursment" (typo)
→ Ne trouve pas "remboursement"

Solution : Fuzzy matching

DEVELOPERpython
es.search( index="knowledge_base", body={ "query": { "match": { "content": { "query": "remboursment", "fuzziness": "AUTO" # Tolère 1-2 erreurs } } } } )

3. Kein Kontextverständnis

Requête : "Apple"
→ Trouve autant la marque que les recettes de pommes

Sparse Retrieval kann ohne zusätzlichen Kontext nicht disambiguieren.

Integration in eine RAG-Pipeline

DEVELOPERpython
class 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 ]

Nächste Schritte

Sparse Retrieval glänzt bei exakten Übereinstimmungen, aber es fehlt an semantischem Verständnis. Die Lösung? Beide Ansätze kombinieren.

FAQ

BM25 übertrifft Embeddings bei exakten Übereinstimmungen (Fehlercodes, Produktreferenzen), seltenen technischen Begriffen, die nicht im Vokabular der Modelle enthalten sind, Eigennamen und sehr kurzen Anfragen (1-2 Wörter). Es ist auch schneller, benötigt keine GPU und ist völlig transparent in seiner Funktionsweise. Für ein produktives RAG-System kombinieren Sie beide Ansätze.
Der Parameter k1 (typischerweise 1,2-2,0) steuert die Sättigung der Termhäufigkeit: Ein niedriger Wert bestraft übermäßige Wiederholung desselben Worts. Der Parameter b (typischerweise 0,75) passt die Normalisierung nach Dokumentlänge an: Je höher b, desto stärker werden lange Dokumente benachteiligt. Für kurze FAQs reduzieren Sie b (0,3-0,5). Für lange Artikel behalten Sie b hoch bei.
Natives BM25 scheitert bei Tippfehlern, da es exakte Begriffe vergleicht. Drei Lösungen: Fuzzy Matching (Elasticsearch fuzziness="AUTO" toleriert 1-2 Fehler), Rechtschreibkorrektur im Preprocessing oder Query Expansion mit generierten Varianten. Für ein robustes System kombinieren Sie BM25 mit Embeddings, die von Natur aus tolerant gegenüber orthographischen Abweichungen sind.
Ja, erheblich. Die Lemmatisierung ("retournés" wird zu "retourner") ermöglicht das Matching aller konjugierten und deklinierten Formen eines Wortes. Verwenden Sie SpaCy mit dem französischen Modell für eine qualitativ hochwertige Lemmatisierung. Ergänzen Sie auch die Entfernung von Stoppwörtern ("le", "de", "et"), die keinen unterscheidenden Wert haben. Diese Vorverarbeitungen können den Recall um 10-15% verbessern.
Ja, mit BM25F (Field-weighted BM25). Diese Variante ermöglicht es, verschiedenen Feldern unterschiedliche Gewichte zuzuweisen: Titel (Gewicht x3), Beschreibung (Gewicht x1), Kategorie (Gewicht x2). Eine Übereinstimmung im Titel zählt mehr als eine Übereinstimmung in der Beschreibung. Elasticsearch und Lucene unterstützen dieses Multi-Field-Boosting nativ. Dies ist wesentlich für Produktkataloge und strukturierte Dokumentenbasen. ---

Optimiertes Sparse Retrieval mit Ailog

Ailog kombiniert automatisch Sparse und Dense Retrieval für optimale Ergebnisse:

  • Natives französisches BM25 mit Lemmatisierung und linguistischer Analyse
  • Automatische hybride Fusion angepasst an Ihre Inhalte
  • Parameter-Tuning basierend auf Ihrem Nutzerfeedback
  • Null Konfiguration - alles funktioniert ab dem Import

Kostenlos testen und profitieren Sie vom Besten beider Welten.

Tags

ragretrievalbm25sparse retrievalrecherche lexicale

Verwandte Artikel

Ailog Assistant

Ici pour vous aider

Salut ! Pose-moi des questions sur Ailog et comment intégrer votre RAG dans vos projets !