Normalizzazione della ricerca araba: supporto mancante per varianti Hamza, forme Ya/Kaf ed equivalenza ortografica

Non sono un esperto tecnico, ma ho ricercato questo problema perché voglio assicurarmi che nessuna query di ricerca venga persa su un forum bilingue persiano-arabo di Discourse. Poiché Discourse utilizza PostgreSQL, la normalizzazione diventa essenziale: un utente potrebbe cercare usando caratteri persiani, mentre la stessa parola è memorizzata usando caratteri arabi, o viceversa. Senza una corretta normalizzazione, la ricerca fallirà.
Sulla base di ciò che ho appreso, l’utilizzo della normalizzazione Unicode NFKC è un buon punto di partenza: gestisce molti casi di compatibilità come legature, forme di presentazione e cifre arabe/persiane.

Tuttavia, per il testo persiano e arabo, NFKC da solo non è sufficiente. Non normalizza diverse varianti di caratteri critiche che sono visivamente e semanticamente equivalenti ma differiscono a livello binario.

Di seguito, delineo le procedure e le intuizioni che ho raggiunto attraverso la mia ricerca ed esplorazione.


:wrench: Strategia di Progettazione Generale

  1. Applicare prima la normalizzazione Unicode NFKC per gestire legature, forme di presentazione e unificazione delle cifre.
  2. Quindi applicare mappature di caratteri personalizzate in un ordine definito (ad esempio, normalizzare le varianti di Hamza prima di Ya arabo).
  3. Separare le policy di normalizzazione per l’archiviazione e la ricerca:
    • Utilizzare un profilo Conservativo per l’archiviazione canonica (preservare ZWNJ, evitare spostamenti semantici).
    • Utilizzare un profilo Permissivo per la ricerca (ignorare ZWNJ, unificare le varianti di Hamza, normalizzare le cifre).
  4. Tutte le mappature dovrebbero essere configurabili tramite una tabella di mappatura centralizzata nel database o un hash Ruby nell’applicazione.

:one: Profili di Normalizzazione

:green_circle: Conservativo (per l’archiviazione)

  • Trasformazione minima
  • Applicare NFKC
  • Normalizzare Kaf/Ya arabi in equivalenti persiani
  • Rimuovere i segni diacritici
  • Preservare ZWNJ
  • Archiviare come original_text + normalized_conservative

:blue_circle: Permissivo (per la ricerca)

  • Corrispondenza aggressiva
  • Applicare tutte le regole Conservativo
  • Rimuovere/ignorare ZWNJ
  • Normalizzare le varianti di Hamza in lettere di base
  • Convertire tutte le cifre in ASCII
  • Opzionalmente unificare Taa Marbuta → Heh
  • Utilizzato per la pre-elaborazione delle query

:two: Tabella di Mappatura Completa

Sorgente Destinazione Unicode Note
ك ک U+0643 → U+06A9 Kaf arabo → Kaf persiano
ي ی U+064A → U+06CC Ya arabo → Ya persiano
ى ی U+0649 → U+06CC Variante Ya finale
أ, إ, ٱ ا Varie → U+0627 Forme di Hamza → Alef
ؤ و U+0624 → U+0648 Hamza Waw
ئ ی U+0626 → U+06CC Hamza Ya
ء — U+0621 Rimuovere o preservare (configurabile)
ة ه U+0629 → U+0647 Taa Marbuta → Heh (opzionale)
ۀ هٔ U+06C0 ↔ U+0647+U+0654 Normalizzare forma composta
ڭ گ U+06AD → U+06AF Varianti regionali
‌ — U+200C ZWNJ: preservare in Conservativo, rimuovere in Permissivo
٤, ۴ 4 U+0664, U+06F4 → ASCII Normalizzare cifre
Diacritici — U+064B–U+0652 Rimuovere tutti gli harakat
ZWJ — U+200D Rimuovere joiner invisibili
Spazi multipli Spazio singolo — Normalizzare spaziature

:three: Snippet di Mappatura Veloce (per SQL o Ruby)

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

:four: Implementazione in PostgreSQL

  • Creare una tabella text_normalization_map
  • Utilizzare catene regexp_replace o TRANSLATE per le prestazioni
  • Opzionalmente implementare in PL/Python o PL/v8 per il supporto Unicode
  • Normalizzare sia il contenuto archiviato che le query in arrivo utilizzando la stessa logica

Strategia di Indicizzazione

  • Archiviare normalized_conservative per l’indicizzazione canonica
  • Normalizzare le query con normalize_persian_arabic(query, 'permissive')
  • Se si utilizza la ricerca permissiva, l’indice deve corrispondere allo stesso profilo
  • Opzionalmente archiviare entrambe le versioni per il confronto incrociato

:five: Esempio di Hash Ruby (per Discourse)

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

:six: Note sulle Prestazioni e Pratiche

  1. Applicare NFKC nello strato applicativo (ad esempio, Ruby unicode_normalize(:nfkc))
  2. Utilizzare indici separati per i profili conservativo vs. permissivo
  3. Evitare mappature forzate di caratteri semanticamente sensibili (ad esempio, Hamza, Taa Marbuta) a meno che non siano esplicitamente configurate
  4. Eseguire test A/B su dati reali del forum per misurare il tasso di successo e i falsi positivi
  5. Documentare ogni mappatura con motivazioni ed esempi
  6. Definire unit test sia in Ruby che in SQL per ogni mappatura

:seven: Raccomandazione Finale

  • Utilizzare Unicode NFKC come base
  • Estenderlo con uno strato di mappatura personalizzato
  • Mantenere profili duali per l’archiviazione e la ricerca
  • Implementare la normalizzazione sia nello strato dell’app che in quello del database
  • Documentare e testare ogni mappatura
  • Creare indici appropriati (GIN + to_tsvector) sulle colonne normalizzate