Я не технический эксперт, но я изучал эту проблему, так как хочу убедиться, что ни один поисковый запрос не будет упущен на двуязычном форуме на персидском и арабском языках, работающем на Discourse. Поскольку Discourse использует PostgreSQL, нормализация становится критически важной: пользователь может вводить запрос на персидском языке, в то время как то же самое слово хранится на арабском, или наоборот. Без правильной нормализации поиск не сработает.
Судя по моим исследованиям, использование нормализации Unicode NFKC — это надежная отправная точка: она обрабатывает многие случаи совместимости, такие как лигатуры, формы представления и арабские/персидские цифры.
Однако для персидского и арабского текста одного NFKC недостаточно. Он не нормализует несколько критических вариантов символов, которые визуально и семантически эквивалентны, но различаются на бинарном уровне.
Ниже я излагаю процедуры и выводы, к которым пришел в ходе своих исследований и экспериментов.
Общая стратегия проектирования
- Сначала применить нормализацию Unicode NFKC для обработки лигатур, форм представления и унификации цифр.
- Затем применить пользовательские маппинги символов в определенном порядке (например, нормализовать варианты хамзы перед арабской Ya).
- Разделить политики нормализации для хранения и поиска:
- Использовать консервативный профиль для канонического хранения (сохранять ZWNJ, избегать семантических сдвигов).
- Использовать разрешительный профиль для поиска (игнорировать ZWNJ, унифицировать варианты хамзы, нормализовать цифры).
- Все маппинги должны быть настраиваемыми через централизованную таблицу маппингов в базе данных или хэш Ruby в приложении.
Профили нормализации
Консервативный (для хранения)
- Минимальное преобразование
- Применение NFKC
- Нормализация арабского Kaf/Ya до персидских эквивалентов
- Удаление диакритических знаков
- Сохранение ZWNJ
- Хранение как
original_text+normalized_conservative
Разрешительный (для поиска)
- Агрессивное совпадение
- Применение всех правил консервативного профиля
- Удаление/игнорирование ZWNJ
- Нормализация вариантов хамзы до базовых букв
- Преобразование всех цифр в ASCII
- Опциональная унификация Taa Marbuta → Heh
- Используется для предварительной обработки запросов
Полная таблица маппингов
| Источник | Цель | 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 | Удалить невидимые соединители |
| Несколько пробелов | Один пробел | — | Нормализация пробелов |
Быстрый фрагмент маппинга (для SQL или Ruby)
ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (удаляется в разрешительном режиме)
Харака (U+064B..U+0652) → удаляются
ZWJ (U+200D) → удаляются
Реализация в PostgreSQL
- Создать таблицу
text_normalization_map - Использовать цепочки
regexp_replaceилиTRANSLATEдля производительности - Опционально реализовать на PL/Python или PL/v8 для поддержки Unicode
- Нормализовать как сохраненный контент, так и входящие запросы, используя одну и ту же логику
Стратегия индексации
- Хранить
normalized_conservativeдля канонической индексации - Нормализовать запросы с помощью
normalize_persian_arabic(query, 'permissive') - При использовании разрешительного поиска индекс должен соответствовать тому же профилю
- Опционально хранить обе версии для кросс-сравнения
Пример хэша Ruby (для Discourse)
NORMALIZATION_MAP = {
"ك" => "ک",
"ي" => "ی",
"ى" => "ی",
"أ" => "ا",
"إ" => "ا",
"ٱ" => "ا",
"ؤ" => "و",
"ئ" => "ی",
"ة" => "ه",
"ۀ" => "هٔ",
"۴" => "4",
"٤" => "4",
"\u200C" => "", # ZWNJ
"\u200D" => "", # ZWJ
}
Заметки о производительности и практике
- Применять NFKC на уровне приложения (например, Ruby
unicode_normalize(:nfkc)) - Использовать отдельные индексы для консервативного и разрешительного профилей
- Избегать принудительных маппингов семантически чувствительных символов (например, хамза, Taa Marbuta), если это явно не настроено
- Проводить A/B-тесты на реальных данных форума для оценки процента попаданий и ложных срабатываний
- Документировать каждое маппинг с обоснованием и примерами
- Определить юнит-тесты на Ruby и SQL для каждого маппинга
Итоговая рекомендация
- Использовать Unicode NFKC как основу
- Расширить его пользовательским слоем маппингов
- Поддерживать два профиля для хранения и поиска
- Реализовать нормализацию как на уровне приложения, так и на уровне базы данных
- Документировать и тестировать каждое маппинг
- Создать соответствующие индексы (
GIN + to_tsvector) на нормализованных колонках