Привет, с вами снова Даша и Uzum Market. В прошлый раз мы глубоко погрузились в пайплайн работы поиска нашего маркетплейса, и я обещала вам вернуться с новостями о его улучшении. Так вот, время пришло, и сегодня мы поговорим про наш опыт внедрения векторного поиска!

В последние несколько лет векторный поиск является the hottest topic во всем поисковом сообществе, поэтому не будем отставать от трендов. Начнем с небольшой теории и ответим на 2 основополагающих вопроса:

  • Кто такой векторный поиск?

  • И зачем, собственно, он всем нужен?

Векторный поиск означает, что вместо поиска совпадений на основе найденных ключевых слов или подобранных синонимов мы получаем семантически схожие товары на основе введенного запроса. Такой подход позволил бы нам извлекать релевантные товары из индекса без необходимости точного соответствия запросу. Звучит очень хорошо, правда? Но, как вы догадались, это может оказаться очень нетривиальной задачей по ряду причин:

  • Пользователи используют термины, отличные от используемых в описании товаров в каталоге, например: «футляр для лэптопа» вместо «чехол для ноутбука».

  • Поиск работает по-разному с разным написанием одного и того же запроса, например: «малиновый чай» и «чай с малиной».

  • Пользователи все чаще формулируют свои запросы на естественном языке (спасибо, ChatGPT), например: «стильные и комфортные кроссовки для повседневной носки».

  • Пользователи часто делают ошибки в запросах, а механизмы исправления орфографии на основе поисковых индексов ограничены.

Эти проблемы относятся к первой фазе пайплайна поиска под названием retrieval (отбор кандидатов по запросу), и именно векторный поиск может помочь их решить. 

Из чего состоит векторный поиск?

Прежде чем мы погрузимся в технические детали векторного поиска в Uzum Market, давайте разберем компоненты, из которых он состоит. Это энкодеры, языковые модели, эмбединги и векторы. Все слова знакомы, но как это работает вместе?

В первую очередь векторный поиск — это про ретривал — тот самый этап, который идет до ранжирования. C его помощью мы получаем набор релевантных товаров P по запросу Q.

В полнотекстовом ретривале (term-based retrieval) мы с вами сравнивали токены (слова) запроса Q и названий товаров из всего множества товаров в индексе для получения итогового набора P.

В семантическом ретривале (dense retrieval) мы получаем набор релевантных товаров, используя векторы. Первый вектор отвечает за текстовый запрос, второй — за название товара с его характеристиками. Перемножая эти векторы, мы получаем меру близости или косинусную меру, которая показывает, насколько близко друг к другу по семантике находятся в пространстве запрос и товар.

Как получить векторы из наших данных?

Для преобразования запросов и текстовых данных о товарах нам нужны текстовые энкодеры. Именно они преобразуют текстовую информацию с помощью языковой модели в числовые представления, такие как векторы или эмбеддинги. Далее индекс ElasticSearch хранит эти векторы в виде полей товаров с типом dense_vector.

Запросы пользователей также преобразуются в векторы, однако это приходится делать на лету, ведь мы не знаем запрос пользователя до тех пор, пока он его не ввел. Ниже на диаграмме показано, как эти компоненты работают вместе:

Получение эмбеддингов товаров и запросов для вычисления косинусной близости между ними.
Получение эмбеддингов товаров и запросов для вычисления косинусной близости между ними.

Языковая модель

Еще раз обозначим задачу, которая стоит перед нами:

Обучить энкодер для запросов и энкодер для товаров, а затем отображать запросы и товары в такие векторы-представления, чтобы векторы запросов имели высокую косинусную схожесть с векторами релевантных товаров, и низкую — с векторами нерелевантных товаров.

Для этой задачи отлично подойдет двухбашенная архитектура модели. Идея такой архитектуры двольно проста, она состоит из двух башен: одна для запроса, другая для товара. С помощью нейронных сетей модель учит абстрактные представления (эмбеддинги) запросов и товаров по отдельности, а потом считает косинусную меру близости с помощью скалярного произведения

На инференсе эмбеддинги товаров могут быть подсчитаны заранее и закешированы, в то время как эмбеддинги запросов пользователей будут считаться на лету.

