Normalização de Pesquisa em Árabe: Falta Suporte para Variantes de Hamza, Formas de Ya/Kaf e Equivalência Ortográfica

Olá equipe do Discourse,

Estamos executando um fórum multilíngue com conteúdo significativo em árabe e persa e encontramos uma limitação crítica na funcionalidade de pesquisa relacionada à normalização ortográfica árabe.

:magnifying_glass_tilted_left: Descrição do Problema

O script árabe inclui múltiplas representações Unicode para caracteres semanticamente idênticos. Infelizmente, o mecanismo de busca atual do Discourse parece tratar essas variantes como distintas, o que leva a resultados de pesquisa incompletos ou enganosos.

Exemplos:

  • Pesquisar por إطلاق مقامي retorna apenas correspondências exatas, enquanto postagens contendo اطلاق مقامي, أطلاق مقامي ou إطلاق‌مقامي são excluídas.
  • Da mesma forma, pesquisar por ي (U+064A) não corresponde a ی (U+06CC), e ك (U+0643) falha em corresponder a ک (U+06A9), apesar de sua equivalência funcional em contextos árabe/persa.

Isso afeta não apenas as variantes de hamza (أ, إ, ء, ؤ, ئ), mas também substituições comuns como:

Caractere Unicode Normalização Sugerida
أ, إ, ء, آ U+0623, U+0625, U+0621, U+0622 Normalizar para ا
ؤ U+0624 Normalizar para و
ئ U+0626 Normalizar para ي
ى U+0649 Normalizar para ي
ة U+0629 Normalizar para ه
ي vs ی U+064A vs U+06CC Normalizar para ی
ك vs ک U+0643 vs U+06A9 Normalizar para ک

Esse problema é agravado quando os usuários omitem diacríticos ou usam diferentes layouts de teclado, resultando em comportamento de pesquisa fragmentado.


:gear: Solução Proposta

Recomendamos a implementação de uma camada de normalização ciente de Unicode durante a indexação e a análise de consultas. Isso pode ser alcançado por meio de:

  1. Pré-processamento do conteúdo indexado e das consultas do usuário para unificar as variantes de caracteres.
  2. Aplicação de regras de normalização semelhantes às usadas em bibliotecas de Processamento de Linguagem Natural (PLN) árabes ou mecanismos de busca (por exemplo, Farasa, Hazm ou mapeadores personalizados baseados em regex).
  3. Opcionalmente, suporte à correspondência aproximada ou distância de Levenshtein para correspondências quase exatas.

Aqui está um exemplo simplificado de uma função de normalização (estilo Java):

public static String normalizeArabic(String text) {
  return text.replace("أ", "ا")
             .replace("إ", "ا")
             .replace("آ", "ا")
             .replace("ؤ", "و")
             .replace("ئ", "ي")
             .replace("ى", "ي")
             .replace("ة", "ه")
             .replace("ي", "ی")
             .replace("ك", "ک");
}

:folded_hands: Solicitação

Essa normalização poderia ser considerada para inclusão no mecanismo de busca principal ou como um plugin? Isso melhoraria significativamente a usabilidade para as comunidades de língua árabe e persa que usam o Discourse.

Se houver uma solução alternativa ou plugin existente que resolva isso, agradeceríamos qualquer orientação.

Obrigado pelo seu tempo e por construir uma plataforma tão poderosa.

Atenciosamente

1 curtida

A configuração do site search_ignore_accents tem algum efeito sobre este problema?

1 curtida

Muito obrigado por participar e contribuir para a discussão

Para responder à sua pergunta: sim, a configuração search_ignore_accents está habilitada em nosso fórum.
Infelizmente, isso não resolve o problema que estamos enfrentando. Os resultados da pesquisa ainda não correspondem a caracteres árabes e persas ortograficamente equivalentes, então o problema persiste apesar dessa configuração.

1 curtida

Acho que este é um pedido razoável, pois melhoraria muito a experiência de busca para sites em árabe e persa. Adoraríamos revisar um PR que implemente este recurso, então vou adicionar um #pr-welcome a ele.

Para quem decidir trabalhar neste recurso: toda a lógica de normalização deve ser controlada por uma configuração do site que a habilite por padrão para sites em árabe e persa (veja locale_default em site_settings.yml) e todos os outros locais devem ter esta configuração desativada por padrão. O Core já possui lógica de normalização semelhante para caracteres acentuados (veja lib/search.rb), então isso seria uma referência útil ao implementar este recurso.

4 curtidas

Muito obrigado, Osama! Fico muito feliz em ver que esta sugestão foi bem recebida.

2 curtidas

Para esta parte do problema, estamos falando de uma normalização do padrão Unicode NFKC (para citar um)?

(Eu nem tenho certeza do que fazemos… presumo que normalizamos o texto das postagens no pipeline de processamento?)

1 curtida

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.


:wrench: Estratégia Geral de Design

  1. Aplique primeiro a normalização NFKC do Unicode para lidar com ligaturas, formas de apresentação e unificação de dígitos.
  2. Em seguida, aplique mapeamentos de caracteres personalizados em uma ordem definida (por exemplo, normalize variantes de Hamza antes de Ya árabe).
  3. 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).
  4. 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.

:one: Perfis de Normalização

:green_circle: 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

:blue_circle: 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

:two: 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

:three: 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

:four: Implementação em PostgreSQL

  • Crie uma tabela text_normalization_map
  • Use regexp_replace ou cadeias TRANSLATE para 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_conservative para 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

:five: Exemplo de Hash Ruby (para Discourse)

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

:six: Desempenho e Notas Práticas

  1. Aplique NFKC na camada de aplicação (por exemplo, Ruby unicode_normalize(:nfkc))
  2. Use índices separados para perfis conservador vs. permissivo
  3. Evite mapeamentos forçados de caracteres semanticamente sensíveis (por exemplo, Hamza, Taa Marbuta) a menos que explicitamente configurado
  4. Execute testes A/B em dados reais do fórum para medir a taxa de acertos e falsos positivos
  5. Documente cada mapeamento com justificativa e exemplos
  6. Defina testes de unidade em Ruby e SQL para cada mapeamento

:seven: 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