Назад в блог

«Носки» и «гольфы» — один товар. 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: доля сессий с поиском, завершившихся покупкой. Считаем цепочку searchproduct_vieworder. У нас: 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.*