# Retrieval-Evaluation – Suchqualität mit Recall, MRR und nDCG prüfen

> **Autor:** Lukas Kopischke (Lead Software-Engineer, Düsseldorf)
> **Kanonische URL:** https://lukiluki.me/garden/retrieval-evaluation
> **Markdown-Endpunkt:** https://lukiluki.me/garden/retrieval-evaluation.md
> **Kategorie:** KI & Agents | **Tags:** Recall@k, MRR, nDCG, Node.js
> **Veröffentlicht:** 2026-10-29 | **Aktualisiert:** 2026-11-02
> **Zusammenfassung:** Retrieval-Evaluation misst Suchqualität reproduzierbar mit Recall@k, MRR und nDCG auf festen Testfragen und prüft Filter sowie unbeantwortbare Fragen.

---
Nach dem Wechsel auf ein neues Embedding-Modell wirken deine ersten drei Stichproben im Browser deutlich besser. Doch erst im Betrieb fällt auf: Bei Fragen zu Fehlercodes fehlt plötzlich die richtige Anleitung komplett, und bei einem anderen Thema rutscht sie von Platz 1 auf Platz 6 ab. Wie stellst du sicher, dass jede Änderung an Modell, Chunking oder Index deine Suche nachweisbar verbessert statt nur Fehler zu verschieben?

Bei einer **Retrieval-Evaluation** prüfst du deine Suche automatisiert gegen einen festen Satz aus repräsentativen Suchfragen und vorab definierten Sollquellen (**Ground Truth**). Dabei misst **Recall@k**, ob die richtigen Dokumente überhaupt unter den ersten `k` Treffern auftauchen, während **MRR@k** und **nDCG@k** bewerten, wie weit oben sie stehen. Ergänzt um harte Prüfungen auf unzulässige Dokumente (z. B. interne Notizen oder alte Versionen) und Fragen ohne Antwort im Bestand wird aus Bauchgefühl ein reproduzierbarer Regressionstest.

> **Merksatz:**
> Miss zuerst über Recall@k, ob die richtige Quelle überhaupt im Netz landet – und optimiere erst danach mit MRR und nDCG ihre Platzierung an der Spitze.

## Was gehört in einen belastbaren Testbestand (Ground Truth)?

Ein guter Evaluierungs-Datensatz entsteht **vor** dem Modellvergleich und unabhängig von deinem aktuellen Suchsystem. Wenn du einfach die heutigen Top-Treffer deines bestehenden Modells als „richtig“ abspeicherst, belohnst du bei jedem späteren Test nur das alte Verhalten.

Unser [deutscher Referenz-Datensatz (`retrieval-de.json`)](/datasets/retrieval-de.json) (CC0) zeigt den minimalen Aufbau für einen solchen Durchstich- und Regressionstest:
- **12 Dokumente mit klaren Metadaten:** Darunter 10 öffentliche Artikel der aktuellen `version: 2`, aber auch bewusst eine veraltete Anleitung (`account-reset-old`, `v1`) und eine interne Admin-Vorschrift (`admin-reset`, `visibility: "internal"`).
- **8 beantwortbare Testfragen mit nahen Gegenbeispielen:** Etwa Umschreibungen („Mein Handy mit dem Authenticator ist weg“ → `account-mfa` statt `account-reset`), Adress-Paare (Rechnungs- vs. Lieferadresse) und exakte Fehlercodes (`ERR-1042` vs. `ERR-1043`).
- **1 unbeantwortbare Kontrollfrage (`q-missing`):** Eine Frage außerhalb des Dokumentbestands (`relevantIds: []`), um zu prüfen, ob dein System irrelevante Treffer zurückhalten kann.

Für Entscheidungen in Produktion erweiterst du dieses Grundgerüst um echte, anonymisierte Nutzerfragen aus deiner Domäne und hältst einen Teil der Fragen als unberührtes Test-Set zurück, damit du deine Parameter nicht unbemerkt auf wenige Beispiele überanpasst.

## Recall@k, MRR@k und nDCG@k: Welche Zahl zeigt welches Problem?

Jede der drei Standardmetriken beantwortet eine andere Diagnosefrage auf deinen ersten `k` Treffern:

| Metrik | Welche konkrete Frage beantwortet sie? | Wo liegt ihr blinder Fleck? |
| :--- | :--- | :--- |
| **Recall@k (Anteil der für eine Frage relevanten Dokumente, die innerhalb der ersten k Suchtreffer gefunden wurden (Gefundene relevante Dokumente in Top-k geteilt durch alle relevanten Dokumente dieser Frage).)** | Wurden die benötigten Quellen überhaupt in den Top-`k` gefunden? | Unterscheidet nicht zwischen Platz 1 und Platz `k` – solange der Treffer im Fenster liegt, zählt er voll |
| **MRR@k** (Mean Reciprocal Rank) | Auf welchem Platz taucht der **allererste** relevante Treffer auf (`1 / Rang`)? | Ignoriert jeden weiteren relevanten Treffer komplett; ideal, wenn genau *ein* Zielartikel gesucht wird |
| **nDCG@k** (Normalized Discounted Cumulative Gain) | Wie nah liegt die gesamte Trefferliste an der idealen Sortierung? | Hängt von der gewählten Abstufung der Relevanzlabels und der logarithmischen Abwertung (`1 / log2(Rang + 1)`) ab |

