Как я добавил поиск с устойчивостью к опечаткам с помощью OpenSearch и CJK
Запросы с нулевым результатом убивали наше время просмотра.
В течение года TopVideoHub использовал SQLite FTS5 для поиска. Это работало для точных совпадений, но не справлялось, когда пользователи допускали опечатки.
Люди искали "demon slyer" вместо "demon slayer". Они добавляли лишние пробелы в японских или корейских названиях. Поскольку FTS5 ищет точные токены, одна опечатка означала нулевой результат. Пользователи не пытались искать снова — они просто уходили.
Я перенес поиск по названиям в OpenSearch. Вот как я решил эту задачу для мультиязычной аудитории.
Проблема стандартной нечеткости (fuzziness)
Большинство туториалов советуют использовать "fuzziness" для исправления опечаток. Это работает для английского, но не для китайского, японского или корейского (CJK) языков.
- Расстояние редактирования (edit distance) плохо подходит для CJK. Опечатка в CJK часто означает использование совершенно другого иероглифа.
- Стандартная нечеткость создает семантический мусор. Она может сопоставить "fire" с "water", потому что они находятся на расстоянии одной правки.
- Токенизация затруднена. В языках CJK между словами не используются пробелы.
Решение: мультиполевой подход
Я перестал относиться ко всему тексту одинаково. Я создал одно логическое поле названия, которое индексирует данные тремя разными способами:
- Латиница и ромадзи: Я использовал стандартную токенизацию с включенной нечеткостью (fuzziness). Я установил длину префикса (prefix length) равной 1. Это гарантирует, что "demon" совпадет с "demn", но "lemon" не совпадет с "demon".
- Текст CJK: Я использовал биграммный анализатор CJK и нормализатор ICU. Я ОТКЛЮЧИЛ нечеткость. Вместо этого я использовал порог минимального совпадения (minimum match threshold) в 70%.
- Автодополнение: Я использовал поле
edge-ngramдля реализации функции поиска по мере ввода (search-as-you-type).
Архитектура и безопасность данных
Я оставил SQLite в качестве единого источника истины. OpenSearch выступает в роли быстрого, восстанавливаемого индекса.
- Я использую PHP для массовой отправки обновлений в OpenSearch пачками (bulk batches).
- Я никогда не запускаю индексацию во время пользовательского запроса.
- Я запускаю Python-скрипт для проверки «расхождения» (drift). Это гарантирует, что количество записей в OpenSearch совпадает с количеством в SQLite.
Результаты
Изменения были колоссальными:
- Количество запросов с нулевым результатом упало с 14% до менее 3%.
- Поисковые сессии стали длиннее, так как пользователи сразу находили то, что искали.
- Задержка (latency) остается низкой — около 40 мс.
Если вы работаете с мультиязычной аудиторией, помните: устойчивость к опечаткам и сопоставление CJK — это две разные проблемы. Вам нужны два разных решения.
