KI & Agents
BM25EmbeddingsRRFQdrant

Hybrid Search – Keyword- und Vektorsuche mit RRF verbinden

Hybrid Search kombiniert BM25-Keyword-Suche und Embeddings über Reciprocal Rank Fusion (RRF), um exakte Kennungen und Umschreibungen präzise zu finden.

LKLukas Kopischke
Zuletzt aktualisiert am
Erstellt am

Ein Entwickler kopiert den Fehlercode ERR-1042 ins Suchfeld und erwartet exakt den Artikel zu diesem Token-Ablauf. Eine Minute später sucht eine Kollegin nach „Meine Sitzung ist abgelaufen“ – sie meint dasselbe Problem, kennt den Code aber nicht. Wie baust du eine Suche, die bei exakten Kennungen keinen Millimeter abweicht und trotzdem freie Umschreibungen versteht?

Hybrid Search führt eine klassische Keyword-Suche (meist BM25) und eine Vektorsuche parallel aus und führt beide Trefferlisten zu einem gemeinsamen Ranking zusammen. Während die lexikalische Suche exakte Begriffe, Produktnummern und Fehlercodes absichert, fängt das Embedding-Modell Synonyme und Umschreibungen auf. Über Reciprocal Rank Fusion (RRF) lassen sich beide Ergebnislisten allein anhand ihrer Rangpositionen verrechnen – ohne dass du unvergleichbare Roh-Scores aufwendig kalibrieren musst.

Merksatz

BM25 trifft das exakte Wort, der Vektor versteht die Umschreibung – erst wenn beide Suchpfade dieselben Zugriffs- und Versionsfilter teilen, entsteht aus ihrer Fusion eine verlässliche Trefferliste.

Warum ergänzen sich BM25 und Embeddings so gut?

Beide Suchverfahren haben genau dort ihren blinden Fleck, wo das andere stark ist:

  • Lexikalische Suche mit : Wenn ERR-1042 nur in einem einzigen Hilfeartikel vorkommt, erhält dieser Begriff ein enormes Gewicht. Voraussetzung ist allerdings, dass dein Tokenizer Bindestriche und Ziffernfolgen nicht unglücklich zerschneidet.
  • Dichte Vektorsuche (Embeddings): Sie erkennt problemlos, dass „Anmeldung abgelaufen“ und „Sitzung abgelaufen“ zusammengehören. Dafür sehen sich ERR-1042 und ERR-1043 für ein Embedding-Modell oft zum Verwechseln ähnlich, weil der umgebende Satzbau fast identisch ist.

Im deutschen Support-Datensatz sind beide Fehlernummern als Nachbarn angelegt. Wer hier beide Suchpfade kombiniert, muss vor allem auf Filter-Symmetrie achten: Wenn dein Vektorpfad sauber auf version = 2 und visibility = "public" filtert, die Volltextsuche diesen Filter aber vergisst, spült die Fusion plötzlich veraltete oder interne Dokumente nach oben.

Warum RRF statt einfacher Score-Addition?

Auf den ersten Blick liegt es nahe, den BM25-Score und die Kosinus-Ähnlichkeit einfach zu addieren. In der Praxis scheitert das sofort an der Mathematik: Ein Kosinus-Score liegt begrenzt zwischen -1 und +1, während ein BM25-Wert je nach Dokumentlänge und Begriffsseltenheit nach oben offen ist (z. B. 0 bis 25+). Eine unkalibrierte Summe würde fast vollständig vom BM25-Ausschlag dominiert.

umgeht dieses Skalenproblem komplett, indem es nur auf die Rangposition (1, 2, 3, …) schaut. Jedes gefundene Dokument erhält pro Liste den Wert 1 / (k + Rang). Taucht die richtige Anleitung sowohl in der Keyword-Suche auf Platz 2 als auch in der Vektorsuche auf Platz 3 auf, addieren sich beide Beiträge und schieben das Dokument an die Spitze.

Was bewirkt die Glättungskonstante k (meist 60)?

Die Konstante k (im ursprünglichen RRF-Paper von Cormack et al. oft auf 60 gesetzt) dämpft den Abstand zwischen Platz 1 und Platz 2. Ohne k wäre Platz 1 (1/1 = 1.0) doppelt so viel wert wie Platz 2 (1/2 = 0.5) – ein einzelner Ausreißer auf Platz 1 würde dann Dokumente schlagen, die in beiden Listen stabil auf Platz 2 stehen.

Referenzimplementierung: Getestetes RRF in TypeScript

