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.
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 (Best Matching 25: bewährtes Ranking-Verfahren der Volltextsuche, das Treffer nach Wortfrequenz (TF), Seltenheit des Begriffs im Gesamtbestand (IDF) und Dokumentlänge gewichtet.): Wenn
ERR-1042nur 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-1042undERR-1043fü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.
(RRF berechnet den neuen Gesamtscore eines Dokuments ausschließlich aus seinen Listenplätzen in den Einzelsuchen nach der Formel 1 / (k + Rang) und summiert diese Werte auf.) 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.
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:
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:
| Vergleichslauf | Welche Stärke prüfst du damit? | Was für einen fairen Test gleich bleiben muss |
|---|---|---|
| 1. Nur Keyword (BM25) | Treffsicherheit bei Fehlercodes, Eigennamen und Fachbegriffen | Identischer Dokumentbestand, gleiche Zugriffs- und Versionsfilter |
| 2. Nur Vektorsuche | Umgang mit Alltagssprache, Synonymen und Umschreibungen | Identisches Cutoff-Limit (k) und dieselben Testfragen |
| 3. Hybrid (RRF-Fusion) | Gewinn über das gesamte Fragenspektrum vs. zusätzliche Latenz | Dieselben Sollquellen (Ground Truth) und dieselbe Metrik (z. B. nDCG@5) |
- 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).
- 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-1042werden im Tokenizer erhalten und nicht mitERR-1043verwechselt. - Die RRF-Fusion schlägt im Evaluation-Lauf sowohl die reine Keyword- als auch die reine Vektorsuche über deinen gesamten Fragensatz.
- Kandidatenlimits pro Pfad,
rankConstantund das Timeout-Verhalten sind dokumentiert und getestet.
Quellen und nächste Schritte
- Qdrant Documentation: Hybrid Queries – Umsetzung mehrstufiger Queries und RRF-Fusion in einer Vektordatenbank.
- Elasticsearch Guide: Reciprocal Rank Fusion (RRF) – Funktionsweise, Parameter
rank_constantund Grenzen der Rangfusion.
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.