# Hybrid Search – Keyword- und Vektorsuche mit RRF verbinden

> **Autor:** Lukas Kopischke (Lead Software-Engineer, Düsseldorf)
> **Kanonische URL:** https://lukiluki.me/garden/hybrid-search
> **Markdown-Endpunkt:** https://lukiluki.me/garden/hybrid-search.md
> **Kategorie:** KI & Agents | **Tags:** BM25, Embeddings, RRF, Qdrant
> **Veröffentlicht:** 2026-10-29 | **Aktualisiert:** 2026-11-02
> **Zusammenfassung:** Hybrid Search kombiniert BM25-Keyword-Suche und Embeddings über Reciprocal Rank Fusion (RRF), um exakte Kennungen und Umschreibungen präzise zu finden.

---
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 BM25 (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-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](/datasets/retrieval-de.json) 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.

Reciprocal Rank Fusion (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.

> **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](https://github.com/lnk1812/lukiluki/blob/main/src/lib/retrieval-evaluation.ts) 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
export function reciprocalRankFusion(
  rankings: readonly (readonly string[])[],
  rankConstant = 60,
): { id: string; score: number }[] {
  if (!Number.isFinite(rankConstant) || rankConstant < 0) {
    throw new RangeError("rankConstant must be finite and non-negative");
  }
  const scores = new Map<string, number>();
  for (const ranking of rankings) {
    const seen = new Set<string>();
    ranking.forEach((id, index) => {
      if (typeof id !== "string" || !id.trim()) {
        throw new TypeError("Invalid document ID");
      }
      if (seen.has(id)) throw new TypeError("Duplicate document ID within a ranking");
      seen.add(id);
      scores.set(id, (scores.get(id) ?? 0) + 1 / (rankConstant + index + 1));
    });
  }
  return [...scores].map(([id, score]) => ({ id, score }))
    .sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
}
```

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) |

  #### Empfohlen (Do): 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).
  #### Vermeiden (Don't): 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

- [Qdrant Documentation: Hybrid Queries](https://qdrant.tech/documentation/search/hybrid-queries/) – Umsetzung mehrstufiger Queries und RRF-Fusion in einer Vektordatenbank.
- [Elasticsearch Guide: Reciprocal Rank Fusion (RRF)](https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html) – Funktionsweise, Parameter `rank_constant` und Grenzen der Rangfusion.

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

---
*Quelle: [Lukas Kopischke – Digital Garden](https://lukiluki.me/garden/hybrid-search) | Vollständiger Index: [llms.txt](https://lukiluki.me/llms.txt)*
