Hybride Suche für RAG auf Persisch: ein Praxisleitfaden

Hybride Suche für RAG auf Persisch (Farsi) mit PostgreSQL und pgvector: sichere Normalisierung, RRF über dichte und lexikalische Ränge, kein Sprachfilter.

Hybride Suche für RAG auf Persisch (Farsi) bedeutet, zwei Retriever nebeneinander zu betreiben, eine dichte Embedding-Suche und eine lexikalische Suche, und ihre Rankings mit Reciprocal Rank Fusion (RRF) zu fusionieren. Sie brauchen beides, weil Persisch die stillschweigenden Annahmen englischer Rezepte bricht: Dasselbe Wort kann mit arabischen oder mit persischen Buchstaben gespeichert sein, mitten im Wort steht ein Zero-Width Non-Joiner, und weder Snowball noch PostgreSQL liefert einen persischen Stemmer mit. Kurz gefasst: beim Speichern normalisieren, die lexikalische Seite ohne Stemmer betreiben, Ränge statt Scores fusionieren, nie nach Sprache filtern, entlang der Struktur des Dokuments chunken und alles an einem zweisprachigen Fragenkatalog testen, bevor Sie sich auf irgendetwas davon verlassen.

Dieser Leitfaden stützt sich auf zwei Systeme. Das eine ist der Assistent dieser Website, den ich gebaut habe und betreue. Das andere ist Kartez Contract, wo ich als KI-Ingenieur gearbeitet habe; dessen Copilot musste in persischen Verträgen, nummeriert nach Artikeln, Klauseln und Anmerkungen, die richtigen Klauseln finden. Das hier beschriebene Retrieval-Design (dicht plus lexikalisch, Rangfusion, pgvector) ist das dieser Website; von Kartez übernehme ich nur, was in der Fallstudie steht. Keines der beiden Systeme wird hier einem Benchmark unterzogen.

Hybrides Retrieval für Persisch: Chunks werden beim Speichern normalisiert und eingebettet; dichte und lexikalische Liste werden mit RRF fusioniert, wobei die Sprache nur bei exaktem Gleichstand entscheidet; vor dem Aufruf des Sprachmodells wird eine Ähnlichkeitsschwelle geprüft: Darunter erhält eine Frage über mich die Datensätze der Website, und jede andere Frage wird abgelehnt.

Warum englische RAG-Rezepte an persischem Text scheitern

Der typische Fehler ist keine falsche Antwort, sondern gar keine: Die Suche findet ein Wort nicht, das der Nutzer im Dokument sehen kann, weil sich zwei Zeichenketten, die identisch aussehen, unter der Oberfläche unterscheiden.

Ein Wort, zwei Zeichenketten: arabisches und persisches Yeh und Kaf

Persisch und Arabisch teilen sich eine Schrift, aber nicht jeden Codepunkt. Das persische ی (U+06CC ARABIC LETTER FARSI YEH) und das arabische Yeh (U+064A ARABIC LETTER YEH) sind zwei verschiedene Zeichen, ebenso das persische ک (U+06A9 ARABIC LETTER KEHEH) und das arabische Kaf (U+0643 ARABIC LETTER KAF). Die Arabic and Persian Layout Requirements des W3C benennen das Yeh-Paar ausdrücklich: Der Buchstabe wird „mit zwei verschiedenen Zeichen dargestellt“. Eine Passage, die mit der einen Form gespeichert ist, passt nicht zu einer Frage, die mit der anderen eingegeben wurde; der Normalisierungsschritt im Diagramm oben zeigt beide Paare.

Diese Website vereinheitlicht unter anderem arabisches Yeh, Alef Maksura und Kaf auf ihre persischen Formen, bevor eine Passage in den Retrieval-Index gelangt, entfernt unsichtbare Bidi-Markierungen aus dieser Kopie und wendet dieselbe Vereinheitlichung auf jede Frage an.

Der Zero-Width Non-Joiner gehört zum Wort

می‌شود, میشود und می شود sind drei verschiedene Zeichenketten. Die erste trennt das Präfix vom Verb mit einem Zero-Width Non-Joiner (ZWNJ, U+200C), der die Verbindung der Buchstaben unterbricht, ohne ein Leerzeichen einzufügen. Der Unicode-Standard beschreibt dieses abweichende Verbindungsverhalten als etwas, das „beim Schreiben von Persisch in arabischer Schrift manchmal erforderlich ist“, und die W3C-Anforderungen sagen, Steuerzeichen „sollten beim Speichern und Anzeigen von Texten erhalten bleiben“.

Entfernt man den ZWNJ, wie es eine englische Pipeline tun könnte, wird می‌شود zu میشود; macht man ein Leerzeichen daraus, werden aus einem Wort zwei Tokens. Der Normalisierer von hazm, einem NLP-Toolkit für Persisch, enthält eine „ZWNJ-Korrektur“, die کتاب های in کتاب‌های umwandelt. Auf dieser Website behält der gespeicherte Text den ZWNJ; nur die Vereinheitlichung innerhalb von Vergleichen behandelt ihn als Leerzeichen.

Drei Ziffernsysteme

Persisch schreibt ۱۲۳ mit östlichen arabisch-indischen Ziffern (U+06F0 bis U+06F9), Arabisch schreibt dieselbe Zahl mit arabisch-indischen Ziffern (U+0660 bis U+0669), und technischer Text schreibt oft 123.

