Startseite / Artikel

Jenseits der Top-K-Ähnlichkeit: BM25, hybride Suche, Reranking und MMR in RAG

Jenseits der top-K-Ähnlichkeit in RAG: BM25 für exakte Begriffe, hybrische Suche mit RRF, Cross-Encoder-Reranking sowie MMR zur Vielfalt – alles zusammen in einem einzigen Pipeline-Verfahren.

Verlag: REACTAPP.TOP7 Min. Lesezeit1698 Wörter

Quellen: Retrieval Strategies in RAG: Beyond Basic Similarity Search

Das erste RAG-System, das die meisten Entwickler erstellen, folgt einem bestimmten Ansatz: Die Dokumente werden eingebettet und in eine Vektordatenbank gespeichert. Wenn eine Frage eintrifft, werden die am nächsten liegenden Textabschnitte ausgewählt und an das LLM übergeben. Das funktioniert – bis es plötzlich nicht mehr funktioniert. Die Nutzer beginnen zu klagen, dass offensichtliche Antworten übersehen werden. In den Dokumenten befindet sich zwar eindeutig die gewünschte Information, doch die Suche bringt sie nicht zum Vorschein, oder der am besten passende Treffer ähnelt zwar auf den ersten Blick der Frage, entspricht aber nicht dem, was der Nutzer wirklich braucht. Eine einfache Ähnlichkeitssuche ist ein guter Ausgangspunkt, weist jedoch klare Grenzen auf. Sobald diese erkennbar werden, beheben bessere Suchstrategien diese Probleme. Hier werden die Gründe für das Scheitern erläutert, wie hybride Suchverfahren und Neubewertungen dagegen helfen, sowie wie alle Komponenten zu einem integrierten Workflow zusammengefügt werden können.

Ähnlichkeitssuche – kurz erklärt

In einer einfachen Konfiguration wird die Frage des Benutzers zu einem Embedding, einer Liste von Zahlen, die seine Bedeutung darstellen. Jeder Datensatz im Vektorlager besitzt ebenfalls ein Embedding. Das System findet die Datensätze, deren Embeddings am nächsten an denen der Frage liegen – gemessen an der Kosinusähnlichkeit oder dem Skalarprodukt – und gibt die besten K Ergebnisse zurück.

# Basic similarity search — what most RAG systems do
results = vector_store.similarity_search(query, k=3)

Es eignet sich gut für allgemeine semantische Abgleiche. Eine Frage dazu, wie die Photosynthese funktioniert, liefert Informationen darüber, wie Pflanzen Sonnenlicht in Energie umwandeln, selbst wenn die Formulierung unterschiedlich ist. Allerdings hat es Schwachstellen.

Wo es versagt

Genaue Schlüsselwörter werden übersehen. Die semantische Suche erfasst die Bedeutung statt die wörtlichen Wörter, was in der Regel vorteilhaft ist. Wenn jemand jedoch nach einem spezifischen Begriff sucht – wie einem Produktnamen, einem Akronym oder einer Person – kann die Embedding-Struktur nicht ausreichend genau übereinstimmen. Nehmen wir an, der Benutzer fragt nach der Architektur des BERT-Modells. Es existiert zwar ein Abschnitt, der besagt, dass BERT bidirektionale Transformer verwendet, doch ein umfassenderer Abschnitt über Sprachmodellarchitekturen kann an Priorität gewinnen, weil er semantisch breiter gefasst ist.

Eine hohe Punktzahl bedeutet nicht automatisch die beste Antwort. Ähnlichkeitsmaße messen, wie eng miteinander zwei Texte verbunden sind, nicht, wie nützlich einer von ihnen ist, um die Frage des anderen zu beantworten. Ein Abschnitt, der besagt, dass RAG-Systeme für die Fragebeantwortung nützlich sind, steht der Anfrage „Was ist RAG?“ sehr nahe, während der Abschnitt mit der eigentlichen Definition nützlicher ist.

Auch die besten Ergebnisse können redundant sein. Reale Dokumente wiederholen sich in verschiedenen Abschnitten, sodass die drei besten Ergebnisse oft fast dasselbe aussagen und dadurch Platz im Kontextfenster durch doppelte Inhalte verschwendet wird.

BM25 für das Schlüsselwortabrufen

BM25 ist ein klassischer Informationssuchalgorithmus, der hinter herkömmlichen Suchmaschinen steht. Er vergleicht Dokumente anhand exakter Schlüsselwörter, gewichtet jedes davon nach seiner Seltenheit im gesamten Korpus und verwendet keine Embeddings, sondern nur Rohtext. Eine Suche nach „FAISS index“ liefert Abschnitte, die buchstäblich beide Wörter enthalten, wobei das seltene Wort mehr Gewicht hat als das häufige.

