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.
Strategia di Progettazione Generale
- Applicare prima la normalizzazione Unicode NFKC per gestire legature, forme di presentazione e unificazione delle cifre.
- Quindi applicare mappature di caratteri personalizzate in un ordine definito (ad esempio, normalizzare le varianti di Hamza prima di Ya arabo).
- 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).
- Tutte le mappature dovrebbero essere configurabili tramite una tabella di mappatura centralizzata nel database o un hash Ruby nell’applicazione.
Profili di Normalizzazione
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
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
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 |
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
Implementazione in PostgreSQL
- Creare una tabella
text_normalization_map - Utilizzare catene
regexp_replaceoTRANSLATEper 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_conservativeper 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
Esempio di Hash Ruby (per Discourse)
NORMALIZATION_MAP = {
"ك" => "ک",
"ي" => "ی",
"ى" => "ی",
"أ" => "ا",
"إ" => "ا",
"ٱ" => "ا",
"ؤ" => "و",
"ئ" => "ی",
"ة" => "ه",
"ۀ" => "هٔ",
"۴" => "4",
"٤" => "4",
"\u200C" => "", # ZWNJ
"\u200D" => "", # ZWJ
}
Note sulle Prestazioni e Pratiche
- Applicare NFKC nello strato applicativo (ad esempio, Ruby
unicode_normalize(:nfkc)) - Utilizzare indici separati per i profili conservativo vs. permissivo
- Evitare mappature forzate di caratteri semanticamente sensibili (ad esempio, Hamza, Taa Marbuta) a meno che non siano esplicitamente configurate
- Eseguire test A/B su dati reali del forum per misurare il tasso di successo e i falsi positivi
- Documentare ogni mappatura con motivazioni ed esempi
- Definire unit test sia in Ruby che in SQL per ogni mappatura
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