Vereinheitlichen Sie Ziffern nur für den Vergleich, nie im gespeicherten Text; auf dieser Website würde ein Umschreiben gespeicherter persischer Ziffern verändern, wie der persische Lebenslauf dargestellt wird. Die Vereinheitlichung muss allerdings auf beiden Seiten laufen: Vereinheitlicht man nur die Ziffern der Frage, passt eine Frage mit persischen Ziffern nicht mehr zu der Passage, aus der sie kopiert wurde. Der Artikel über LLM-Guardrails im Streaming wendet dieselbe Vereinheitlichung auf Zahlen an, die ein Modell ausgibt.

Diakritika sind selten, deshalb verhindert ein einzelnes verirrtes Zeichen den exakten Treffer

Die W3C-Anforderungen sagen es deutlich: „In Texten in arabischer Schrift ist es unüblich, Diakritika für Vokalinformationen und für die Längung von Konsonanten zu verwenden.“ Schon eine einzige Passage mit einer Kasra (einem Kurzvokalzeichen) oder eine Frage, die mit einem Taschdid (dem Verdopplungszeichen) eingefügt wurde, verfehlt deshalb jeden exakten Abgleich mit der Schreibung ohne diese Zeichen. In der Forschung zur persischen Sprachsynthese (Text-to-Speech), an der ich für MCI gearbeitet habe, war dieselbe Eigenschaft auf der Sprachseite das eigentlich Schwierige: Kurzvokale und die Ezāfe, die ein Nomen mit seinem Attribut verbindet, werden nicht geschrieben. Für das Retrieval heißt das: Diakritika innerhalb von Vergleichen entfernen und im gespeicherten Text behalten.

Nutzer fragen umgangssprachlich, Dokumente sind formell geschrieben

„Persisch gehört zu den Sprachen mit vielen Unterschieden zwischen formellem und informellem Schreibstil“, schreiben Tajalli, Shamsfard und Kalantari, die ein Parallelkorpus mit 50.000 Satzpaaren vorstellen. Ein Nutzer tippt می‌تونم, wo formeller Text می‌توانم schreibt. Meine Folgerung daraus: Ein lexikalischer Abgleich findet wenig Überschneidung zwischen einer umgangssprachlichen Frage und einer formellen Klausel, also ist es der dichte Retriever, der eine Chance hat, und ein Testset aus sauber formulierten Fragen verdeckt diese Lücke.

Sichere Normalisierung für Persisch

  • Arabisches Yeh und Kaf beim Speichern auf persisches ی und ک vereinheitlichen, nur in persischen Inhalten.

  • Den ZWNJ im gespeicherten Text behalten.

  • Bidi-Markierungen aus der Kopie entfernen, die Sie indexieren und vergleichen, nicht aus dem angezeigten Text.

  • Ziffern vereinheitlichen und Diakritika entfernen: nur innerhalb von Vergleichen, dann aber auf beiden Seiten.

  • Auf Fragen exakt dieselbe Funktion anwenden wie auf Passagen.

  • Die persische Vereinheitlichung nie auf arabischen Text anwenden; dort sind arabisches Yeh (U+064A) und Kaf (U+0643) die richtigen Buchstaben.

Der Retriever dieser Website erfüllt diese Liste noch nicht: Er vereinheitlicht persische Buchstabenformen in jeder Sprache, Arabisch eingeschlossen; seine lexikalische Liste kann eine Passage mit persischen Ziffern verfehlen; und Diakritika der arabischen Schrift werden aus den Termen der Frage und aus jedem Vergleich des Satz-Gates entfernt, nicht aber aus dem gespeicherten Text, den die Stichwortsuche durchsucht. Lesen Sie die Liste als Ziel.

Persische Volltextsuche in PostgreSQL: keine eingebaute Konfiguration

Volltextsuche ist die naheliegende lexikalische Hälfte einer hybriden Suche, und laut der PostgreSQL-Dokumentation wird PostgreSQL „mit vordefinierten Konfigurationen für viele Sprachen ausgeliefert“. Persisch gehört nicht dazu.

Snowball hat einen arabischen Stemmer, aber keinen persischen

Snowballs Liste der Stemming-Algorithmen enthält Arabisch, aber keinen Eintrag für Persisch oder Farsi. PostgreSQL baut seine Stemming-Wörterbücher aus Snowball: Die Sprachliste im Quellcode von PostgreSQL 18 enthält arabic, aber kein persian, und die Wörterbuchliste in der Dokumentation zeigt arabic_stem und simple, aber kein persisches Wörterbuch. Ein arabischer Stemmer teilt mit Persisch die Schrift, nicht die Grammatik. Außerhalb der Datenbank liefert hazm einen persischen Stemmer und Lemmatisierer mit; innerhalb gibt es keinen.

Was die Konfiguration simple leistet und was nicht

Die Wörterbuchvorlage simple arbeitet laut der PostgreSQL-Dokumentation zu Wörterbüchern, indem sie „das Eingabe-Token in Kleinbuchstaben umwandelt und es mit einer Datei von Stoppwörtern abgleicht“. Das eingebaute Wörterbuch simple hat keine Stoppwortdatei, daher bleibt jedes Token erhalten, und gestemmt wird auch nicht: قرارداد und قراردادها bleiben zwei Lexeme, ein Verlust beim Recall und ein Gewinn an Vorhersagbarkeit.

