«Носки» и «гольфы» — один товар. Elasticsearch об этом не знал
После трёх недель настройки Elasticsearch — шарды, реплики, mapping, кастомный токенайзер — мы разобрались с нечёткостью: добавили fuzziness AUTO и zero-results упали с 22% до 11%. Хорошо. Но 11% покупателей по-прежнему уходили ни с чем. Я полез в логи и обнаружил: большинство из этих запросов не были опечатками. Это были синонимы.
«Кабель usb-c» не матчил «зарядку». «Мышка» не находила «манипулятор». «Гречка» и «крупа гречневая» — технически один товар, но для индекса два разных пространства. Это не решает ни fuzzy, ни морфология. Нужен словарь.
Четыре класса синонимов, которые разрушают поиск
У нас каталог 28 тысяч SKU на Bitrix. Я вытащил 340 самых частых zero-results запросов за неделю и разбил их по категориям.
Первая группа — торговые марки в разговорном написании: «айфон» вместо iPhone, «самсунк» вместо Samsung, «ксяоми» вместо Xiaomi. Покупатели пишут так, как привыкли говорить.
Вторая — сокращения и технические варианты: «usb-c», «type-c», «bt», «sd». Один физический разъём, десяток написаний.
Третья — функциональные синонимы: «зарядное» и «зарядка», «наушники» и «гарнитура», «принтер» и «МФУ». Не совсем одно и то же, но покупатель обычно имеет в виду одно из них.
Четвёртая — профессиональный язык против разговорного: «тумба под телевизор» и «ТВ-тумба», «стул офисный» и «кресло для компьютера», «носки» и «гольфы». Именно последняя пара дала мне название для этой статьи.
Из 340 запросов мы собрали 180 синонимических пар — это примерно 4 часа работы двух человек с заготовкой из логов.
Откуда берётся словарь
Самая большая ошибка — пытаться составить словарь «с головы». Ни один разработчик не знает, как именно покупатель формулирует запрос в 2 часа ночи с телефона.
Правильный источник: _msearch logs или кастомное логирование через PHP-обёртку. Конкретно — запросы с hits.total.value == 0.
GET /logs-search-*/_search
{
"query": {
"term": { "hits_count": 0 }
},
"aggs": {
"top_queries": {
"terms": { "field": "query.keyword", "size": 500 }
}
}
}
Экспортируешь в CSV, сортируешь по частоте, руками проставляешь синоним. Не нужно 10 000 строк. У нас 180 пар дали ощутимый результат уже через неделю.
Хранить словарь я рекомендую в файле на сервере — не в коде и не в базе. Elasticsearch умеет обновлять synonym_graph без перезапуска индекса через update_settings + /_close / /_open.
Expand vs. contraction: какую стратегию выбрать
Это выбор, который многие пропускают, потому что в документации он описан технически, но без бизнес-контекста.
Expand (носки, гольфы, чулки => носки, гольфы, чулки): каждый термин маппируется на все остальные взаимно. Запрос «носки» найдёт и «гольфы», и «чулки». Это увеличивает recall, но снижает точность.
Contraction (носки, гольфы, чулки => колготочно-носочные изделия): все варианты маппируются на канонический термин. Recall хуже, зато релевантность выше — покупатель не получит нерелевантные товары.
Мы используем гибрид: expand — для брендов и технических вариантов («usb-c, type-c, type c»), contraction — для функциональных синонимов, где мы хотим управлять результатами.
Важное ограничение: synonym_graph работает корректно только при анализе в момент поиска (search_analyzer), не индексации. Если включить в index_time, получишь непредсказуемое поведение с составными токенами.
Порядок в цепочке анализаторов
Здесь несколько месяцев назад я потратил полдня на отладку. Морфология и синонимы — оба работают с токенами, но взаимодействуют неочевидно.
Для русского e-commerce правильная цепочка в search_analyzer:
"filter": [
"lowercase",
"russian_morphology",
"synonym_graph_filter"
]
Морфология идёт до синонимов, потому что она приводит словоформы к основе. Если перевернуть — словарь перестаёт срабатывать на запросах с падежами и числом. «Гречки» не совпадёт с «гречка», потому что до морфологии они разные токены, а синонимы уже отработали.
Ещё один нюанс: для search_analyzer используй synonym_graph (не synonym). Первый корректно обрабатывает мультиграммы — «usb type c» как фразу, а не три отдельных токена.
Что измерить
Три метрики, которые дают сигнал сразу.
Zero-results rate: сколько поисковых сессий закончились ничем. У нас было 11% после fuzzy, после синонимов стало 6%.
Search conversion: доля сессий с поиском, завершившихся покупкой. Считаем цепочку search → product_view → order. У нас: 8.3% → 9.4%.
Exit rate после поиска: доля тех, кто ушёл сразу после пустой выдачи. Должна падать.
Снимай эти метрики за 2 недели до и 2 недели после. Каталог живёт, результаты будут флуктуировать — нужен горизонт.
Что не делать
Самая частая ошибка после первого успеха — расширять словарь слишком агрессивно.
«Гречка» и «рис» — оба крупы, но не синонимы. Мы добавили несколько таких пар на волне энтузиазма и получили жалобы: «ищу гречку, показываете рис». Покупатель — сложный человек. Он знает разницу.
Простое правило проверки: перед добавлением пары задай вопрос — «можно ли купить X вместо Y в 90% случаев?». Если нет — не синоним, а просто похожая категория. Такое лучше решать через фасеты, а не через поисковый индекс.
Ещё одно: не синонимизируй бренды с конкурентами. «Lenovo» и «HP» — не синонимы, даже если покупатель иногда путается. Это путь к судебным рискам и недовольным пользователям.
Итог
Elasticsearch хорошо ищет то, что вы ему показали. Проблема в том, что покупатели говорят не на языке каталога. 180 строк CSV из zero-results логов — за несколько часов работы — дали нам больше поисковой конверсии, чем три недели тюнинга шардов. Потому что это, по сути, переводчик между языком покупателя и языком вашего индекса.
Начните с zero-results лога, возьмите топ-300 запросов за неделю и руками разберите первые 100. Это займёт меньше дня. Результаты увидите через две недели в аналитике.
*Технический контекст: как Elasticsearch стал UX-инструментом, что рассказывают zero-results логи, fuzzy-поиск для русского e-commerce.*