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 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 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 hinter 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
Praktische Implementierung
Mit 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}")
Mit 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" } } } } )
Mit 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} )] )
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'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 |
BM25 optimieren
Tuning der Parameter k1 und 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
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:
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']
Query Expansion
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: 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
DEVELOPERpythones.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
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 ]
Nächste Schritte
Sparse Retrieval glänzt bei exakten Übereinstimmungen, aber es fehlt an semantischem Verständnis. Die Lösung? Beide Ansätze kombinieren.
- Hybride Fusion - Dense und Sparse kombinieren
- Dense Retrieval - Semantische Suche mit Embeddings
- Retrieval-Grundlagen - Überblick
FAQ
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
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.
Filtern nach Metadaten: RAG-Suche verfeinern
Beherrschen Sie das Filtern nach Metadaten für präzise RAG-Suchen. Filtertypen, Indexierung, kombinierte Abfragen und Optimierung.
Ensemble Retrieval: Mehrere retrievers kombinieren
Implementieren Sie Ensemble Retrieval, um die Stärken mehrerer retrievers zu kombinieren. Voting, stacking und fortgeschrittene Fusionsstrategien.