Bevor ein Wörterbuch läuft, entscheidet der Parser, was ein Wort ist, und was er als Buchstaben ansieht, „wird durch die Locale-Einstellung der Datenbank bestimmt, genauer durch lc_ctype“. Nehmen Sie also nicht an, wie Ihre Datenbank persischen Text zerlegt, schon gar nicht Wörter, die ein ZWNJ zusammenhält. Fragen Sie die Datenbank selbst, mit der Funktion ts_debug aus der Dokumentation zum Testen und Debuggen der Textsuche:

-- How does this database tokenise Persian? Check; do not assume.
SELECT alias, token, lexemes
FROM ts_debug('simple', 'قراردادها را می‌توان فسخ کرد');

Die Stichwortsuche dieser Website verwendet simple für persische Zeilen und english für englische. Die Demo „Semantische vs. Stichwortsuche“ lässt sie neben dem hybriden Retriever des Assistenten über die Inhalte dieser Website laufen.

Trigramm-Ähnlichkeit gegen Tippfehler

Die Extension pg_trgm vergleicht Folgen aus drei Zeichen, sodass ein falsch geschriebenes Wort noch Trigramme mit dem richtigen teilt. Sie „ignoriert Nicht-Wort-Zeichen (nicht alphanumerische Zeichen) beim Extrahieren von Trigrammen“, prüfen Sie das also, bevor Sie sich darauf verlassen: SELECT show_trgm('قرارداد'); sollte Trigramme aus persischen Buchstaben liefern. Die Stichwortsuche der Website ergänzt einen Zweig mit word_similarity, der eine kurze Anfrage mit dem am besten passenden zusammenhängenden Abschnitt einer langen Passage abgleicht.

Ganzwort-Abdeckung: die lexikalische Liste im Retriever dieser Website

Der Retriever des Assistenten nutzt für seine lexikalische Liste keine Volltextsuche, da es keine persische Konfiguration gibt. Er zählt, wie viele Inhaltswörter der Frage als ganze Wörter in einer Passage vorkommen, mit Wortgrenzen aus regulären Ausdrücken. Er entfernt die Stoppwörter jeder Sprache der Website, überspringt sehr kurze Terme, lässt die Vokalzeichen des Hindi in dem Wort, zu dem sie gehören, und wendet kein Stemming an. Das Ergebnis ist ein Abdeckungsgrad zwischen 0 und 1.

Wortgrenzen sind wichtig: Ein Teilstring-Abgleich auf „is“ trifft „his“, „this“ und „Isfahan“, und jede Passage sieht plötzlich wie ein Beleg aus. Auch der begrenzte Wertebereich zählt, denn mit ihm lässt sich eine Ähnlichkeitsschwelle vergleichen. Für die Ablehnungsschwelle zählt der Wert nur, wenn mindestens zwei verschiedene Inhaltswörter übereinstimmen.

Gelernte Sparse-Vektoren: die dokumentierte Option

Learned Sparse Retrieval gleicht Terme ab wie eine lexikalische Suche, mit Gewichten, die ein Modell lernt. Bei BGE-M3 sind die Terme Teilstücke aus dem Tokenizer (seine Model Card gewichtet „De“, „fin“ und „ation“), und der Vektor ist, in den Worten des Papers, das das Modell vorgestellt hat, „so groß wie das Vokabular“: 250.002 in der Konfiguration des Modells. Der Produktions-Retriever dieser Website nutzt sie nicht: Er kombiniert eine dichte und eine lexikalische Suche, und die Embeddings-API, die er aufruft, liefert keine Sparse-Gewichte. Wenn Sie sie ergänzen, lesen Sie zuerst den Abschnitt zum Off-by-one-Fehler weiter unten.

Hybride Suche mit Reciprocal Rank Fusion: Ränge fusionieren, nicht Scores

Ein dichter und ein lexikalischer Retriever liefern Ranglisten, deren Scores Verschiedenes bedeuten. Reciprocal Rank Fusion ignoriert die Scores und kombiniert die Positionen.

Die Formel und woher k = 60 kommt

Cormack, Clarke und Büttcher definieren den fusionierten Score eines Dokuments als Summe von 1/(k + r(d)) über alle Rankings, wobei r(d) sein Rang in der jeweiligen Liste ist. Die Konstante ist weniger heikel, als sie aussieht: „k = 60 wurde in einer Pilotuntersuchung festgelegt und in der anschließenden Validierung nicht verändert“, und die Pilotuntersuchung „deutete darauf hin, dass k = 60 nahezu optimal war, die Wahl aber nicht kritisch“.

Der Retriever dieser Website verwendet k = 60, und jede Liste steuert mehr Kandidaten bei, als die Antwort verwenden wird; so kann eine Passage, die in der einen Liste im Mittelfeld und in der anderen weit oben steht, trotzdem nach oben kommen. In Grundzügen:

K = 60  # Cormack, Clarke and Büttcher

def fuse(lists, k=K):
    """lists: (weight, passage ids best-first) for each retriever."""
    scores = {}
    for weight, ranked in lists:
        for rank, passage in enumerate(ranked, start=1):
            scores[passage] = scores.get(passage, 0.0) + weight / (k + rank)
    return scores

def order(scores, locales_of, query_locale):
    # The fused score decides. Locale only separates exactly equal
    # scores, and the id keeps the order identical on every run.
    return sorted(scores, key=lambda p: (-scores[p], query_locale not in locales_of[p], str(p)))

fused = fuse([(1.0, dense_ids), (0.5, lexical_ids)])
passages = order(fused, locales_of, query_locale="fa")[:top_k]

Warum Score-Summen Gewichte brauchen, die Sie abstimmen müssen

