Привет! Меня зовут Александр Баранов, я аналитик данных в команде поиска Купера. Цель этого рассказа, поделиться наработками в деле оптимизации разметки текстовых данных при помощи большой языковой модели (LLM). Если после прочтения вы захотите что-то добавить или спросить, буду только рад!

Как устроен поиск товаров
Важность качества данных в ML-задачах сложно переоценить, но особенно они критичны для поиска — процесса в котором приходится постоянно сверяться с разметкой и асессорами. Однако, вместо того чтобы страдать над рутиной в одиночестве, ее можно переложить на плечи больших языковых моделей и страдать уже вместе с ними, используя LLM для автоматизации.

Итак, пользователи вводят запрос в строку поиска, и мы обязаны сделать их опыт комфортным. Для этого:
-
исправляем все возможные опечатки;
-
обогащаем запрос синонимами, чтобы учесть разные варианты формулировок;
-
достаем кандидатов из поискового индекса и переранжируем их.
Полученную выборку показываем людям.
По-хорошему, разметка текстовых данных нужна на каждом из этих этапов. Сначала, для исправления опечаток — чтобы оценить, корректно ли система исправила запрос. Затем, для генерации кандидатов — чтобы понять, логично ли расширять запрос именно этими синонимами. А после реранкинга — чтобы убедиться, что мы показываем действительно релевантные товары.
Делать это вручную — неудобно, сложно и дорого. Чтобы разметить данные, надо выгрузить их и отправить на краудсорсинговую платформу. Потом долго ждать, пока асессоры возьмут задачу в работу и сделают ее. Наконец, такие задания требуют немалого бюджета, что тоже не очень приятно.
Вот эти сложности и стали стимулом поискать более быстрый и экономичный способ разметки. С учетом того, что большие языковые модели обладают бесконечными знаниями, способны работать круглосуточно и без выходных, к тому же быстрее и дешевле живых асессоров, нам показалось логичным, попытаться делегировать эту работу (или хотя бы ее часть) им.
Внедрение LLM в поиск
Давайте последовательно пройдем по всем этапам.
Исправление опечаток
Elastic* — полнотекстовая система, это означает, что после лемматизации она находит документы исключительно с такими же токенами. Если пользователь вводит запрос с опечаткой, а все искомые товары содержат только правильное написание, система просто не найдет такой товар.
Скрытый текст
* Elastic — открытая поисковая система (аналитическая платформа на основе Lucene), которая индексирует данные и находит документы (в вашем случае — товары) по введенному запросу.
Чтобы не допустить этого, мы используем опечаточник — базу, содержащую словари исправлений, которые периодически пополняются. Кандидаты для попадания туда майнятся автоматически — система самостоятельно анализирует реальные запросы и находит вероятные опечатки.
Казалось бы, что еще нужно? Тем не менее есть проблема: вместе с правильными исправлениями в словарь может попадать мусор. Например, в паре «колея» и «Корея» — расстояние Левенштейна* довольно низкое, меняется лишь один символ, однако полностью утрачивается смысл. Чтобы контролировать этот момент, мы и применяем большие языковые модели. Они автоматически оценивают, правильно ли сработало исправление.
* Расстояние Левенштейна — метрика, которая показывает, насколько два слова отличаются друг от друга. Точнее — минимальное количество операций (вставка, удаление, замена символа), необходимых, чтобы превратить одно слово в другое.Скрытый текст
Мы тестировали разные модели: Mistral, GigaChat, DeepSeek, их комбинации, например, голосование моделей, сценарии с переоценкой, когда одна модель давала помимо ответа степень уверенности в своем ответе.
Если уверенность была низкой, разметка передавалась другой модели. Кто, как вы думаете, победил? Правильно — победил GigaChat. У него оказался не только самый быстрый инференс, он к тому же прекрасно работал с русским языком, показывая стабильно высокую точность.
Пайплайн разметки исправления опечаток прост. В специально подготовленный промпт мы передаем запрос и исправление. Промпт подробно описывает классы — что считать опечаткой, а что избыточным исправлением, показывает примеры, тонкости работы с корнер-кейсами и пробелами, а также однозначно описывает, как формировать ответ. С промптами для этого сценария и для последующих можете ознакомиться по ссылке.
Синонимы: расширять осторожно
Следующий вызов, после того как мы исправили опечатки — объяснить системе, что «помидор» и «томат» на самом деле обозначают один и тот же объект. Набирая в строке «помидор», пользователь ожидает увидеть в товарной выдаче товары, логично связанные с термином «томат». Но, поскольку (как мы уже зафиксировали выше) Elastic осуществляет исключительно полнотекстовый поиск, «помидор» и «томат» для нее — различные токены, поэтому система не находит и не показывает товары, в названии которых присутствует «томат». Для этого в ней предусмотрен механизм синонимов.
Синонимы бывают Index time (время индексации) и Query time (время запроса):
-
первые применяются на этапе индексации товаров:
-
вторые — непосредственно во время запроса.
Они могут быть записаны через запятую (двусторонние синонимы), тогда токены будут равнозначными и по любому из терминов будут найдены остальные.
Пример: «помидор, томат, помидорка» — все эти термины считаются равнозначными. Если пользователь ищет «помидор», найдутся товары с названием «томат» и «помидорка». И наоборот — ищет «томат», найдутся «помидор» и «помидорка». Связь работает в обе стороны.
Или с направлениями (односторонние синонимы), когда термины расширяются по направленным связям, но не в обратную сторону.
Пример: «помидор => красный овощ». Здесь стрелка показывает направление. Если пользователь ищет «помидор», система его расширит на «красный овощ» и найдет товары с этим описанием. Но если кто-то ищет «красный овощ», система не будет искать «помидор». Связь работает только в одну сторону.
У нас эти словари хранятся в GitLab. Это удобно для версионирования: если очередная партия синонимов вдруг что-то сломает, мы сможем безболезненно откатиться к предыдущей версии.
Как и опечатки, синонимы майнятся автоматически при помощи анализа пользовательских переформулировок. Плюс, их приносит бизнес вместе с оперативными задачами. От мусора — слишком сужающие синонимы, слишком расширяющие или просто логически неверные — мы защищаемся валидацией с участием языковых моделей.
Пайплайн соответствия синонимов аналогичен валидации опечаток, правда, с особенностями. Промпт здесь подготовлен именно для рядов синонимов:
-
подробно описывает, что считать синонимом, а что нет;
-
учитывает корнер-кейсы;
-
содержит инструкцию по обращению с транслитерациями и переводами;
-
знает, в каком варианте предоставить ответ.
После того как ответ от модели получен, мы фильтруем валидные синонимы и отправляем запрос на слияние изменений (merge request) в Git-репозиторий со словарями.
И наконец последний слой валидации — бот в GitLab. Он ищет разницу (diff) в merge request с добавлением синонимов, и найдя ее, отправляет в языковую модель, которая предлагает подсказки-варианты для подозрительных мест. Автор merge request может принять или отклонить их буквально одной кнопкой. Да, помимо дополнительной валидации, наш сценарий предоставляет также генеративную часть — модель может предлож��ть что-то исправить или добавить.
Релевантность: как ESCI упрощает жизнь
В его основе модель CatBoost, которая учитывает как предрассчитанную косинусную близость эмбеддингов запроса и товаров, так и различные конверсии, статистики на уровне магазинов, пользователей и SKU.
Когда в модель вносятся какие-то изменения, мы смотрим на актуальную метрику качества ранжирования в поиске (NDCG) относительно пользовательских сигналов — игноров, кликов, добавлений в корзину, покупок. При этом не забываем мониторить смысловое соответствие.
Для этого товары из поисковой выдачи размечаем на четыре класса:
-
Высокая релевантность (Vital) — товар полностью подходит запросу. Совпали все ключевые характеристики, если они были указаны.
-
Средняя релевантность (Rel Plus) — некоторые характеристики не совпали, но в целом товар подойдет на замену большинству пользователей.
-
Низкая релевантность (Rel Minus) — не совпали какие-то существенные характеристики. На замену товар подойдет лишь небольшой части пользователей.
-
Отсутствие релевантности (Irrelevant) — товар не подходит ни в каком виде. В идеале эти позиции вообще не должны появляться в выдаче при поиске. Если они появляются, это значит, что модель ранжирования работает плохо.

