Não sou um especialista técnico, mas tenho pesquisado este problema porque quero garantir que nenhuma consulta de pesquisa seja perdida em um fórum bilíngue Persa-Árabe no Discourse. Como o Discourse usa PostgreSQL, a normalização torna-se essencial: um usuário pode pesquisar usando caracteres persas, enquanto a mesma palavra é armazenada usando caracteres árabes — ou vice-versa. Sem a normalização adequada, a pesquisa falhará.
Com base no que aprendi, usar a normalização NFKC do Unicode é um bom ponto de partida — ela lida com muitos casos de compatibilidade, como ligaturas, formas de apresentação e dígitos árabes/persas.
No entanto, para texto persa e árabe, o NFKC sozinho não é suficiente. Ele não normaliza várias variantes de caracteres críticas que são visual e semanticamente equivalentes, mas diferem no nível binário.
Abaixo, descrevo os procedimentos e insights que cheguei através de minha pesquisa e exploração.
Estratégia Geral de Design
- Aplique primeiro a normalização NFKC do Unicode para lidar com ligaturas, formas de apresentação e unificação de dígitos.
- Em seguida, aplique mapeamentos de caracteres personalizados em uma ordem definida (por exemplo, normalize variantes de Hamza antes de Ya árabe).
- Separe as políticas de normalização para armazenamento vs. pesquisa:
- Use um perfil Conservador para armazenamento canônico (preserve ZWNJ, evite mudanças semânticas).
- Use um perfil Permissivo para pesquisa (ignore ZWNJ, unifique variantes de Hamza, normalize dígitos).
- Todos os mapeamentos devem ser configuráveis através de uma tabela de mapeamento centralizada no banco de dados ou um hash Ruby na aplicação.
Perfis de Normalização
Conservador (para armazenamento)
- Transformação mínima
- Aplique NFKC
- Normalize Kaf/Ya árabe para equivalentes persas
- Remova diacríticos
- Preserve ZWNJ
- Armazene como
texto_original+normalizado_conservador
Permissivo (para pesquisa)
- Correspondência agressiva
- Aplique todas as regras Conservadoras
- Remova/ignore ZWNJ
- Normalize variantes de Hamza para letras base
- Converta todos os dígitos para ASCII
- Opcionalmente, unifique Taa Marbuta → Heh
- Usado para pré-processamento de consulta
Tabela de Mapeamento Abrangente
| Fonte | Destino | Unicode | Notas |
|---|---|---|---|
ك |
ک |
U+0643 → U+06A9 | Kaf árabe → Kaf persa |
ي |
ی |
U+064A → U+06CC | Ya árabe → Ya persa |
ى |
ی |
U+0649 → U+06CC | Variante Ya final |
أ, إ, ٱ |
ا |
Vários → U+0627 | Formas de Hamza → Alef |
ؤ |
و |
U+0624 → U+0648 | Hamza Waw |
ئ |
ی |
U+0626 → U+06CC | Hamza Ya |
ء |
— | U+0621 | Remover ou preservar (configurável) |
ة |
ه |
U+0629 → U+0647 | Taa Marbuta → Heh (opcional) |
ۀ |
هٔ |
U+06C0 ↔ U+0647+U+0654 | Normalizar forma composta |
ڭ |
گ |
U+06AD → U+06AF | Variantes regionais |
|
— | U+200C | ZWNJ: preservar em Conservador, remover em Permissivo |
٤, ۴ |
4 |
U+0664, U+06F4 → ASCII | Normalizar dígitos |
| Diacríticos | — | U+064B–U+0652 | Remover todos os harakat |
| ZWJ | — | U+200D | Remover conectores invisíveis |
| Múltiplos espaços | Espaço único | — | Normalizar espaçamento |
Snippet de Mapeamento Rápido (para SQL ou Ruby)
ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (removido em permissivo)
Harakat (U+064B..U+0652) → removido
ZWJ (U+200D) → removido
Implementação em PostgreSQL
- Crie uma tabela
text_normalization_map - Use
regexp_replaceou cadeiasTRANSLATEpara desempenho - Opcionalmente, implemente em PL/Python ou PL/v8 para suporte a Unicode
- Normalize o conteúdo armazenado e as consultas recebidas usando a mesma lógica
Estratégia de Indexação
- Armazene
normalized_conservativepara indexação canônica - Normalize consultas com
normalize_persian_arabic(query, 'permissive') - Se usar pesquisa permissiva, o índice deve corresponder ao mesmo perfil
- Opcionalmente, armazene ambas as versões para comparação cruzada
Exemplo de Hash Ruby (para Discourse)
NORMALIZATION_MAP = {
"ك" => "ک",
"ي" => "ی",
"ى" => "ی",
"أ" => "ا",
"إ" => "ا",
"ٱ" => "ا",
"ؤ" => "و",
"ئ" => "ی",
"ة" => "ه",
"ۀ" => "هٔ",
"۴" => "4",
"٤" => "4",
"\u200C" => "", # ZWNJ
"\u200D" => "", # ZWJ
}
Desempenho e Notas Práticas
- Aplique NFKC na camada de aplicação (por exemplo, Ruby
unicode_normalize(:nfkc)) - Use índices separados para perfis conservador vs. permissivo
- Evite mapeamentos forçados de caracteres semanticamente sensíveis (por exemplo, Hamza, Taa Marbuta) a menos que explicitamente configurado
- Execute testes A/B em dados reais do fórum para medir a taxa de acertos e falsos positivos
- Documente cada mapeamento com justificativa e exemplos
- Defina testes de unidade em Ruby e SQL para cada mapeamento
Recomendação Final
- Use Unicode NFKC como base
- Estenda-o com uma camada de mapeamento personalizada
- Mantenha perfis duplos para armazenamento e pesquisa
- Implemente a normalização nas camadas de aplicativo e banco de dados
- Documente e teste cada mapeamento
- Crie índices apropriados (
GIN + to_tsvector) em colunas normalizadas