Die Alternative ist, Scores zu addieren, wie es das Paper hinter BGE-M3 tut: s_rank ← w1·s_dense + w2·s_lex + w3·s_mul, „wobei die Werte von w1, w2 und w3 vom Downstream-Szenario abhängen“. Für Dense plus Sparse setzen die Autoren auf MIRACL w1 = 1 und w2 = 0,3, auf MLDR, einem Benchmark für lange Dokumente, dagegen w1 = 0,2 und w2 = 0,8.

Eine Kosinusähnlichkeit, ein Sparse-Skalarprodukt und ein Abdeckungsgrad liegen auf verschiedenen Skalen, und der Wechselkurs zwischen ihnen verschiebt sich mit dem Korpus. Rangfusion umgeht diese Kalibrierung. Das ist ein Kompromiss, kein Vorteil zum Nulltarif: Das Paper berichtet starke Ergebnisse mit gewichteten Summen, und RRF kann einen Erdrutschsieg nicht von einem knappen Sieg unterscheiden.

Warum die lexikalische Liste halbes Gewicht bekommt

Auf dieser Website soll die lexikalische Liste Passagen nach oben bringen, die exakte Zeichenketten enthalten: einen Domainnamen wie booqai.app, ein Akronym, einen Programmnamen wie SAMPAD — genau das, was ein dichtes Modell verwischen kann. Das ist der übliche Grund, eine solche Liste zu führen. In Anthropics Beitrag zu Contextual Retrieval heißt es, ein Embedding-Modell „findet vielleicht Inhalte über Fehlercodes im Allgemeinen, könnte aber den exakten Treffer ‚TS-999‘ verpassen“.

Die lexikalische Liste fließt mit halbem Gewicht ein. Das hebt eine Passage an, die auch die dichte Liste geliefert hat, ohne dass Stichwortüberschneidungen das dichte Ranking überstimmen. Das hat seinen Preis: Mit k = 60 und den Kandidatenzahlen dieser Website landet eine Passage, die nur die lexikalische Liste findet, unter jedem dichten Kandidaten; die Fusion kann also keine Passage zurückholen, die das dichte Modell vollständig verfehlt hat. Die einzige Ausnahme ist die Passage mit der höchsten Ähnlichkeit, die immer zurückgegeben wird, wie der nächste Abschnitt erklärt. Schlägt der Embedding-Aufruf fehl, fällt das Retrieval auf die lexikalische Liste allein zurück, und die Ähnlichkeitsschwelle gilt weiterhin.

Ein RRF-Score ist kein Konfidenzwert

Ein RRF-Score entsteht allein aus Positionen; also liefert selbst ein Korpus aus reinem Rauschen eine erstplatzierte Passage mit dem besten verfügbaren fusionierten Score. Die Ablehnungsschwelle dieser Website wird deshalb für jede Passage mit einer echten Ähnlichkeit verglichen, nie mit dem fusionierten Score: mit dem höheren Wert aus ihrer Kosinusähnlichkeit zur Frage, wie sie gestellt wurde (bei einer Anschlussfrage auch zur Frage zusammen mit der vorherigen, zwischen denen der Eintrag steht, auf den sie zeigt, wenn die Anschlussfrage als Position in einer Liste formuliert ist, oder der Anfang der vorherigen Antwort, wenn sich dieser Eintrag nicht lesen lässt), und ihrer Ganzwort-Abdeckung, die nur zählt, wenn mindestens zwei Inhaltswörter übereinstimmen. Die Schwelle wird geprüft, bevor das Sprachmodell aufgerufen wird.

Die Passage mit der höchsten Ähnlichkeit unter allen Kandidaten ist immer unter den zurückgegebenen, auch wenn die Fusion sie zu weit unten eingeordnet hat, um ausgewählt zu werden. Unterhalb der Schwelle wird eine Frage, die nicht mich betrifft, ohne Modellaufruf abgelehnt, und eine Frage über mich erhält stattdessen die Datensätze der Website; der Artikel über Guardrails gegen RAG-Halluzinationen behandelt beides.

Retrieval nie nach Sprache filtern

Ein Sprachfilter sieht nach Ordnung aus: Persische Fragen durchsuchen persische Passagen. In einem zweisprachigen Korpus löscht er Belege.

Ihr bester Beleg kann in der anderen Sprache stehen

Auf dieser Website liegen Passagen in allen Sprachen nebeneinander in einer Tabelle, und eine Frage durchsucht alle. Der persische Lebenslauf ist das Ausgangsdokument, der englische eine Adaption; eine englische Frage zu einer beruflichen Station kann ihren besten Beleg deshalb auf Persisch haben. Kartez Contract hatte dieselbe Konstellation in umgekehrter Richtung: Der Entwurfsassistent musste persische Anforderungen in eine Bibliothek englischsprachiger Fachtexte zur Vertragsgestaltung übertragen.

Sprache als Tiebreaker nur bei exaktem Gleichstand

Einen Nutzen im Ranking hat die Sprache trotzdem: Haben zwei Passagen exakt denselben fusionierten Score, kommt die zuerst, die in der Sprache der Frage vorliegt, danach entscheidet eine stabile ID, wie im Sortierschlüssel oben. Mit einer dichten Liste und der lexikalischen Liste bei halbem Gewicht kommen exakte Gleichstände praktisch nie vor. Sie können vorkommen, wenn eine Frage in mehreren Fassungen gesucht wird, wie weiter unten beschrieben, und zwei Passagen aus den gleich gewichteten dichten Listen dieser Fassungen dieselben Ränge erhalten; selbst dann entscheidet die Sprache nur, wenn lediglich eine der beiden in der Sprache der Frage vorliegt. Das ist Absicht: Die Sprache darf nie schwerer wiegen als ein Score. Eine weitere Aufgabe hat die Sprache: Sie bestimmt, welche Übersetzung einer Passage der Leser sieht.

