Je ne suis pas un expert technique, mais j’ai recherché ce problème car je veux m’assurer qu’aucune requête de recherche ne soit manquée sur un forum bilingue persan-arabe Discourse. Comme Discourse utilise PostgreSQL, la normalisation devient essentielle : un utilisateur pourrait rechercher en utilisant des caractères persans, tandis que le même mot est stocké en utilisant des caractères arabes, ou vice versa. Sans normalisation appropriée, la recherche échouera.
D’après ce que j’ai appris, l’utilisation de la normalisation Unicode NFKC est un bon point de départ : elle gère de nombreux cas de compatibilité tels que les ligatures, les formes de présentation et les chiffres arabes/persans.
Cependant, pour le texte persan et arabe, NFKC seul n’est pas suffisant. Il ne normalise pas plusieurs variantes de caractères critiques qui sont visuellement et sémantiquement équivalentes mais diffèrent au niveau binaire.
Ci-dessous, je décris les procédures et les idées auxquelles je suis parvenu grâce à mes recherches et explorations.
Stratégie de conception globale
- Appliquer d’abord la normalisation Unicode NFKC pour gérer les ligatures, les formes de présentation et l’unification des chiffres.
- Appliquer ensuite des mappages de caractères personnalisés dans un ordre défini (par exemple, normaliser les variantes de Hamza avant le Ya arabe).
- Séparer les politiques de normalisation pour le stockage et la recherche :
- Utiliser un profil Conservateur pour le stockage canonique (préserver ZWNJ, éviter les changements sémantiques).
- Utiliser un profil Permissif pour la recherche (ignorer ZWNJ, unifier les variantes de Hamza, normaliser les chiffres).
- Tous les mappages doivent être configurables via une table de mappage centralisée dans la base de données ou un hash Ruby dans l’application.
Profils de normalisation
Conservateur (pour le stockage)
- Transformation minimale
- Appliquer NFKC
- Normaliser le Kaf/Ya arabe en équivalents persans
- Supprimer les diacritiques
- Préserver ZWNJ
- Stocker sous forme de
texte_original+normalisé_conservateur
Permissif (pour la recherche)
- Correspondance agressive
- Appliquer toutes les règles Conservatrices
- Supprimer/ignorer ZWNJ
- Normaliser les variantes de Hamza en lettres de base
- Convertir tous les chiffres en ASCII
- Optionnellement, unifier Taa Marbuta → Heh
- Utilisé pour le prétraitement des requêtes
Table de mappage complète
| Source | Cible | Unicode | Notes |
|---|---|---|---|
ك |
ک |
U+0643 → U+06A9 | Kaf arabe → Kaf persan |
ي |
ی |
U+064A → U+06CC | Ya arabe → Ya persan |
ى |
ی |
U+0649 → U+06CC | Variante Ya finale |
أ, إ, ٱ |
ا |
Divers → U+0627 | Formes de Hamza → Alef |
ؤ |
و |
U+0624 → U+0648 | Hamza Waw |
ئ |
ی |
U+0626 → U+06CC | Hamza Ya |
ء |
— | U+0621 | Supprimer ou préserver (configurable) |
ة |
ه |
U+0629 → U+0647 | Taa Marbuta → Heh (optionnel) |
ۀ |
هٔ |
U+06C0 ↔ U+0647+U+0654 | Normaliser la forme composée |
ڭ |
گ |
U+06AD → U+06AF | Variantes régionales |
|
— | U+200C | ZWNJ : préserver en Conservateur, supprimer en Permissif |
٤, ۴ |
4 |
U+0664, U+06F4 → ASCII | Normaliser les chiffres |
| Diacritiques | — | U+064B–U+0652 | Supprimer tous les harakat |
| ZWJ | — | U+200D | Supprimer les joncteurs invisibles |
| Espaces multiples | Espace simple | — | Normaliser l’espacement |
Extrait de mappage rapide (pour SQL ou Ruby)
ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (supprimé en permissif)
Harakat (U+064B..U+0652) → supprimé
ZWJ (U+200D) → supprimé
Implémentation dans PostgreSQL
- Créer une table
text_normalization_map - Utiliser
regexp_replaceou des chaînesTRANSLATEpour la performance - Optionnellement, implémenter en PL/Python ou PL/v8 pour le support Unicode
- Normaliser le contenu stocké et les requêtes entrantes en utilisant la même logique
Stratégie d’indexation
- Stocker
normalized_conservativepour l’indexation canonique - Normaliser les requêtes avec
normalize_persian_arabic(query, 'permissive') - Si vous utilisez la recherche permissive, l’index doit correspondre au même profil
- Optionnellement, stocker les deux versions pour une comparaison croisée
Exemple de Hash Ruby (pour Discourse)
NORMALIZATION_MAP = {
"ك" => "ک",
"ي" => "ی",
"ى" => "ی",
"أ" => "ا",
"إ" => "ا",
"ٱ" => "ا",
"ؤ" => "و",
"ئ" => "ی",
"ة" => "ه",
"ۀ" => "هٔ",
"۴" => "4",
"٤" => "4",
"\u200C" => "", # ZWNJ
"\u200D" => "", # ZWJ
}
Notes de performance et pratiques
- Appliquer NFKC dans la couche applicative (par exemple, Ruby
unicode_normalize(:nfkc)) - Utiliser des index séparés pour les profils conservateur et permissif
- Éviter les mappages forcés de caractères sémantiquement sensibles (par exemple, Hamza, Taa Marbuta) sauf s’ils sont explicitement configurés
- Exécuter des tests A/B sur des données réelles du forum pour mesurer le taux de correspondance et les faux positifs
- Documenter chaque mappage avec sa justification et des exemples
- Définir des tests unitaires en Ruby et en SQL pour chaque mappage
Recommandation finale
- Utiliser Unicode NFKC comme base
- L’étendre avec une couche de mappage personnalisée
- Maintenir des profils doubles pour le stockage et la recherche
- Implémenter la normalisation dans les couches applicative et de base de données
- Documenter et tester chaque mappage
- Créer des index appropriés (
GIN + to_tsvector) sur les colonnes normalisées