Arabische Suchnormalisierung: Fehlende Unterstützung für Hamza-Varianten, Ya/Kaf-Formen und orthografische Äquivalenz

Ich bin kein technischer Experte, aber ich habe mich mit diesem Thema beschäftigt, da ich sicherstellen möchte, dass keine Suchanfragen auf einem persisch-arabischen zweisprachigen Discourse-Forum übersehen werden. Da Discourse PostgreSQL verwendet, wird die Normalisierung unerlässlich: Ein Benutzer könnte mit persischen Zeichen suchen, während dasselbe Wort in arabischen Zeichen gespeichert ist – oder umgekehrt. Ohne ordnungsgemäße Normalisierung schlägt die Suche fehl.
Basierend auf dem, was ich gelernt habe, ist die Unicode NFKC-Normalisierung ein solider Ausgangspunkt – sie behandelt viele Kompatibilitätsfälle wie Ligaturen, Darstellungsformen und arabische/persische Ziffern.

NFKC allein reicht jedoch für persischen und arabischen Text nicht aus. Es normalisiert mehrere kritische Zeichenvarianten nicht, die visuell und semantisch äquivalent sind, sich aber auf binärer Ebene unterscheiden.

Unten skizziere ich die Verfahren und Erkenntnisse, zu denen ich durch meine Forschung und Erkundung gelangt bin.


:wrench: Gesamtentwurfsstrategie

  1. Zuerst Unicode NFKC-Normalisierung anwenden, um Ligaturen, Darstellungsformen und Ziffernvereinheitlichung zu behandeln.
  2. Dann benutzerdefinierte Zeichenzuordnungen anwenden in einer definierten Reihenfolge (z. B. Hamza-Varianten vor arabischem Ya normalisieren).
  3. Separate Normalisierungsrichtlinien für Speicherung vs. Suche:
    • Verwenden Sie ein konservatives Profil für die kanonische Speicherung (ZWNJ beibehalten, semantische Verschiebungen vermeiden).
    • Verwenden Sie ein permissives Profil für die Suche (ZWNJ ignorieren, Hamza-Varianten vereinheitlichen, Ziffern normalisieren).
  4. Alle Zuordnungen sollten über eine zentralisierte Zuordnungstabelle in der Datenbank oder einen Ruby-Hash in der Anwendung konfigurierbar sein.

:one: Normalisierungsprofile

:green_circle: Konservativ (für Speicherung)

  • Minimale Transformation
  • NFKC anwenden
  • Arabisches Kaf/Ya in persische Entsprechungen normalisieren
  • Diakritika entfernen
  • ZWNJ beibehalten
  • Speichern als original_text + normalized_conservative

:blue_circle: Permissiv (für Suche)

  • Aggressives Matching
  • Alle konservativen Regeln anwenden
  • ZWNJ entfernen/ignorieren
  • Hamza-Varianten in Basisbuchstaben normalisieren
  • Alle Ziffern in ASCII konvertieren
  • Optional Taa Marbuta → Heh vereinheitlichen
  • Wird für die Vorverarbeitung von Abfragen verwendet

:two: Umfassende Zuordnungstabelle

Quelle Ziel Unicode Hinweise
ك ک U+0643 → U+06A9 Arabisches Kaf → Persisches Kaf
ي ی U+064A → U+06CC Arabisches Ya → Persisches Ya
ى ی U+0649 → U+06CC Finale Ya-Variante
أ, إ, ٱ ا Verschiedene → U+0627 Hamza-Formen → Alef
ؤ و U+0624 → U+0648 Hamza Waw
ئ ی U+0626 → U+06CC Hamza Ya
ء — U+0621 Entfernen oder beibehalten (konfigurierbar)
ة ه U+0629 → U+0647 Taa Marbuta → Heh (optional)
ۀ هٔ U+06C0 ↔ U+0647+U+0654 Zusammengesetzte Form normalisieren
ڭ گ U+06AD → U+06AF Regionale Varianten
‌ — U+200C ZWNJ: in Konservativ beibehalten, in Permissiv entfernen
٤, ۴ 4 U+0664, U+06F4 → ASCII Ziffern normalisieren
Diakritika — U+064B–U+0652 Alle Harakat entfernen
ZWJ — U+200D Unsichtbare Verbinder entfernen
Mehrere Leerzeichen Einzelnes Leerzeichen — Leerzeichen normalisieren

:three: Schneller Zuordnungs-Snippet (für SQL oder Ruby)

ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (in permissiv entfernt)
Harakat (U+064B..U+0652) → entfernt
ZWJ (U+200D) → entfernt

:four: Implementierung in PostgreSQL

  • Erstellen Sie eine Tabelle text_normalization_map.
  • Verwenden Sie regexp_replace oder TRANSLATE-Ketten für die Leistung.
  • Implementieren Sie optional in PL/Python oder PL/v8 für Unicode-Unterstützung.
  • Normalisieren Sie sowohl gespeicherte Inhalte als auch eingehende Abfragen mit derselben Logik.

Indexierungsstrategie

  • Speichern Sie normalized_conservative für die kanonische Indexierung.
  • Normalisieren Sie Abfragen mit normalize_persian_arabic(query, 'permissive').
  • Wenn Sie permissive Suche verwenden, muss der Index demselben Profil entsprechen.
  • Speichern Sie optional beide Versionen für den Quervergleich.

:five: Ruby Hash Beispiel (für Discourse)

NORMALIZATION_MAP = {
  "ك" => "ک",
  "ي" => "ی",
  "ى" => "ی",
  "أ" => "ا",
  "إ" => "ا",
  "ٱ" => "ا",
  "ؤ" => "و",
  "ئ" => "ی",
  "ة" => "ه",
  "ۀ" => "هٔ",
  "۴" => "4",
  "٤" => "4",
  "\u200C" => "", # ZWNJ
  "\u200D" => "", # ZWJ
}

:six: Leistungs- und praktische Hinweise

  1. Wenden Sie NFKC in der Anwendungsschicht an (z. B. Ruby unicode_normalize(:nfkc)).
  2. Verwenden Sie separate Indizes für konservative vs. permissive Profile.
  3. Vermeiden Sie erzwungene Zuordnungen von semantisch sensiblen Zeichen (z. B. Hamza, Taa Marbuta), es sei denn, sie sind explizit konfiguriert.
  4. Führen Sie A/B-Tests mit echten Forendaten durch, um Trefferquote und Fehlalarme zu messen.
  5. Dokumentieren Sie jede Zuordnung mit Begründung und Beispielen.
  6. Definieren Sie Unit-Tests sowohl in Ruby als auch in SQL für jede Zuordnung.

:seven: Abschließende Empfehlung

  • Verwenden Sie Unicode NFKC als Basis.
  • Erweitern Sie es mit einer benutzerdefinierten Zuordnungsschicht.
  • Pflegen Sie duale Profile für Speicherung und Suche.
  • Implementieren Sie die Normalisierung sowohl in der Anwendungs- als auch in der Datenbankebene.
  • Dokumentieren und testen Sie jede Zuordnung.
  • Erstellen Sie geeignete Indizes (GIN + to_tsvector) für normalisierte Spalten.