Вроде хорошо, однако подход с такой классификацией несет проблему. Дело в том, что промежуточные классы Rel Plus и Rel Minus достаточно размыты и часто требуют субъективной оценки: системе сложно понять, подойдет ли товар на замену большинству пользователей или небольшой части. Обычно в подобных ситуациях асессоры не приходили к консенсусу, да и большие языковые модели ошибались тоже.
В поисках решения этой проблемы мы пробовали разные подходы, в итоге остановились на ESCI — схеме разметки релевантности результатов поиска, которую обычно используют, чтобы с большей однозначностью оценивать, как товар соотносится с запросом пользователя.
В ESCI четыре класса:
-
Exact — точное совпадение, товар полностью соответствует запросу.
-
Substitute — заменитель, товар не тот же самый, но его можно разумно купить вместо искомого.
-
Complementary — комплиментарный, товар дополняет искомый (аксессуар, расходник, сопутствующее).
-
Irrelevant — нерелевантный, товар к запросу не относится.
Главная идея ESCI в том, что классы разделены по смыслу более четко, чем шкалы типа «почти подходит большинству / меньшинству», поэтому и людям, и LLM проще работать с разметкой. В нашем случае — это помогло. Точность LLM-разметки стала выше: на сбалансированной выборке, где этих классов поровну, она выросла с 0,55 до 0,7, а в промежуточных классах стало меньше ошибок.
Батчи, кеш и контроль качества
Если вы уже посмотрели промпты, то, скорее всего, заметили, что они предлагают батчевую обработку — мы посылаем несколько элементов сразу в одном запросе, вместо того чтобы обрабатывать их по одному.
Пример:
-
Неэффективно (обработка по одному):
text
Запрос 1: "Проверь синоним: помидор → томат"
Запрос 2: "Проверь синоним: яблоко → фрукт"
Запрос 3: "Проверь синоним: кот → животное"
-
Эффективно (батчевая обработка):
text
Один запрос: "Проверь синонимы:
1. помидор → томат
2. яблоко → фрукт
3. кот → животное"
Для нас это важно по нескольким причинам:
-
экономия токенов — инструкция пишется один раз вместо трех;
-
экономия времени — один API-запрос, а не три;
-
та же точность — на тестах выяснилось, что батчевая обработка не снижает точность.
В GigaChat input (вход) и output (выход) тарифицируются одинаково, поэтому много повторяющихся инструкций в каждом запросе было бы транжирством токенов. LLM получает и отдает batch, полученный JSON легко распарсить и извлечь оттуда нужную информацию.
Все сценарии мы собрали в Python-библиотеку, которую в любой момент можно вызывать из кода. Технически это реализовано достаточно элементарно, при этом на гибкости и масштабируемости такая простота никак не отражается.
В основе решения лежит базовый для разметчиков класс LLM Labeler, от которого легко наследуются другие языковые модели. За кеширование отвечают контекстные менеджеры — при инициализации они подгружают кеш с S3, а после выхода из контекста отправляют его обратно. Разметчик релевантности также обращается к внутренним сервисам, чтобы собрать метаданные про SKU (Stock Keeping Unit) — уникальный код товара.
В нашем решении можно с легкостью использовать любую другую LLM, архитектура открыта к добавлению новых сценариев разметки. Все, что нужно — описать входные и выходные данные, промпт и логику обработки. Чтобы не тратить время на повторную разметку, кэшируем результаты. Как-либо менять существующую инфраструктуру не нужно.
Заключение: выводы и roadmap
То, что началось как эксперимент с LLM-разметкой, превратилось в полноценный инструмент. Мы значительно ускорили время, которое тратили на разметку текстовых данных, снизили зависимость от внешних сервисов краудсорсинга и затраты на ручную разметку, у нас появилось больше времени на различные эксперименты.
Из последнего — был построен пайплайн автоматической разметки и мониторинга качества поиска. Алгоритм следующий:
-
берем семпл реальных запросов за вчерашний день;
-
товары, которые были в них, размечаем на предмет релевантности;
-
результат сохраняем в базу;
-
подключаем BI для визуализации трендов и аномалий;
-
мониторим динамику долей классов, а также конкретные нерелевантные кейсы, чтобы иметь возможность быстро их чинить.
Чуть дальше по времени, изучение возможностей больших языковых моделей, применительно к различным сценариям. Например, для разметки релевантности при ранжировании магазинов в поиске по запросу или новых сценариев генерации данных.
На этом все! Если у вас появились вопросы или желание поделить��я опытом в LLM, с удовольствием пообщаюсь в комментариях!
Автор: Alexandr_Baranov