Ein kleiner Bonus für gleichsprachige Passagen ist kein Tiebreaker. Benachbarte Beiträge aus einer Liste mit vollem Gewicht unterscheiden sich um 1/((k + r)(k + r + 1)), ein Abstand, der weiter unten in der Liste schrumpft, und Summen aus zwei Listen können noch dichter beieinanderliegen. Ein fester Bonus ist nur sicher, solange er unter jedem Abstand bleibt, den Ihre Listentiefen und Gewichte erzeugen können, und diese Grenze verschiebt sich, sobald sich eines von beidem ändert. Überschreitet der Bonus diese Grenze, tauschen Ergebnisse, die nicht gleichauf lagen, mitten in der Liste die Plätze, und zwar genau unter den Kandidaten, die die Fusion nach oben bringen soll.

Eine Passage, zehn Übersetzungen

Die Seiten dieser Website erscheinen in bis zu zehn Sprachen, und Fassungen derselben Passage in verschiedenen Sprachen konnten mehrere der acht Plätze belegen, die das Modell erhält. Das Retrieval behandelt die Übersetzungen einer Passage jetzt als einen Kandidaten: Er wird nach seiner besten Übersetzung bewertet, einmal eingeordnet und in der Sprache des Lesers angezeigt, wenn es diese Übersetzung gibt. Artikel werden nach Abschnitten gruppiert statt nach Chunks, weil ein langer Abschnitt in jeder Sprache in eine andere Zahl von Chunks zerfällt; eine Übersetzung, deren Abschnitte nicht zu denen der englischen Fassung passen, bleibt für sich, statt falsch gruppiert zu werden. Die Sprache des Lesers bestimmt, welche Übersetzung erscheint; außer bei exaktem Gleichstand bestimmt sie nie, wo die Passage im Ranking steht.

Persische Fragen an englische Dokumente

Nur die dichte Seite kann Sprachgrenzen überwinden: Eine persische Frage und eine englische Passage haben kaum Wörter gemeinsam, abgesehen von lateinisch geschriebenen Begriffen wie RAG. Die Evaluation von BGE-M3 selbst klärt das für Persisch nicht: Ihr sprachübergreifender Benchmark MKQA sucht Passagen aus der englischen Wikipedia zu Anfragen in 25 Sprachen außer Englisch, Arabisch eingeschlossen, Persisch nicht. Testen Sie Ihre eigenen persisch-englischen Paare.

Fragen ohne Stichwörter

Die Frage eines Besuchers „Was hat er zuletzt gemacht?“ enthält kein Wort, das das Korpus treffen kann, und „Und davor?“ enthält gar nichts. Betrifft eine Frage mich, wird sie zusätzlich mit meinem Namen davor gesucht, und eine Anschlussfrage zusätzlich zusammen mit der vorherigen Frage; ist sie als Position in einer Liste formuliert, etwa „das zweite“, steht der Eintrag dazwischen, auf den sie zeigt, oder der Anfang der vorherigen Antwort, wenn sich dieser Eintrag nicht lesen lässt. Alle Fassungen werden in einem einzigen Aufruf eingebettet und nach Rang fusioniert. Die Fassung mit meinem Namen ordnet die Passagen, zählt aber nie für die Ablehnungsschwelle, denn mein Name allein kann eine Frage über mich, die mit meiner Arbeit nichts zu tun hat, nah an meinen Lebenslauf rücken. Fragen zu meiner Arbeit als Ganzes hängen nicht vom Retrieval ab: Sie werden aus datierten Datensätzen beantwortet, die die Website aus meinem Lebenslauf, meinen Fallstudien, Artikeln und Leistungen erstellt, wie der Artikel über Guardrails gegen RAG-Halluzinationen beschreibt.

Persische Verträge nach Artikel, Klausel und Anmerkung chunken

Persische Verträge haben ihre eigene Struktur aus Artikeln (ماده), Klauseln (بند) und Anmerkungen (تبصره). Im Copiloten von Kartez Contract wurde der Vertrag des Nutzers entlang dieser Struktur zerlegt, und für die Antwort auf eine Frage wurden nur die dafür relevanten Abschnitte herangezogen; Antworten „stützen sich nur auf die ausgewählten Klauseln“. Die Fallstudie zu Kartez Contract beschreibt den Rest des Projekts, der Artikel Was man gegen Halluzinationen juristischer KI bauen sollte das Fachgebiet.

Dasselbe Prinzip steuert das Chunking dieser Website: Ein Chunk ist ein semantischer Datensatz. Eine berufliche Station im Lebenslauf wird nie von ihren Zeitangaben getrennt, eine Fallstudie wird an den Abschnitten geteilt, in die sie gegliedert ist, ein Beitrag an seinen Überschriften, und ein langer Abschnitt wird nur an einer Absatzgrenze umbrochen. Jeder Abschnitts-Chunk trägt den Dokumenttitel, sodass ein einzeln abgerufener Chunk noch verrät, woher er stammt.

