Normalización de búsqueda en árabe: Falta compatibilidad con variantes de Hamza, formas de Ya/Kaf y equivalencia ortográfica

Hola equipo de Discourse,

Estamos ejecutando un foro multilingüe con un contenido significativo en árabe y persa, y hemos encontrado una limitación crítica en la funcionalidad de búsqueda relacionada con la normalización ortográfica del árabe.

:magnifying_glass_tilted_left: Descripción del problema

El script árabe incluye múltiples representaciones Unicode para caracteres semánticamente idénticos. Desafortunadamente, el motor de búsqueda actual de Discourse parece tratar estas variantes como distintas, lo que lleva a resultados de búsqueda incompletos o engañosos.

Ejemplos:

  • Buscar إطلاق مقامي solo devuelve coincidencias exactas, mientras que las publicaciones que contienen اطلاق مقامي, أطلاق مقامي o إطلاق‌مقامي se excluyen.
  • De manera similar, buscar ي (U+064A) no coincide con ی (U+06CC), y ك (U+0643) no coincide con ک (U+06A9), a pesar de su equivalencia funcional en contextos árabes/persas.

Esto afecta no solo a las variantes de hamza (أ, إ, ء, ؤ, ئ) sino también a sustituciones comunes como:

Carácter Unicode Normalización Sugerida
أ, إ, ء, آ U+0623, U+0625, U+0621, U+0622 Normalizar a ا
ؤ U+0624 Normalizar a و
ئ U+0626 Normalizar a ي
ى U+0649 Normalizar a ي
ة U+0629 Normalizar a ه
ي vs ی U+064A vs U+06CC Normalizar a ی
ك vs ک U+0643 vs U+06A9 Normalizar a ک

Este problema se agrava cuando los usuarios omiten los diacríticos o utilizan diferentes diseños de teclado, lo que resulta en un comportamiento de búsqueda fragmentado.


:gear: Solución Propuesta

Recomendamos implementar una capa de normalización consciente de Unicode durante la indexación y el análisis de consultas. Esto se puede lograr mediante:

  1. Preprocesamiento del contenido indexado y las consultas de usuario para unificar las variantes de caracteres.
  2. Aplicación de reglas de normalización similares a las utilizadas en bibliotecas de PNL árabes o motores de búsqueda (por ejemplo, Farasa, Hazm o mapeadores personalizados basados en expresiones regulares).
  3. Opcionalmente, soporte de coincidencia difusa o distancia de Levenshtein para coincidencias casi exactas.

Aquí hay un ejemplo simplificado de una función de normalización (estilo Java):

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

:folded_hands: Solicitud

¿Se podría considerar la inclusión de esta normalización en el motor de búsqueda principal o como un plugin? Mejoraría significativamente la usabilidad para las comunidades de habla árabe y persa que utilizan Discourse.

Si existe una solución alternativa o un plugin que aborde esto, agradeceríamos cualquier orientación.

Gracias por su tiempo y por construir una plataforma tan potente.

Saludos cordiales

1 me gusta

¿Tiene la configuración del sitio search_ignore_accents algún efecto en este problema?

1 me gusta

Muchas gracias por participar y contribuir a la discusión.

Para responder a tu pregunta: sí, la configuración search_ignore_accents está habilitada en nuestro foro.
Desafortunadamente, no resuelve el problema que estamos enfrentando. Los resultados de búsqueda aún no coinciden con caracteres árabes y persas ortográficamente equivalentes, por lo que el problema persiste a pesar de esa configuración.

1 me gusta

Creo que esta es una solicitud razonable, ya que mejoraría enormemente la experiencia de búsqueda para los sitios en árabe y persa. Nos encantaría revisar una PR que implemente esta función, así que le pondré un pr-welcome.

Para cualquiera que decida trabajar en esta función: toda la lógica de normalización debe estar controlada por una configuración del sitio que la habilite por defecto para los sitios en árabe y persa (ver locale_default en site_settings.yml) y todas las demás locales deben tener esta configuración desactivada por defecto. Core ya tiene una lógica de normalización similar para caracteres acentuados (ver lib/search.rb), por lo que sería una referencia útil al implementar esta función.

4 Me gusta

¡Muchas gracias, Osama! Me alegra mucho ver que esta sugerencia fue bien recibida.

2 Me gusta

Para esta parte del problema, ¿estamos hablando de una normalización del estándar Unicode NFKC (por poner un ejemplo)?

(Ni siquiera estoy seguro de lo que hacemos… ¿Supongo que normalizamos el texto de las publicaciones en el proceso de cocción?)

1 me gusta

No soy un experto técnico, pero he estado investigando este problema porque quiero asegurarme de que no se pierdan consultas de búsqueda en un foro bilingüe persa-árabe de Discourse. Dado que Discourse utiliza PostgreSQL, la normalización se vuelve esencial: un usuario podría buscar usando caracteres persas, mientras que la misma palabra se almacena usando caracteres árabes, o viceversa. Sin una normalización adecuada, la búsqueda fallará.
Según lo que he aprendido, usar la normalización NFKC de Unicode es un buen punto de partida: maneja muchos casos de compatibilidad como ligaduras, formas de presentación y dígitos árabes/persas.