In unserer Referenzimplementierung arbeiten wir mit binärer Relevanz und dem Standard-Positionsrabatt `1 / log2(Rang + 1)` (ab Rang 1), normalisiert gegen die bestmögliche Anordnung derselben Frage. Wenn es für eine Frage genau eine richtige Quelle gibt, sinkt bei Platz 2 der Reciprocal Rank auf `0.5` und der nDCG-Wert auf `1 / log2(3) ≈ 0.631`. Die [Evaluations-Dokumentation von Sentence Transformers](https://sbert.net/docs/package_reference/sentence_transformer/evaluation.html) nutzt dieselbe Aufteilung.

## Auswertung direkt im Repository ausführen

Damit du deine eigenen Suchläufe ohne externe Pakete prüfen kannst, bringt dieses Repository das CLI-Skript [`scripts/evaluate-retrieval.mjs`](https://github.com/lnk1812/lukiluki/blob/main/scripts/evaluate-retrieval.mjs) und die Rechenlogik in [`src/lib/retrieval-evaluation.ts`](https://github.com/lnk1812/lukiluki/blob/main/src/lib/retrieval-evaluation.ts) für Node.js 24 direkt mit. Als Startpunkt dient die [leere Run-Vorlage (`retrieval-run-template.json`)](/datasets/retrieval-run-template.json):

1. **Trefferlisten eintragen:** Trage für jede Frage-ID die von deinem System zurückgegebenen **Dokument-IDs** in ihrer echten Rangfolge ein (führe mehrere Chunks desselben Dokuments vorab unter Erhalt der besten Position zusammen).
2. **Evaluation und Unit-Tests starten:**

```bash
node scripts/evaluate-retrieval.mjs ./rankings.json 5
node --test tests/retrieval-evaluation.test.mjs
```

Das Skript berechnet für alle acht beantwortbaren Fragen `Recall@5`, `MRR@5` und `nDCG@5` (einzeln und als Makro-Durchschnitt) – und erzwingt gleichzeitig **harte Sicherheits-Invaranten**: Sobald in deiner Ergebnisliste das interne Dokument `admin-reset`, die veraltete Anleitung `account-reset-old`, eine doppelte Dokument-ID oder eine fehlende Frage auftaucht, bricht der Lauf sofort mit einem expliziten Fehler ab, statt das Datenleck in einem harmlosen Durchschnittswert zu verstecken.

> **Warum Fragen ohne Antwort (q-missing) getrennt gezählt werden**
> Für eine Frage ohne relevante Quelle (`relevantIds: []`) wäre der Nenner beim Recall gleich null. Deshalb schließt das Skript `q-missing` sauber aus dem Recall-/MRR-/nDCG-Mittelwert aus und weist sie separat über das Feld `abstained: true | false` aus (abhängig davon, ob deine Suche eine leere Liste `[]` zurückgegeben hat).

## Wann ist eine Änderung wirklich ein Gewinn?

Verlasse dich niemals allein auf den Gesamt-Durchschnitt über alle Fragen. Wenn ein neues Modell fünf einfache Umschreibungen von Platz 2 auf Platz 1 hebt, dafür aber den kritischen Fehlercode `ERR-1042` nicht mehr findet, sieht der Mittelwert oft besser aus – während deine Nutzer im Support scheitern.

  #### Empfohlen (Do): Fehlerklassen trennen & Scope hart prüfen
* Werte Umschreibungen, exakte Fehlercodes, nahe Nachbarn und unbeantwortbare Fragen getrennt aus.
    * Lass den Evaluationslauf fehlschlagen, sobald ein veraltetes oder internes Dokument in den Top-k auftaucht.
    * Protokolliere zu jedem Lauf Modellversion, Chunk-Größe, Filter, Kandidatenlimit und p95-Latenz.
  #### Vermeiden (Don't): Chunk-Dubletten & ungeprüfte Mittelwerte feiern
* Drei überlappende Chunks desselben Dokuments als drei separate Treffer werten.
    * Fragen ohne Antwort mit `Recall = 0` oder `1` in den normalen Durchschnitt mischen.
    * Die aktuellen Treffer deines Produktivsystems ungeprüft als Ground Truth für den nächsten Test übernehmen.

## Checkliste für deinen Evaluations-Lauf

- [ ] Suchfragen und Sollquellen (Ground Truth) stehen fest, bevor Parameter oder Modelle verglichen werden.
- [ ] Alle verglichenen Läufe nutzen exakt denselben Cutoff (`k`), dieselbe Deduplizierung auf Dokumentebene und dieselben Filter.
- [ ] Unzulässige Dokumente (intern / falsche Version) führen zum sofortigen Testabbruch statt nur zu Punktabzug.
- [ ] Fragen ohne Antwort im Bestand werden separat auf Zurückhaltung (`abstained`) geprüft.

## Quellen und nächste Schritte

- [Sentence Transformers: Information Retrieval Evaluator](https://sbert.net/docs/package_reference/sentence_transformer/evaluation.html) – Referenzimplementierung für Recall@k, MRR und nDCG im Python-Ökosystem.
- [MTEB: Massive Text Embedding Benchmark (GitHub)](https://github.com/embeddings-benchmark/mteb) – standardisierte Auswertungs-Pipelines für Retrieval-Aufgaben.

Mit diesem Messaufbau kannst du nun objektiv prüfen, welches [Embedding-Modell](/garden/embedding-models) auf deinen deutschen Texten vorn liegt und ob ein nachgeschaltetes [Reranking](/garden/reranking) die ersten Plätze deiner Trefferliste tatsächlich verbessert.

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