Für persische Verträge ergeben sich daraus einige Regeln, gedacht als allgemeine Empfehlung und nicht als Beschreibung, wie Kartez gebaut wurde:

  • Die Nummer beim Text lassen. „ماده ۵“ und sein Wortlaut gehören in einen Chunk.

  • Die Überschrift des übergeordneten Artikels in jeden Klausel- und Anmerkungs-Chunk mitnehmen.

  • Eine Klausel nie nach Tokenzahl zerschneiden. Eine von ihrer Klausel getrennte Anmerkung kann verändern, was die Klausel zu sagen scheint.

  • Den Strukturpfad speichern, Artikel › Klausel › Anmerkung, damit eine Antwort eine Fachperson zur genauen Stelle führen kann.

pgvector: exakter Scan oder HNSW?

pgvector beantwortet die Hälfte davon selbst: „Standardmäßig führt pgvector eine exakte Nearest-Neighbor-Suche durch, die perfekten Recall bietet.“ Ein approximativer Index „tauscht etwas Recall gegen Geschwindigkeit“.

Weniger offensichtlich sind die Kosten bei Filtern. „Bei approximativen Indizes wird die Filterung nach dem Scan des Index angewendet. Trifft eine Bedingung auf 10 Prozent der Zeilen zu, passen mit HNSW und dem Standardwert 40 für hnsw.ef_search im Durchschnitt nur 4 Zeilen.“ Ab Version 0.8.0 kann pgvector den Index iterativ scannen, bis genug Zeilen passen, wenn Sie das aktivieren.

Die Retrieval-Tabelle dieser Website hat keinen approximativen Index: Die Vektorsuche ist ein exakter Scan. Der Veröffentlichungsstatus ist Teil der WHERE-Klausel in SQL, nie ein Filter nach LIMIT, wo er die Ergebnismenge stillschweigend verkleinern würde. Beginnen Sie mit einem exakten Scan, ergänzen Sie einen Index, wenn die Latenz es verlangt, und vergleichen Sie seine Top-k auf Ihren eigenen Fragen mit einem exakten Scan, bevor Sie ihm vertrauen.

Der Off-by-one-Fehler bei sparsevec in pgvector

Das spielt nur eine Rolle, wenn Sie PostgreSQL um gelernte Sparse-Vektoren erweitern, und der Fehler bleibt stumm.

Das Textformat für Sparse-Vektoren in pgvector ist {index1:value1,index2:value2}/dimensions, „und Indizes beginnen wie bei SQL-Arrays bei 1“. Tokenizer-IDs beginnen bei 0. Der Python-Client nimmt 0-basierte Indizes entgegen („Indizes beginnen bei 0“) und addiert beim Schreiben der Textform selbst 1.

from pgvector import SparseVector

weights = {0: 0.05, 2: 0.14, 4: 0.04}  # tokeniser ids, 0-based

# Client library: pass 0-based ids; it adds 1 when it writes SQL text.
vec = SparseVector(weights, 6)

# Hand-built SQL literal: shift exactly once, here.
literal = "{" + ",".join(f"{i + 1}:{w}" for i, w in weights.items()) + "}/6"
# "{1:0.05,3:0.14,5:0.04}/6"

Verschieben Sie genau einmal, und zwar dort, wo ein SQL-Literal von Hand gebaut wird. Die Texteingabe von pgvector rechnet jeden Index in eine 0-basierte Zählung um und meldet „sparsevec index out of bounds“ nur, wenn das Ergebnis unter 0 liegt oder die Dimension erreicht oder überschreitet. Eine doppelte Verschiebung führt also nur am oberen Ende des Vokabulars zu einem Fehler, eine fehlende nur bei Token-ID 0, und jedes andere Gewicht landet stillschweigend auf dem Nachbartoken.

Ein Embedding-Modell für Persisch (Farsi) auswählen

Was MIRACL über Persisch und Arabisch sagt

MIRACL ist ein Retrieval-Benchmark über 18 Sprachen, darunter Persisch und Arabisch. Das Paper hinter BGE-M3 berichtet nDCG@10, ein Maß für die Ranking-Qualität der ersten zehn Ergebnisse, auf dem Entwicklungsset von MIRACL (ACL-Version; die Autoren haben diese Ergebnisse im Juli 2024 aktualisiert):

Retriever

Persisch (fa)

Arabisch (ar)

BM25 auf XLM-RoBERTa-Tokens

28,7

39,5

mE5-large (multilinguales E5)

59,0

76,0

BGE-M3 sparse

45,1

67,1

BGE-M3 dense

60,9

78,4

BGE-M3 dense + sparse + multi-vector

63,4

80,2

Auf Persisch liegt dichtes Retrieval weit über diesem BM25, gelerntes Sparse-Retrieval liegt dazwischen, und die Kombination aller drei Modi bringt gegenüber dense noch ein wenig mehr. Vier Vorbehalte gehören zu diesen Zahlen:

  • Dieses BM25 arbeitet mit Subword-Tokens. Die Autoren haben es passend zum Modell auf dem Tokenizer von XLM-RoBERTa ausgeführt; ihr Anhang zeigt, dass der Analyzer von Lucene den MIRACL-Durchschnitt über alle Sprachen von 31,9 auf 38,5 hebt. MIRACLs eigene BM25-Baseline, mit sprachspezifischen Analyzern, wo es sie gibt, erreicht 0,333 nDCG@10 auf Persisch und 0,481 auf Arabisch, auf einer Skala von 0 bis 1.

  • MIRACL ist einsprachig. Es ist „dafür ausgelegt, monolinguale Retrieval-Aufgaben zu unterstützen, bei denen Anfragen und Korpora in derselben Sprache vorliegen“, und sagt daher nichts über persische Fragen an englische Dokumente.

  • Persisch und Arabisch sind kein kontrollierter Vergleich. Sie nutzen verschiedene Fragen und Wikipedia-Korpora; die Projektseite führt 632 Entwicklungsanfragen für Persisch und 2.896 für Arabisch auf. Der Abstand ist also ein Signal, kein Maß für Schwierigkeit.

  • BGE-M3 wurde auf den Trainingsdaten von MIRACL feinabgestimmt, seine Zeilen sind also In-Domain-Scores.

