Нормализация поиска на арабском: отсутствует поддержка вариантов хамзы, форм я/каф и орфографической эквивалентности

Привет, команда Discourse,

Мы управляем многоязычным форумом с значительным количеством контента на арабском и персидском языках и столкнулись с критическим ограничением в функциональности поиска, связанным с орфографической нормализацией арабского языка.

:magnifying_glass_tilted_left: Описание проблемы

Арабский алфавит включает несколько представлений Unicode для семантически идентичных символов. К сожалению, текущий поисковый движок Discourse, по-видимому, рассматривает эти варианты как различные, что приводит к неполным или вводящим в заблуждение результатам поиска.

Примеры:

  • Поиск по запросу إطلاق مقامي возвращает только точные совпадения, тогда как сообщения, содержащие اطلاق مقامي, أطلاق مقامي или إطلاق‌مقامي, исключаются.
  • Аналогично, поиск по символу ي (U+064A) не находит ی (U+06CC), а ك (U+0643) не совпадает с ک (U+06A9), несмотря на их функциональную эквивалентность в арабском и персидском контекстах.

Это касается не только вариантов хамзы (أ, إ, ء, ؤ, ئ), но и распространенных замен, таких как:

Символ Unicode Предлагаемая нормализация
أ, إ, ء, آ U+0623, U+0625, U+0621, U+0622 Нормализовать до ا
ؤ U+0624 Нормализовать до و
ئ U+0626 Нормализовать до ي
ى U+0649 Нормализовать до ي
ة U+0629 Нормализовать до ه
ي vs ی U+064A vs U+06CC Нормализовать до ی
ك vs ک U+0643 vs U+06A9 Нормализовать до ک

Проблема усугубляется, когда пользователи опускают диакритические знаки или используют различные раскладки клавиатуры, что приводит к фрагментированному поведению поиска.


:gear: Предлагаемое решение

Мы рекомендуем внедрить слой нормализации, учитывающий Unicode, как при индексации, так и при анализе запросов. Это можно реализовать следующим образом:

  1. Предварительная обработка как индексированного контента, так и запросов пользователей для унификации вариантов символов.
  2. Применение правил нормализации, аналогичных тем, что используются в библиотеках NLP для арабского языка или поисковых системах (например, Farasa, Hazm или пользовательские мапперы на основе регулярных выражений).
  3. По желанию, поддержка нечеткого поиска или расстояния Левенштейна для приближенных совпадений.

Вот упрощенный пример функции нормализации (в стиле Java):

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

:folded_hands: Запрос

Можно ли рассмотреть возможность включения этой нормализации в основной поисковый движок или в качестве плагина? Это значительно улучшит удобство использования для арабских и персидских сообществ, использующих Discourse.

Если существует существующее решение или плагин, решающий эту проблему, мы будем признательны за любые рекомендации.

Спасибо за ваше время и за создание такой мощной платформы.

С уважением

1 лайк

Влияет ли настройка сайта search_ignore_accents на эту проблему?

1 лайк

Большое спасибо за то, что присоединились к обсуждению и внесли свой вклад.

Отвечая на ваш вопрос: да, настройка search_ignore_accents включена на нашем форуме. К сожалению, это не решает нашу проблему. Результаты поиска по-прежнему не находят орфографически эквивалентные арабские и персидские символы, поэтому проблема сохраняется, несмотря на эту настройку.

1 лайк

Я считаю, что это разумная просьба, так как она значительно улучшит поиск на арабских и персидских сайтах. Мы с радостью рассмотрим PR с реализацией этой функции, поэтому я помечу его тегом pr-welcome.

Для тех, кто решит поработать над этой функцией: вся логика нормализации должна быть скрыта за настройкой сайта, которая по умолчанию включена для арабских и персидских сайтов (см. locale_default в site_settings.yml), а для всех остальных локалей эта настройка должна быть выключена по умолчанию. В ядре уже есть аналогичная логика нормализации для акцентированных символов (см. lib/search.rb), поэтому это будет полезным ориентиром при реализации данной функции.

4 лайка

Огромное спасибо, Осам! Я очень рад, что это предложение было принято с таким энтузиазмом.

2 лайка

Говоря об этой части проблемы, мы имеем в виду нормализацию по стандарту Unicode NFKC (выбирая один из вариантов)?

(Я даже не уверен, что именно мы делаем… Предполагаю, что мы нормализуем текст постов в конвейере обработки данных?)

1 лайк

Я не технический эксперт, но я изучал эту проблему, так как хочу убедиться, что ни один поисковый запрос не будет упущен на двуязычном форуме на персидском и арабском языках, работающем на Discourse. Поскольку Discourse использует PostgreSQL, нормализация становится критически важной: пользователь может вводить запрос на персидском языке, в то время как то же самое слово хранится на арабском, или наоборот. Без правильной нормализации поиск не сработает.