Двухбашенная модель.
Двухбашенная модель.

Как обучать башни?

Обучение всей системы ретривала мы разделили на два этапа:

  1. предобучение башни товаров на наших данных;

  2. дообучение модели на целевой задаче.

Далее я раскрою смысл каждого этапа, но начать предлагаю, по традиции, с данных.

Данные

Любая языковая модель основана в первую очередь на языке, а в нашем случае — на языках! Их три: узбекский, русский и английский. Как вы могли предположить, предобученных моделей на русском и английском пруд пруди, а вот на узбекском проблемы паралича выбора не стоит. Именно поэтому нам необходимо уделить узбекскому языку особое внимание и дообучить на нем нашу языковую модель.

Башня запросов принимает только сырой текст самого запроса. У башни товаров входные данные более сложные — название товара на русском и узбекском языках, а также его описание и характеристики. Так как все текстовые поля товаров в Uzum Market существуют на двух языках, то мы конкатенировали тексты при помощи специальных токенов: [UZ_TEXT] текст на узбекском [RU_TEXT] текст на русском.

Карточка с описанием товара на узбекском и на русском языках.
Карточка с описанием товара на узбекском и на русском языках.

Итоговый датасет состоял из пар «запрос-товар» и их метки: действия, которое совершил пользователь. В зависимости от совершенного действия, наши пары тоже разделились на два кластера. Позитивные пары — когда товар соответствует запросу пользователя, негативные — когда товар запросу не соответствует. В качестве позитивных действий мы взяли клики по товару и добавления в корзину.

Датасет разделили на train, validation и test части:

  • В train попали пары «запрос-товар», у которых было либо одно добавление в корзину, либо два клика и ноль добавлений в корзину. Таким образом мы убрали большую часть шума в данных, облегчив задачу моделям.

  • Каждый элемент в validation описали как кортеж вида (запрос, добавленные в корзину по этому запросу товары за период валидации, кликнутые по этому запросу товары за период валидации).

  • Для test поменяли только период сбора данных.

Первый этап обучения

На первом этапе для получения эмбеддингов товаров обучили модель на двух задачах:

  1. Классификация категории (catpred) товаров по эмбеддингу CLS токена. Модель научилась извлекать семантические представления для каждой категории товара. Полученные эмбеддинги CLSтокена стали содержать информацию о семантике и контексте категорий товаров.

  2. Masked Language Modeling (MLM). Это одна из задач, на которой обучался оригинальный BERT. MLM учит модель предсказывать замаскированные слова в предложении. Этой задачей мы хотели научить модель семантике наших товарных описаний, а также избежать переобучения на catpred.

Для обучения под обе задачи мы воспользовались гугловской моделью LaBSE. Loss — сумма двух лоссов MLM и catpred без весов, так как эти лоссы оказались одного порядка.

? Важный вывод, который мы сделали: клиппинг градиентов сильно замедлял обучение, а его отсутствие не приводило к нестабильности.

Второй этап обучения: Metric Learning

Metric Learning — подход к обучению модели, при котором ищется оптимальное пространство признаков, в котором эмбеддинги объектов, имеющие схожие характеристики, находятся близко друг к другу, а отличающиеся объекты — далеко. Это достигается путем минимизации функции потерь, которая измеряет расстояние между представлениями объектов в этом пространстве.

Данные снова разделили на три части — train/validation/test — в зависимости от времени взаимодействия. Отфильтровали пары «запрос-товар» с низким количеством взаимодействий, а веса пар вычислили с помощью коэффициента для уравнивания вклада кликов и добавлений в корзину.

Задача метрического обучения включает в себя:

  • Приближение эмбеддингов соответствующих товаров.

  • Отдаление эмбеддингов несоответствующих товаров.

  • Формирование in-batch негативов для несоответствующих пар (об этом дальше).

  • Использование функции потерь softmax с учетом обучаемой температуры, аналогичной CLIP.

  • Определение финального лосса-скаляра с учетом весовых коэффициентов и сглаживающей функции (sqrt или log).

  • Вычисление среднего взвешенного значения функции потерь для получения финального лосса.

Процесс валидации

Важной частью процесса валидации является построение индекса товаров ANN (Approximate KNN), в нашем случае — HNSW. С помощью ANN мы получили сложные негативы для каждого запроса из top-100.