from langchain_community.retrievers import BM25Retriever
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load and split documents
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Create BM25 retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# Search
results = bm25_retriever.invoke("FAISS vector index")
for doc in results:
    print(doc.page_content[:200])

Es zeichnet sich durch spezifische Produktbezeichnungen, Personen oder technische Begriffe aus, durch kurze, präzise Abfragen sowie dort, wo die genaue Formulierung wichtiger ist als die Bedeutung. Es hat Schwierigkeiten, wenn der Benutzer Dinge anders formuliert als im Dokument (Synonyme, Paraphrasen), bei langen, konversationalen Abfragen sowie dann, wenn die Bedeutung wichtiger ist als die wörtlichen Worte.

Hybride Suche

Bei der hybriden Suche werden semantische Suchverfahren und BM25 parallel eingesetzt. Die semantische Suche kümmert sich um Bedeutung und Synonyme, während BM25 sich auf genaue Schlüsselwörter konzentriert; gemeinsam decken sie Fälle ab, die jeweils für sich allein übersehen würden. Die beiden Ergebnislisten werden mithilfe von Reciprocal Rank Fusion (RRF) zusammengeführt – einer Technik, die gerankte Listen ohne Notwendigkeit, dass ihre Bewertungen denselben Maßstab teilen, zu einer Liste verschmilzt.

from langchain_community.retrievers import BM25Retriever
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.retrievers import EnsembleRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
#Load and split
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
#Retriever 1 — Semantic (FAISS)
embeddings = GoogleGenerativeAIEmbeddings(
    model="models/embedding-001",
    google_api_key="YOUR_KEY"
)
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 5})
#Retriever 2 — Keyword (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
#Combine both — Hybrid Search
hybrid_retriever = EnsembleRetriever(
    retrievers=[semantic_retriever, bm25_retriever],
    weights=[0.6, 0.4]  # 60% semantic, 40% keyword
)
#Search
results = hybrid_retriever.invoke("What is FAISS and how does it store vectors?")
print(f"Retrieved {len(results)} chunks")
for i, doc in enumerate(results):
    print(f"\nChunk {i+1}: {doc.page_content[:150]}")

Der Parameter weights legt fest, wie stark jeder Suchmechanismus beiträgt. Ein leichter Schwerpunkt auf der semantischen Suche eignet sich in den meisten Fällen, wobei das richtige Gleichgewicht von Ihren Daten abhängt. Wählen Sie die hybride Methode, wenn die Dokumente technische Begriffe, Produktnamen oder Eigennamen enthalten, wenn die Nutzer gesprochene Fragen mit Suchbegriffen mischen oder wenn Sie eine Suchbegriffserkennung als Sicherheitsnetz hinter dem semantischen Verständnis möchten.

Neubewertung

Die Neubewertung erfolgt in zwei Schritten. Zunächst werden im Groben die besten 20 bis 30 Ergebnisse mit dem Suchmechanismus abgerufen, was kostengünstig und schnell ist. Anschließend werden diese Kandidaten durch ein spezielles Neubewertungsmodell geleitet, das jedes Ergebnis danach bewertet, wie gut es die Frage beantwortet, und nur die besten 3 bis 5 Ergebnisse behält.

Der Grund dafür ist, dass die Vektorähnlichkeit eine schnelle Annäherung an die Relevanz darstellt, während ein Neuranking zwar langsamer, aber deutlich genauer ist. Zunächst durch Ähnlichkeit einzuschränken und anschließend neu zu ranken, bietet sowohl Geschwindigkeit als auch Präzision.

from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain.retrievers import ContextualCompressionRetriever
#Base retriever — fetch top 20 candidates
base_retriever = vector_store.as_retriever(search_kwargs={"k": 20})
#Reranker model
reranker_model = HuggingFaceCrossEncoder(
    model_name="BAAI/bge-reranker-base"
)
#Reranker compressor — keeps only top 3 after reranking
reranker = CrossEncoderReranker(model=reranker_model, top_n=3)
#Combine base retriever + reranker
reranking_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=base_retriever
)
#Search — fetches 20, reranks, returns top 3
results = reranking_retriever.invoke("How does FAISS perform similarity search?")
print(f"Final chunks after reranking: {len(results)}")
for i, doc in enumerate(results):
    print(f"\nTop {i+1}: {doc.page_content[:200]}")

Ein Cross-Encoder-Reranker liest die Frage zusammen mit einem bestimmten Textabschnitt als Paar und prüft, wie relevant dieser spezifische Abschnitt für diese spezifische Frage ist. Die Vektorähnlichkeit betrachtet jeden Text unabhängig voneinander und berücksichtigt niemals direkt deren Beziehung zueinander. Wenden Sie Reranking an, wenn Präzision wichtig ist und falsche Antworten kostspielig sind, wenn Sie Fragen zu bestimmten Dokumenten beantworten oder wenn die Qualität der Antworten die Nutzer direkt beeinflusst.