Судя по моим исследованиям, использование нормализации Unicode NFKC — это надежная отправная точка: она обрабатывает многие случаи совместимости, такие как лигатуры, формы представления и арабские/персидские цифры.

Однако для персидского и арабского текста одного NFKC недостаточно. Он не нормализует несколько критических вариантов символов, которые визуально и семантически эквивалентны, но различаются на бинарном уровне.

Ниже я излагаю процедуры и выводы, к которым пришел в ходе своих исследований и экспериментов.


:wrench: Общая стратегия проектирования

  1. Сначала применить нормализацию Unicode NFKC для обработки лигатур, форм представления и унификации цифр.
  2. Затем применить пользовательские маппинги символов в определенном порядке (например, нормализовать варианты хамзы перед арабской Ya).
  3. Разделить политики нормализации для хранения и поиска:
    • Использовать консервативный профиль для канонического хранения (сохранять ZWNJ, избегать семантических сдвигов).
    • Использовать разрешительный профиль для поиска (игнорировать ZWNJ, унифицировать варианты хамзы, нормализовать цифры).
  4. Все маппинги должны быть настраиваемыми через централизованную таблицу маппингов в базе данных или хэш Ruby в приложении.

:one: Профили нормализации

:green_circle: Консервативный (для хранения)

  • Минимальное преобразование
  • Применение NFKC
  • Нормализация арабского Kaf/Ya до персидских эквивалентов
  • Удаление диакритических знаков
  • Сохранение ZWNJ
  • Хранение как original_text + normalized_conservative

:blue_circle: Разрешительный (для поиска)

  • Агрессивное совпадение
  • Применение всех правил консервативного профиля
  • Удаление/игнорирование ZWNJ
  • Нормализация вариантов хамзы до базовых букв
  • Преобразование всех цифр в ASCII
  • Опциональная унификация Taa Marbuta → Heh
  • Используется для предварительной обработки запросов

:two: Полная таблица маппингов

Источник Цель Unicode Примечания
ك ک U+0643 → U+06A9 Арабский Kaf → Персидский Kaf
ي ی U+064A → U+06CC Арабский Ya → Персидский Ya
ى ی U+0649 → U+06CC Вариант конечной Ya
أ, إ, ٱ ا Различные → U+0627 Формы хамзы → Алеф
ؤ و U+0624 → U+0648 Хамза Waw
ئ ی U+0626 → U+06CC Хамза Ya
ء U+0621 Удалить или сохранить (настраивается)
ة ه U+0629 → U+0647 Taa Marbuta → Heh (опционально)
ۀ هٔ U+06C0 ↔ U+0647+U+0654 Нормализовать составную форму
ڭ گ U+06AD → U+06AF Региональные варианты
U+200C ZWNJ: сохранять в консервативном, удалять в разрешительном
٤, ۴ 4 U+0664, U+06F4 → ASCII Нормализация цифр
Диакритика U+064B–U+0652 Удалить все харака
ZWJ U+200D Удалить невидимые соединители
Несколько пробелов Один пробел Нормализация пробелов

:three: Быстрый фрагмент маппинга (для SQL или Ruby)

ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (удаляется в разрешительном режиме)
Харака (U+064B..U+0652) → удаляются
ZWJ (U+200D) → удаляются

:four: Реализация в PostgreSQL

  • Создать таблицу text_normalization_map
  • Использовать цепочки regexp_replace или TRANSLATE для производительности
  • Опционально реализовать на PL/Python или PL/v8 для поддержки Unicode
  • Нормализовать как сохраненный контент, так и входящие запросы, используя одну и ту же логику

Стратегия индексации

  • Хранить normalized_conservative для канонической индексации
  • Нормализовать запросы с помощью normalize_persian_arabic(query, 'permissive')
  • При использовании разрешительного поиска индекс должен соответствовать тому же профилю
  • Опционально хранить обе версии для кросс-сравнения

:five: Пример хэша Ruby (для Discourse)

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

:six: Заметки о производительности и практике

  1. Применять NFKC на уровне приложения (например, Ruby unicode_normalize(:nfkc))
  2. Использовать отдельные индексы для консервативного и разрешительного профилей
  3. Избегать принудительных маппингов семантически чувствительных символов (например, хамза, Taa Marbuta), если это явно не настроено
  4. Проводить A/B-тесты на реальных данных форума для оценки процента попаданий и ложных срабатываний
  5. Документировать каждое маппинг с обоснованием и примерами
  6. Определить юнит-тесты на Ruby и SQL для каждого маппинга

:seven: Итоговая рекомендация

  • Использовать Unicode NFKC как основу
  • Расширить его пользовательским слоем маппингов
  • Поддерживать два профиля для хранения и поиска
  • Реализовать нормализацию как на уровне приложения, так и на уровне базы данных
  • Документировать и тестировать каждое маппинг
  • Создать соответствующие индексы (GIN + to_tsvector) на нормализованных колонках