FaMTEB und Hakim

FaMTEB, veröffentlicht in Findings of EMNLP 2025, ist ein persischer Benchmark auf Basis von MTEB, der „63 Datensätze aus sieben verschiedenen Aufgaben“ umfasst, darunter Retrieval.

Hakim ist ein persisches Embedding-Modell, beschrieben in einem arXiv-Preprint, erstmals im Mai 2025 veröffentlicht und im Oktober 2025 überarbeitet. Seine Autoren berichten eine Verbesserung um 8,5 Prozent gegenüber bestehenden Ansätzen auf FaMTEB. Zwei Tatsachen gehören neben dieses Ergebnis: Die Autoren von Hakim sind auch die Autoren von FaMTEB, und die Trainingsdaten der zweiten Stufe von Hakim führen MIRACL_fa auf. Keine der beiden macht das Ergebnis falsch; beide sind Gründe, auf Ihren eigenen Daten zu testen.

Was Sie vor der Wahl testen sollten

  • Ihre eigenen Fragen auf Persisch und Englisch, umgangssprachliche Formulierungen eingeschlossen.

  • Sprachübergreifende Paare: persische Fragen, die englischer Text beantwortet, und umgekehrt.

  • Schreibvarianten derselben Frage: Formen von Yeh und Kaf, ZWNJ oder Leerzeichen, jedes Ziffernsystem.

  • Die Lizenz. Die Model Card von BGE-M3 nennt MIT.

  • Die maximale Eingabelänge im Verhältnis zu Ihrer Chunk-Größe, gezählt in Tokens. Die Autoren von BGE-M3 beschreiben lange Dokumente mit bis zu 8.192 Tokens, und Persisch kann für denselben Inhalt mehr Tokens brauchen als Englisch; der Artikel Die persische Token-Steuer fand diesen Aufschlag beim eigenen Tokenizer von BGE-M3 gering und bei den dort gemessenen Tokenizern von Generierungsmodellen deutlich größer.

  • Die Vektordimension. Auf dieser Website ist sie im Schema festgelegt; eine Änderung bedeutet also eine Migration und eine vollständige Neuindexierung.

Der Embedding-Explorer der Website ist bewusst nicht neuronal: Seine Vektoren aus Zeichen-N-Grammen gruppieren Wörter nach Schreibweise, nicht nach Bedeutung, und das ist die Lücke, die ein dichtes Modell bei umgangssprachlichem Persisch schließen muss.

Persisches RAG mit einem zweisprachigen Golden Set evaluieren

Ein Golden Set für persisches RAG braucht Fragen in beiden Sprachen: beantwortbare samt den Passagen, die sie finden sollen, solche ohne Beleg im Korpus, die abgelehnt werden müssen, und solche, deren bester Beleg in der anderen Sprache steht. Messen Sie recall@k auf den beantwortbaren Fragen, prüfen Sie, dass jede Frage ohne Beleg unter der Ähnlichkeitsschwelle bleibt, und ergänzen Sie einen Test, der fehlschlägt, wenn keine Antwort auf eine Passage in der anderen Sprache zurückgreift. Er erwischt einen Sprachfilter an dem Tag, an dem jemand ihn wieder einbaut.

Auf dieser Website läuft das Golden Set, in allen zehn Sprachen der Website und überwiegend mit englischen und persischen Fragen, in der CI durch den echten Code für Fusion, Gruppierung, Sortierung und Top-k, allerdings gegen ein In-Memory-Korpus und einen Bag-of-Tokens-Ersatz-Embedder statt des Produktions-Embedders. Der Ersatz speist außerdem eine Sparse-Liste, die in Produktion nicht läuft, und die Menge der Fragen ohne Beleg ist klein. Die CI erkennt Regressionen in der Ranking-Mechanik, nicht den Recall des laufenden Assistenten. Ein reiner Retrieval-Befehl, der das Sprachmodell nie aufruft, prüft die Ablehnungsschwelle gegen den produktiv eingesetzten Embedder; ein Recall-Lauf gegen diesen Embedder wurde nicht durchgeführt, deshalb veröffentliche ich keine Recall-Zahl für diesen Retriever.

Was sich auf Arabisch übertragen lässt

Retrieval für arabischen Text habe ich weder entworfen noch evaluiert. Der Retriever dieser Website indexiert zwar ihre arabischsprachigen Inhalte, aber nichts daran wurde für Arabisch gebaut oder getestet. Dieser Abschnitt beschreibt, was das persische Design auf dem Papier nahelegt.

Das meiste lässt sich übertragen: Rangfusion, kein Sprachfilter, strukturbewusstes Chunking, das Entfernen von Diakritika innerhalb von Vergleichen (die Aussage des W3C gilt für die arabische Schrift allgemein), zuerst ein exakter Scan und ein zweisprachiges Golden Set. Drei Dinge unterscheiden sich:

  • PostgreSQL liefert Arabisch tatsächlich mit. Snowball hat einen arabischen Stemmer und PostgreSQL ein Wörterbuch arabic_stem; ob Stemming hilft, entscheidet Ihr Golden Set.

  • Die persische Buchstaben-Vereinheitlichung darf nicht laufen. Im Arabischen sind Yeh (U+064A) und Kaf (U+0643) die richtigen Buchstaben; ein gemischtes Korpus muss deshalb je nach Sprache normalisieren.

  • Die Ziffern unterscheiden sich. Arabisch-indische Ziffern liegen bei U+0660 bis U+0669, nicht im Bereich U+06F0, den Persisch verwendet.