? Приближенный метод ближайших соседей (ANN) представляет собой алгоритм поиска близких объектов в наборе данных, который стремится обеспечить баланс между точностью и вычислительной эффективностью. В отличие от точного метода KNN, при котором происходит неэффективный перебор всех данных для поиска ближайших соседей, Approximate KNN использует оптимизированный перебор лишь части данных, что позволяет быстро получить результаты.

Формирование in-batch негативов

Для формирования негативов внутри батча сэмплируем N запросов и N товаров. Причем i-ый товар соответствует i-му запросу в том, что они имели какое-то количество кликов или добавлений в корзину между собой. Все это происходит на основе косинусной схожести, где значение на диагонали увеличивается, а вне диагонали — уменьшается.

Матрица пар «запрос-товар» и их косинусной близости.
Матрица пар «запрос-товар» и их косинусной близости.

Для улучшения обучения мы ввели понятие «сложного негатива»: товар, который модель, скорее всего, отнесла бы к позитивным примерам. Это товары, которые показывались в топе выдачи по запросу, но при этом не были кликнуты по нему. Эмпирически мы определили, что такие сложные негативы находятся на позициях с 15 по 40. Их мы добавляем к каждому товару в матрицу справа. На этих товарах наша модель, скорее всего, ошиблась бы, поэтому мы явно говорим ей: «Не ошибайся здесь!»

Матрица пар «запрос-товар» и их косинусной близости со сложными негативами.
Матрица пар «запрос-товар» и их косинусной близости со сложными негативами.

Метрики

В качестве метрик мы решили взять: recall@20, recall@100, ndcg@20 и ndcg@100.

Скор для клика по товару — 0,5, а для добавления в корзину — 1,0. Основной метрикой считаем recall@100, так как обучаем первую стадию поиска (ретривал), и потому 100 важнее 20. Отмечу, что все метрики линейно взвешивались по частотам запросов за соответствующий период (val/test). Если один запрос имеет частоту 100, а другой — 10, то в рамках метрики первый запрос в 10 раз важнее второго.

Гибридный поиск

Теперь у нас есть модели, которые преобразуют запросы и товары в эмбеддинги, учитывая пользовательские взаимодействия с ними в прошлом. Что дальше? Как это все встроить в уже существующий пайплайн поиска? Для ответа на этот вопрос предлагаю сделать шаг назад и взглянуть со стороны на этап ретривала, к которому мы стремимся.

  1. Sparse retrieval — тот ретривал, который мы обсуждали в прошлой статье, когда выбирали подвыборку товаров с помощью проверки на точное совпадение токенов запроса и товара с помощью minimum_should_match и дальнейшего ранжирования силами bm25 фичей. Важно отметить, что этот этап никуда не девается, ведь он играет важную роль в поиске. С помощью векторного поиска мы хотим только расширить ретривал.

    Term-Based (sparse) retrieval.
    Term-Based (sparse) retrieval.
  1. Dense retrieval — векторный поиск, или второй ручеек (как мы называем его в команде). Именно его мы с вами обсуждаем в этот раз. Нам важно встроить эту часть таким образом, чтобы помочь первому ручейку, не уронив общие бизнес-метрики поиска.

    Embedded (dense) retrieval.
    Embedded (dense) retrieval.

Я думаю, вы уже поняли, к чему я веду: эти два ручейка (метода) можно смешать в один и получить гибридный поиск. Он позволяет точнее удовлетворять запросам пользователей и улучшать качество результатов.

Гибридный поиск.
Гибридный поиск.

KNN в ElasticSearch

Функционал, про который пойдет речь в этой части моей статьи, позволил нам запустить векторный поиск внутри ElasticSearch. Уверена, что без этого обновления путь от начала сбора данных до запуска A/B-теста увеличился бы в несколько раз.