Maximum Marginal Relevance

MMR bekämpft Redundanzen. Anstatt einfach die am nächsten liegenden Abschnitte zu verwenden, wählt es solche aus, die sowohl relevant als auch abwechslungsreich sind, damit dieselben Informationen nicht mehrfach zurückgegeben werden.

# Standard similarity search — might return redundant chunks
standard_results = vector_store.similarity_search(query, k=3)
# MMR search — returns relevant AND diverse chunks
mmr_results = vector_store.max_marginal_relevance_search(
    query,
    k=3,           # final number of chunks to return
    fetch_k=20,    # candidates to consider before selecting diverse ones
    lambda_mult=0.5  # 0 = max diversity, 1 = max relevance
)

Der Parameter lambda_mult legt das Gleichgewicht fest. Ein Wert von 1,0 bedeutet reine Relevanz, ähnlich wie bei einer herkömmlichen Ähnlichkeitssuche. Ein Wert von 0,0 bedeutet reine Vielfalt, wobei die Blöcke so unterschiedlich wie möglich sein sollen. Ein Wert von 0,5 balanciert beides und ist in der Regel eine gute Ausgangseinstellung. MMR ist vorteilhaft, wenn die Dokumente repetitiv sind, wenn mehrere Aspekte eines Themas abgedeckt werden sollen oder wenn das Kontextfenster zu eng ist, um duplizierte Blöcke aufzunehmen.

Ein vollständiger Pipeline

Zusammengefasst wirft die hybride Suche ein breites Netz aus, die Neurangierung wählt die relevantesten Blöcke aus und eine vollständige RAG-Kette erzeugt die Antwort.

from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
# Setup
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key="YOUR_KEY")
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001", google_api_key="YOUR_KEY")
# Load and split documents
loader = TextLoader("your_knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Build retrievers
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 10})
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# Hybrid retriever
hybrid_retriever = EnsembleRetriever(
    retrievers=[semantic_retriever, bm25_retriever],
    weights=[0.6, 0.4]
)
# Add reranking on top
reranker = CrossEncoderReranker(
    model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base"),
    top_n=3
)
final_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=hybrid_retriever
)
# RAG prompt
prompt = ChatPromptTemplate.from_messages([
    ("system", """Answer the question using only the context below.
    If the answer is not in the context, say "I don't have that information."

    Context: {context}"""),
    ("human", "{question}")
])
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)
# Complete chain
rag_chain = (
    {"context": final_retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
# Use it
answer = rag_chain.invoke("Your question here")
print(answer)

Die Qualität der Informationsabrufung ist deutlich besser als bei einer reinen ähnlichkeitsbasierten Suche.

Strategieauswahl

Ein kurzer Entscheidungsführer ordnet Situationen Strategien zu:

Just starting out / simple use case?
    → Basic similarity search is fine
Documents have specific technical terms or product names?
    → Add BM25 → use Hybrid Search
Answer quality is critical, wrong answers are costly?
    → Add Reranking on top of any retriever
Documents have lots of repeated content?
    → Use MMR instead of standard similarity search
Production system with high quality requirements?
    → Hybrid Search + Reranking together

Überengineeringen Sie nicht von Tag eins an. Beginnen Sie mit einer einfachen Ähnlichkeitssuche, fügen Sie eine hybride Suche hinzu, sobald Schlüsselwörter nicht gefunden werden, und fügen Sie eine Neubewertung der Ergebnisse hinzu, wenn die Qualität der Antworten verbessert werden muss. Jeder Zusatz bedeutet höhere Komplexität, längere Latenzzeiten und mehr Rechenleistung. Für produktive RAG-Lösungen ist eine Kombination aus hybrider Suche und Neubewertung der Standard – Komplexität sollte nur dann entstehen, wenn ein spezifisches Problem dies erfordert.

Eine praktische Anmerkung zur Neubewertung der Ergebnisse: Dafür ist pip install sentence-transformers erforderlich, und das Modell BAAI/bge-reranker-base wird beim ersten Gebrauch automatisch heruntergeladen – es umfasst etwa 1 GB. Für produktive Anwendungen vermeiden gehostete Neubewertungs-APIs von Cohere oder Jina AI die Aufwand, das Modell lokal auszuführen.

Verwandte Literatur

  • Jenseits der Similarity Search: Metadatenfilterung für RAG mit S3 Vectors — Woher die Filterwerte stammen, wann sich ein Klassifikator lohnt, und wie man in Amazon S3 Vectors metadatenbasierte Filter für filterbare und nicht-filterbare Daten einrichtet.