Die folgende Funktion stammt direkt aus unserer getesteten Referenzimplementierung im Repository und kommt ohne externe Abhängigkeiten aus. Sie akzeptiert beliebig viele geordnete ID-Listen, verhindert doppelte Einträge innerhalb derselben Liste und löst Gleichstände deterministisch über die Dokument-ID auf:

TYPESCRIPT
1export function reciprocalRankFusion(
2 rankings: readonly (readonly string[])[],
3 rankConstant = 60,
4): { id: string; score: number }[] {
5 if (!Number.isFinite(rankConstant) || rankConstant < 0) {
6 throw new RangeError("rankConstant must be finite and non-negative");
7 }
8 const scores = new Map<string, number>();
9 for (const ranking of rankings) {
10 const seen = new Set<string>();
11 ranking.forEach((id, index) => {
12 if (typeof id !== "string" || !id.trim()) {
13 throw new TypeError("Invalid document ID");
14 }
15 if (seen.has(id)) throw new TypeError("Duplicate document ID within a ranking");
16 seen.add(id);
17 scores.set(id, (scores.get(id) ?? 0) + 1 / (rankConstant + index + 1));
18 });
19 }
20 return [...scores].map(([id, score]) => ({ id, score }))
21 .sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
22}

Wichtig für den Aufruf: Übergib pro Liste eindeutige Dokument-IDs (oder dedupliziere Chunks vorab), damit ein langes Dokument mit vielen Abschnitten nicht künstlich mehrfach gezählt wird. Und denke daran: RRF kann einen exakten Keyword-Treffer auf Platz 1 nach hinten drücken, wenn der Vektorpfad ein anderes Dokument hoch bewertet. Wenn exakte Fehlercodes in deinem Produkt Vorrang haben müssen, kannst du entweder die Listen unterschiedlich gewichten oder erkannte Muster (ERR-\d+) gezielt boosten.

Wie beweist du, dass Hybrid Search deinem System wirklich hilft?

Zwei parallele Suchpfade bedeuten auch mehr Datenbankarbeit und höhere Latenz, weil die Fusion immer auf den langsameren der beiden Pfade warten muss. Rolle Hybrid Search deshalb nur aus, wenn sie im direkten Vergleich gegen die Einzelverfahren gewinnt:

VergleichslaufWelche Stärke prüfst du damit?Was für einen fairen Test gleich bleiben muss
1. Nur Keyword (BM25)Treffsicherheit bei Fehlercodes, Eigennamen und FachbegriffenIdentischer Dokumentbestand, gleiche Zugriffs- und Versionsfilter
2. Nur VektorsucheUmgang mit Alltagssprache, Synonymen und UmschreibungenIdentisches Cutoff-Limit (k) und dieselben Testfragen
3. Hybrid (RRF-Fusion)Gewinn über das gesamte Fragenspektrum vs. zusätzliche LatenzDieselben Sollquellen (Ground Truth) und dieselbe Metrik (z. B. nDCG@5)
Symmetrische Filter & kontrollierte Kandidatenfenster
  • Wende Mandanten-, Rollen- und Versionsfilter identisch in BM25 und Vektorsuche an.
  • Begrenze beide Vorab-Listen sinnvoll (z. B. jeweils Top-50), bevor RRF sie zusammenführt.
  • Definiere ein klares Degradationsverhalten (z. B. Fallback auf BM25, falls der Embedding-Service ein Timeout wirft).
Roh-Scores unkalibriert addieren
  • Unnormalisierte BM25-Werte und Kosinus-Ähnlichkeiten direkt zusammenzählen.
  • Mehrere Chunks desselben Dokuments ungefiltert in einer Ranking-Liste an RRF übergeben.
  • Einen Filter nur im Vektorindex setzen und alte Dokumente über die Volltextsuche wieder einschleusen.

Checkliste für deine Hybrid-Pipeline

  • Beide Suchpfade erzwingen exakt dieselben Berechtigungs- und Versionsgrenzen.
  • Exakte Kennungen wie ERR-1042 werden im Tokenizer erhalten und nicht mit ERR-1043 verwechselt.
  • Die RRF-Fusion schlägt im Evaluation-Lauf sowohl die reine Keyword- als auch die reine Vektorsuche über deinen gesamten Fragensatz.
  • Kandidatenlimits pro Pfad, rankConstant und das Timeout-Verhalten sind dokumentiert und getestet.

Quellen und nächste Schritte

Die Grundlagen des Vektorpfads findest du unter Semantische Suche. Wenn du die besten 20 bis 50 fusionierten Kandidaten anschließend noch einmal inhaltlich scharf sortieren willst, ist Reranking mit Cross-Encodern der logische nächste Baustein.