Arabisch ist außerdem in Benchmarks besser abgedeckt, in MIRACL und MKQA. In der MKQA-Tabelle des BGE-M3-Papers erreichen arabische Fragen an die englische Wikipedia mit BGE-M3 dense einen Recall@100 von 71,1, gegenüber 18,9 für das Subword-BM25 des Papers; Recall@100 zählt eine Frage als gefunden, wenn eine Passage mit ihrer Antwort unter den ersten 100 Ergebnissen ist. Für Persisch gibt es keine solche sprachübergreifende Zahl.

Checkliste: hybride Suche für RAG auf Persisch

  • Persische Buchstabenformen beim Speichern vereinheitlichen, nur in persischen Inhalten; Bidi-Markierungen aus der Retrieval-Kopie entfernen.

  • Denselben Normalisierer auf jede Frage anwenden; den ZWNJ im gespeicherten Text behalten.

  • Die lexikalische Seite ohne Stemmer betreiben; Trigramm-Ähnlichkeit für Tippfehler ergänzen.

  • Dichte und lexikalische Ränge mit RRF fusionieren, k = 60, die lexikalische Liste mit halbem Gewicht, im Wissen, dass halbes Gewicht keine Passage retten kann, die die dichte Liste verfehlt hat.

  • Die Ablehnungsschwelle auf eine Ähnlichkeit legen, nie auf den RRF-Score.

  • Nie nach Sprache filtern; die Sprache nur bei exaktem Gleichstand entscheiden lassen und darüber, welche Übersetzung einer Passage angezeigt wird.

  • Entlang der Dokumentstruktur chunken, mit dem Titel in jedem Chunk.

  • Mit einem exakten Scan beginnen und jeden approximativen Index daran prüfen.

  • Mit einem Golden Set in jeder Sprache evaluieren, die Sie bedienen, mit sprachübergreifenden Fragen und Fragen ohne Beleg.

  • Embedding-Modelle an Ihren eigenen persischen Fragen testen, umgangssprachliche eingeschlossen.

Wenn Sie Retrieval über persische oder gemischtsprachige Inhalte planen, macht die Projektanalyse aus fünf Fragen eine erste Architektur.

Häufige Fragen

Reicht BM25 für die Suche auf Persisch?

Allein nicht. BM25 findet exakte Namen, Zahlen und Codes, verfehlt aber Paraphrasen, und eine umgangssprachliche persische Frage kann mit einem formellen Dokument nur wenige Wörter teilen. Auf dem persischen Set von MIRACL liegen die veröffentlichten Dense-Ergebnisse deutlich über BM25, ob BM25 nun Subword-Tokens oder die Sprachanalyzer von Lucene verwendet. Setzen Sie BM25 oder Ganzwort-Abdeckung als lexikalische Hälfte einer hybriden Suche ein.

Sollte man persische Anfragen vor dem Retrieval ins Englische übersetzen?

Nicht standardmäßig. Ein multilinguales Embedding-Modell bildet beide Sprachen bereits in einen gemeinsamen Vektorraum ab, und eine Übersetzung bringt einen zusätzlichen Modellaufruf, der Namen und exakte Begriffe verstümmeln kann. Der sprachübergreifende Benchmark, den die Autoren von BGE-M3 verwendet haben, enthält Arabisch, aber nicht Persisch; testen Sie also beide Ansätze an Ihren eigenen persisch-englischen Paaren.

Welches Embedding-Modell eignet sich am besten für persisches RAG?

Benchmarks liefern eine Vorauswahl, keine Antwort: MIRACL-Ergebnisse gibt es für BGE-M3 und multilinguales E5 auf Persisch, FaMTEB umfasst 63 Datensätze, und die Autoren von Hakim berichten Verbesserungen auf FaMTEB. Testen Sie die Vorauswahl an Ihren eigenen umgangssprachlichen und sprachübergreifenden Fragen, nachdem Sie Lizenz und Vektordimension geprüft haben.

Braucht man für persisches RAG eine eigene Vektordatenbank?

Für den Anfang nicht. pgvector in Ihrem bestehenden PostgreSQL führt standardmäßig eine exakte Nearest-Neighbor-Suche mit perfektem Recall durch und hält Filter wie den Veröffentlichungsstatus innerhalb der SQL-Abfrage. Ergänzen Sie einen approximativen Index erst, wenn die Latenz es erfordert.

Warum findet meine persische Suche ein Wort nicht, das eindeutig im Text steht?

Meist, weil sich die Zeichenketten auf eine Weise unterscheiden, die das Auge nicht sieht: arabisches Yeh oder Kaf (U+064A, U+0643) statt persischem ی oder ک, ein ZWNJ, der als Leerzeichen geschrieben oder weggelassen wurde, ein anderes Ziffernsystem oder ein verirrtes diakritisches Zeichen. Normalisieren Sie gespeicherte Passagen und Fragen mit derselben Funktion, und vereinheitlichen Sie Ziffern und Diakritika auf beiden Seiten des Vergleichs.

Quellen