Реализация гибридного поиска в ElasticSearch стала доступна в версии 8.0. Она позволяет добавить второй ручеек в поисковый запрос к корпусу товаров. Сделать это можно с помощью нескольких этапов (заметили, как мы все разбиваем на этапы?):

  1. Добавить векторную фичу в маппинг поискового индекса.

    "text_vector": {"type": "dense_vector", 
                  "dims": 256, "index": "true", 
                  "similarity": "dot_product", 
                  "index_options": {"type": "hnsw", 
                                    "m": 32,
                                    "ef_construction": 100}
  2. Добавить в индекс ElasticSearch векторную фичу по всем товарам.

  3. Добавить в запрос часть с ann ретривалом, которая выглядит так:

    "knn": { "field": "vector_text", 
             "query_vector": "", 
             "k": 200, 
             "num_candidates": 2000, 
             "boost": 10 }

Обогащая этап ретривала товарами, пришедшими из KNN, мы получаем 1200 товаров:

  • 1000 приходит из sparse-ручейка, как и раньше;

  • 200 приходят из KNN.

А на этапе реранжирования мы с помощью LTR-модели мы реранжируем top-1000 товаров.

? Важно отметить, что мы также добавили векторную фичу (косинусную меру близости) в модель LTR-ранжирования. Это было сделано для того, чтобы товары, пришедшие из dense-ручейка, могли попасть в топ ранжированной выдачи, так как заведомо известно, что все их фичи текстовой близости (bm25) равны 0.

Объединение результатов dense- и sparse-товаров для ретривала.
Объединение результатов dense- и sparse-товаров для ретривала.

Результаты

Давайте разберемся, какие зависимости выучили башни, рассмотрим неудачные примеры, а также увидим на практике, как нейронный ретривал может помочь поиску.

Хорошие примеры

В рамках анализа результатов векторного поиска можно выделить примеры, когда новая модель ретривала продемонстрировала отличную эффективность. Вместо пустых или малополезных выдач мы получили набор релевантных товаров.

Неудачные примеры

В процессе тестирования мы также выявили неудачные примеры: некоторые запросы привели к менее удовлетворительным результатам, которые явно не совпадают с интеншеном пользователя. Эти негативные сценарии подчеркивают необходимость совершенствования модели. Вот, например, пользователь искал «jo’nli efir», что с узбекского переводится как «прямой эфир» и никак не связано с эфирным маслом.

Челлендж

Мы с вами совершенно не учли тот факт, что KNN всегда пытается вернуть K ближайших соседей. Если изначально у нас есть 1000 товаров, пришедших из sparse ретривала, то добавив еще 10 тысяч товаров по семантической близости, мы кардинально изменим выдачу в нашем маркетплейсе. Такой сценарий может привести к резкому падению ключевых метрик, чего мы, конечно, не хотим.

Даже если мы уменьшим K до 100 и по определенному запросу не будет релевантных товаров, KNN все равно вернет 100 товаров, и они будут располагаться в векторном пространстве далеко от запроса. Что же делать? Как придержать KNN в узде и не дать ей захватить власть над бедным текстовым поиском?

Команда ElasticSearch заботится о нас быстрее, чем мы придумываем себе челленджи. В ElasticSearch 8.8 появилась функциональность, позволяющая установить порог, который отсекает товары со сходством менее указанного значения. Звучит просто, не так ли? Но не торопитесь, тут много нюансов. Если вы внимательно прочитали фразу выше, то поняли, что порог у нас один. Учитывая специфику запросов, их разную частоту и количество контента, мы явно не можем найти такое одно число, которое решит все наши проблемы. Это было бы слишком хорошо. Одно идеальное число мы не найдем, но это одно число может исправить ситуацию кардинально.

Как мы подбирали порог?

Решили делить наших пользователей на три группы:

  • текущий ретривал и ранжирование;

  • ретривал с ANN + LTR-ранжирование с фичей текстовой близости;

  • ретривал с ANN + LTR-ранжирование с фичей текстовой близости + порог.

Зачем там сверху уточнение про фичу текстовой близости? При получении новых товаров в ретривале, мы хотим правильно их дальше ранжировать, однако текущая модель ранжирования ничего не знает про векторную близость товаров. Вот тут я прямо слышу ваши мысли: «Даша, у этих товаров нет статистик, ведь они никогда не были показаны по этому запросу, проблема representation bias!» Соглашусь, тут есть эта проблема и, вероятно, ее нужно решать совсем иными способами, однако у нас фича текстовой близости оказалась top-1 по важности в модели LTR. Таким образом самые релевантные товары по семантической близости все равно попадают в топ-1000 для реранжирования.

Так а что там с порогом? На данный момент это вторая актуальная проблема, которую активно обсуждают в поисковом сообществе, но до сих пор не разработан идеальный алгоритм ее решения. Вот что сделали мы:

  • Выбрали N запросов, для которых в топ-50 выдачи имеются нерелевантные товары.

  • Для каждого запроса получили скор между вектором запроса и вектором товара, начиная с которого появляются нерелевантные товары, и взяли медиану этих скоров.

  • Взяли топ-1000 запросов по частоте.

  • Получили выдачи ElasticSearch для этих запросов.

  • Прошлись по порогам, вычисленным на основе N плохих пар «запрос-товар», и для каждого порога посчитали:

    • Среднее количество дополнительных товаров, которые мы находим по этим топ-1000 запросам относительно sparse поиска.

    • В какой доле запросов из N мы убираем все неподходящие товары.

    • Доля запросов, которые лишаются выдачи в целом.

  • По трем предыдущим метрикам выбрали оптимальное для нас значение.

Результаты A/B-теста

Невозможно закончить эту статью без результатов, которых мы достигли благодаря такой большой работе. Поделиться точными числами не удастся, увы, но мы можем показать примерную картину того, как расположились наши метрики.

Первоочередная цель бизнеса — прибыль, однако она имеет свойство краситься медленно, поэтому основной метрикой выбрали конверсию из поиска в покупку и, предшествующую ей, конверсию из поиска в добавление в корзину.

Охранные, или Guardrail метрики, — это те, которые мы ни в коем случае не хотим просадить. Их рост свидетельствует о значительном импакте в компанию и позволяет представить результаты на квартальном уровне.

To sum up, A/B-тест прошел успешно, основные и охранные метрики стат значимо подросли и мы со спокойной душой раскатили векторный поиск на всех наших пользователей. Лидирующую позицию с отрывом заняла группа под номером 3, то есть векторный поиск с порогом.

Если будете в Узбекистане, или вам просто интересно, как работает векторный поиск, заходите к нам в Uzum Market.

Результаты A/B-теста. Целевые метрики.
Результаты A/B-теста. Целевые метрики.
Результаты A/B-теста. Охранные метрики.
Результаты A/B-теста. Охранные метрики.

Какие у нас планы по улучшению векторного поиска?

Воспользуюсь случаем и поблагодарю маму команду. Спасибо вам, вы все очень крутые!

Кажется, тут можно написать еще одну статью. Это был наш первый эксперимент с ANN, и впереди еще множество итераций по его улучшению. Начнем мы с проработки порогов. Нам хочется детальнее проанализировать текущее состояние ANN и попробовать более Data-Driven подход к выбору того самого одного числа.

На этом у меня все. Все велком в комментарии, если остались вопросы.

До скорой встречи, ведь у меня уже готовится для вас новая тема. Наш поиск не стоит на месте!

Полезные ссылки

  1. Под капотом поискового движка: Как Uzum Market применяет ML, чтобы вы нашли желаемое

  2. Dense vector field type | ElasticSearch Guide [8.13] | Elastic

  3. Approximate kNN | ElasticSearch Guide [8.13] | Elastic

Комментарии (2)


  1. overdose
    26.05.2024 14:51

    может быть в чем то и помогает это, но косяк с поиском немного бесит.

    Искал книгу с словом в название "иффати" (https://uzum.uz/ru/search?query=iffati) выводит нужные результаты, при повторном поиске запрос меняется на "sifati" (https://uzum.uz/ru/search?query=sifati&needsCorrection=1) а там совсем не нужные результаты.


  1. yuma04
    26.05.2024 14:51

    Дарья, а как селлеру для продвижения своих товаров находить самые высокочастотные запросы, чтобы по ним показывать свои товары покупателям?
    Или же рекламироваться только внутри категории достаточно (например, если продаю мультиварки, то выбираю продвижение в разделе "Мультиварки и рисоварки")?

    И как лучше выбрать категорию? Например. Опять же, для мультиварки могу выбрать общий раздел "Мелкая техника для кухни" или же лучше выбирать уже "родной" раздел "Мультиварки и рисоварки"?