Sin embargo, para el texto persa y árabe, NFKC por sí solo no es suficiente. No normaliza varias variantes de caracteres críticas que son visual y semánticamente equivalentes pero difieren a nivel binario.

A continuación, describo los procedimientos y las ideas a las que he llegado a través de mi investigación y exploración.


:wrench: Estrategia General de Diseño

  1. Aplicar primero la normalización NFKC de Unicode para manejar ligaduras, formas de presentación y unificación de dígitos.
  2. Luego aplicar mapeos de caracteres personalizados en un orden definido (por ejemplo, normalizar variantes de Hamza antes que Ya árabe).
  3. Separar las políticas de normalización para almacenamiento vs. búsqueda:
    • Usar un perfil Conservador para el almacenamiento canónico (preservar ZWNJ, evitar cambios semánticos).
    • Usar un perfil Permisivo para la búsqueda (ignorar ZWNJ, unificar variantes de Hamza, normalizar dígitos).
  4. Todos los mapeos deben ser configurables a través de una tabla de mapeo centralizada en la base de datos o un hash de Ruby en la aplicación.

:one: Perfiles de Normalización

:green_circle: Conservador (para almacenamiento)

  • Transformación mínima
  • Aplicar NFKC
  • Normalizar Kaf/Ya árabe a equivalentes persas
  • Eliminar diacríticos
  • Preservar ZWNJ
  • Almacenar como texto_original + normalizado_conservador

:blue_circle: Permisivo (para búsqueda)

  • Coincidencia agresiva
  • Aplicar todas las reglas Conservadoras
  • Eliminar/ignorar ZWNJ
  • Normalizar variantes de Hamza a letras base
  • Convertir todos los dígitos a ASCII
  • Opcionalmente unificar Taa Marbuta → Heh
  • Usado para preprocesamiento de consultas

:two: Tabla de Mapeo Integral

Fuente Destino Unicode Notas
ك ک U+0643 → U+06A9 Kaf árabe → Kaf persa
ي ی U+064A → U+06CC Ya árabe → Ya persa
ى ی U+0649 → U+06CC Variante final de Ya
أ, إ, ٱ ا Varios → U+0627 Formas de Hamza → Alef
ؤ و U+0624 → U+0648 Hamza Waw
ئ ی U+0626 → U+06CC Hamza Ya
ء U+0621 Eliminar o preservar (configurable)
ة ه U+0629 → U+0647 Taa Marbuta → Heh (opcional)
ۀ هٔ U+06C0 ↔ U+0647+U+0654 Normalizar forma compuesta
ڭ گ U+06AD → U+06AF Variantes regionales
U+200C ZWNJ: preservar en Conservador, eliminar en Permisivo
٤, ۴ 4 U+0664, U+06F4 → ASCII Normalizar dígitos
Diacríticos U+064B–U+0652 Eliminar todos los harakat
ZWJ U+200D Eliminar uniones invisibles
Espacios múltiples Espacio simple Normalizar espaciado

:three: Fragmento de Mapeo Rápido (para SQL o Ruby)

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

:four: Implementación en PostgreSQL

  • Crear una tabla text_normalization_map
  • Usar regexp_replace o cadenas TRANSLATE para rendimiento
  • Opcionalmente implementar en PL/Python o PL/v8 para soporte Unicode
  • Normalizar tanto el contenido almacenado como las consultas entrantes utilizando la misma lógica

Estrategia de Indexación

  • Almacenar normalized_conservative para indexación canónica
  • Normalizar consultas con normalize_persian_arabic(query, 'permissive')
  • Si se usa búsqueda permisiva, el índice debe coincidir con el mismo perfil
  • Opcionalmente almacenar ambas versiones para comparación cruzada

:five: Ejemplo de Hash de Ruby (para Discourse)

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

:six: Notas de Rendimiento y Prácticas

  1. Aplicar NFKC en la capa de aplicación (por ejemplo, unicode_normalize(:nfkc) de Ruby)
  2. Usar índices separados para perfiles conservadores vs. permisivos
  3. Evitar mapeos forzados de caracteres semánticamente sensibles (por ejemplo, Hamza, Taa Marbuta) a menos que se configuren explícitamente
  4. Ejecutar pruebas A/B con datos reales del foro para medir la tasa de aciertos y los falsos positivos
  5. Documentar cada mapeo con justificación y ejemplos
  6. Definir pruebas unitarias tanto en Ruby como en SQL para cada mapeo

:seven: Recomendación Final

  • Usar Unicode NFKC como base
  • Extenderlo con una capa de mapeo personalizada
  • Mantener perfiles duales para almacenamiento y búsqueda
  • Implementar la normalización tanto en la capa de la aplicación como en la de la base de datos
  • Documentar y probar cada mapeo
  • Construir índices apropiados (GIN + to_tsvector) en columnas normalizadas