Привет! Меня зовут Илья Валяев, я data science team lead поискового ранжирования в Авито. В этой статье рассказываю, как у нас всё устроена система ранжирования, какие технологии используем и как именно улучшаем поисковые выдачи. Статья будет интересна ML-инженерам, которые владеют базовой теорией машинного обучения и хотят разобраться в том, как устроено поисковое ранжирование.
Навигация по статье:
Воронка пользователя на Авито — путь пользователя
Когда человек заходит на площадку, он может посмотреть рекомендации на главной или сразу задать поисковый запрос. Во втором случае процесс будет выглядеть так:
Покупатель вводит запрос на главной странице. Например, «Купить айфон».
Мы пытаемся угадать фильтры по тексту. Например, пока человек набирает в поисковой строке «Купить айфон», мы подсказываем категорию и конкретные модели. И после нажатия кнопки «Поиск», пользователь попадает в выдачу: «Электроника» → «Мобильные телефоны» → Apple.
Дальше пользователи обычно уточняют запрос. В примере с айфоном покупатель может выбрать модель, цвет, объём памяти.
Схематично путь пользователя можно представить так:
В итоге сессия состоит из множества запросов, по части из которых человек даже не смотрит выдачу.
И примерно на 25% запросов пользователи ставят настолько узкие фильтры, что видят либо очень мало объявлений, либо пустую выдачу.
Лирическое отступление: как мы растим полноту выдачи
Проблемы с полнотой можно разделить на две ситуации. Первая – пользователь оказался на пустой выдаче и пытается указать новую конфигурацию фильтров, чтобы нашлись хоть какие-то товары. Вторая — даже если выдача не пустая, товары всё равно могут быть не самыми подходящими под запрос.
В обоих случаях пользователь расстраивается и хочет реже искать товары или услуги на нашей площадке . Поэтому нам важно обеспечивать достаточную полноту выдач в большинстве случаев. И для этого мы делаем следующее:
Дополняем пустые выдачи. Если по запросу ничего не найдено — начинаем искать похожие товары по векторам и предлагаем их пользователям:
Иллюстрация выше — из статьи: «Использование faiss для поиска по многомерным пространствам».
Подробнее о векторном поиске в рекомендациях рассказывал мой коллега Василий Рубцов:
Статья: «Как мы используем item2vec для рекомендаций похожих товаров»
Показываем товары из других городов, которые можно заказать с доставкой. Этот способ — более кликабельный, чем блок с рекомендациями, поэтому чаще всего в поиске покупатели видят именно такой вариант.
Генерируем пример описания товара во время подачи объявления, чтобы повышать ликвидность. После того как человек выбрал категорию и загрузил фото товара, мы подсказываем, что стоит добавить в описание.
Например, если человек продаёт одежду и не указывает её размер, материал или другие детали в описании — уменьшается шанс, что покупатели найдут этот товар в поиске. Поэтому при подаче объявления мы предлагаем сгенерировать шаблон описания для продавца. В тексте шаблона указываем характеристики, которые интересны покупателям.
Вот пример такого шаблона:
Продавец может добавить данные в пустые поля или полностью переписать сгенерированное описание на свой вкус. А потом мы используем текст для поиска ровно так же, как если бы пользователь написал его самостоятельно.
Поиск кандидатов для выдачи важен не меньше, чем само ранжирование.
Мы стараемся решать проблему полноты с разных сторон и каждой итерацией целимся в рост конверсии из поиска в сделку. По нашему опыту удачное расширение полноты выдачи может улучшать поиск не хуже , чем ранжирование.
Сложности в системе ранжирования высоконагруженных систем
Ранжировать результаты поиска на Авито непросто и вот почему:
Большой объём данных. На момент написания статьи на Авито размещено ≈ 200 миллионов объявлений. Пользователи делают несколько тысяч запросов в секунду, нагрузку нужно уметь выдерживать и быстро отвечать на каждый запрос.
Кроме того, в поиске на Авито есть как очень популярные запросы, например, «купить диван», так и более узкие, с разными критериями. Например, обычно покупатели ищут не просто диван, а конкретную модель, в конкретном регионе, определённой конфигурации и с диапазоном по цене. Всё это нужно уметь грамотно ранжировать: в первом случае нам важно показывать разнообразие, во втором — подводить к покупке.
Офлайн-метрики не всегда бьются с онлайн-результатами в A/B-тестах. Из-за этого приходится каждое изменение проверять в онлайне, что снижает скорость экспериментов.
Ранжирование идёт не только по ML-модели, но и по бизнес-правилам. Например, нужно учитывать, что часть продавцов покупают услуги продвижения, чтобы поднимать просмотры у своих объявлений. Об этом расскажу подробнее в следующих статьях.
Нужно учитывать интересы нескольких групп пользователей. На Авито есть как частные продавцы, так и компании с сотнями товаров.
Покупатели тоже делятся на категории — есть те, кто ищут личные вещи, а есть рекрутеры компаний, которые просматривают резюме на Авито. И у всех категорий пользователей одна поисковая выдача, в которой нам нужно учитывать желания и особенности разных групп.
Для покупателей важно, чтобы объявлений было много и их было удобно искать. А также они хотят как можно быстрее и безопаснее покупать то, что им нужно.
Для частных продавцов важны скорость продажи товаров на площадке, простота условий, справедливость положения в поиске относительно других. При этом сами они не всегда знают, как правильно написать объявление, чтобы как можно быстрее продать товар.
Для компаний важно, чтобы их объявления попадали в топ выдачи и были выше объявлений конкурентов. Так как компании часто платят за продвижение на выдаче — они хотят, чтобы отдача от вложений была предсказуема.
Критерии качества ранжирования
Для себя мы определяем следующие важные свойства поисковой выдачи:
Релевантность. Объявления должны соответствовать поисковому запросу.
Конверсионность. Важно, чтобы объявления нравились покупателям. Для этого продавцы пишут информативное описание и делают качественные фотографии.
Справедливость к продавцу. Мы не отдаём предпочтение отдельным продавцам, а стараемся распределять показы на разных пользователей. Стремимся, чтобы покупка услуг продвижения имела предсказуемый эффект.
Скорость продажи. На запросах с большой долей частных продавцов даём буст свежим объявлениям, чтобы человек мог продать товар быстрее.
Исходя из всех особенностей поисковой выдачи, мы делаем общую систему и стараемся учитывать интересы всех сторон. Смотрим метрики по сегментам и пытаемся не допустить перекосов в пользу какой-то из категорий.
Дальше расскажу, какие технологии используем и как обучаем модели, чтобы справляться с задачами ранжирования.
Пайплайн ранжирования
В этом материале лишь поверхностно расскажу про инфраструктуру и технологии. А про технические особенности поиска на Авито рассказывал мой коллега Вячеслав Крюков в другой статье:
Статья «Эволюция поиска — как купить пианино в 3 клика»
Предрассчитанных выдач у нас нет, поэтому мы показываем объявления, даже если человек выложил его 5 минут назад. Этот факт накладывает особенно жёсткие ограничения на пайплайн ранжирования. Тем не менее, за полсекунды мы успеваем пройти четыре этапа.
Отбираем кандидатов. Ищем их по обратному индексу от текста запросов и фильтров и отбираем до 100 000 объявлений на ранжирование.
L1-ранжирование: отбираем 500 объявлений из кандидатов, собранных по обратному индексу. Учитываем релевантность, вероятность сделки, то есть кликабельность и свежесть объявления. Помним, что продавцам важно продать побыстрее.
L2-ранжирование: из топ-500 отбираем 50 самых хороших. На этом этапе мы успеваем посчитать более долгие фичи вроде персонализации, а также фичи по самой выдаче: распределение цен, количество релевантных объявлений по запросу.
Подмешиваем в выдачу платные объявления — продавцы применили к ним услуги продвижения, чтобы собирать больше просмотров.
Схема с этапами ранжирования и технологиями, которые мы используем:
В итоговой выдаче пользователь видит 30–50 объявлений, которые мы посчитали лучшими по нашим критериям. Дальше расскажу, как мы обучаем и валидируем модели.
Обучение и валидация модели текстовой релевантности
В качестве модели релевантности мы используем градиентный бустинг на 100 фичей и 300 деревьев — помните о скорости ответа! Мы используем библиотеку CatBoost, так как инференс у неё быстрее, чем у XgBoost и LightGBM.
Обучение
Размечаем таргет с помощью сервиса Яндекс.Толока. Мы задаём асессору вопрос: «Подходит ли объявление по запросу?» и показываем пачку объявлений для разметки. Показываем сразу пачки объявлений, так как это помогает толокеру определиться, что считать релевантным, а что нет.
Сейчас используем pointwise лосс, но экспериментируем с pairwise. В результате обучения с pointwise лоссом получаем несмещённую вероятность, что это релевантное объявление, и его действительно стоит показывать в ответ на конкретный запрос. Такой скор легче интерпретировать и использовать в эвристиках.
А с pairwise — получаем абстрактный скор, который отражает, что одно объявление релевантнее другого. Например, можем сравнить два объявления по одному запросу: оба релевантны, но одно явно лучше подходит под запрос, чем другое. Покажу на примере с колясками:
Экспертная и LLM разметка. Если попросите ChatGPT отранжировать заголовки по релевантности, то заметите, что он неплохо справляется:
Но ChatGPT не всесилен и не знает всех товаров на Авито, а тем более их нюансов. Даже на моём примере он начал галлюцинировать новыми вариантами. Поэтому мы собираем экспертную разметку и дообучаем свою LLM учитывать все особенности.
А чтобы скорость расчёта была максимальной, в модели мы используем самые простые фичи. Практически все из них есть в документации Sphinx нашего поискового движка:
Валидация. Модель релевантности используется и для ранжирования, и для отсечек по релевантности в разных бизнес-эвристиках поверх ранжирования. Поэтому смотрим сразу несколько метрик: nDCG и accuracy на нужном нам пороге.
Ещё валидацию упрощает то, что ложноположительные ошибки оказываются в топе и их легко заметить при анализе выдач.
Обучение и валидация модели предсказания сделки
У нас две модели предсказания сделки: одна работает на первом уровне и ранжирует десятки тысяч объявлений, а вторая — сотни. Обе модели – CatBoost, но отличаются фичами и числом деревьев. В остальном они обучаются и валидируются одинаково.
По сравнению с ранжированием только по вероятности сделки, выдача в продакшене сильно меняется за счёт релевантности, свежести и платных объявлений. Так что мы не видим feedback loop. Обучаемся прямо на выдаче по продовой модели и не видим деградации со временем.
Обучение. Наши модели для предсказания сделки обучаются на поисковых логах. Мы боимся допустить даталик: при обучении модель может брать фичи из будущего по сравнению с моментом выдачи и применения модели. Поэтому фичи мы логируем прямо с прода.
Для этого подняли свой небольшой Kafka-топик и льём через него сырые выдачи с фичами в Spark. На Spark также джойним фичи на таргет — и датасет для обучения готов!
Мы обнаружили, что модель довольно слабо деградирует со временем, поэтому позволяем себе переобучать её раз в несколько месяцев.
С обучением есть сложность — мы не всегда знаем, произошла сделка на площадке или нет. Так что нам пришлось собрать свою прокси-метрику, которую и используем как таргет.
Понять, была ли сделка, нам помогают такие сигналы:
покупатель открыл номер телефона, перешёл в диалог с продавцом или нажал кнопку «Купить с доставкой»;
продавец закрыл объявление с пометкой «Продано на Авито»;
покупатель оставил отзыв на товар;
человек купил товар с Авито Доставкой. В этом случае достоверно знаем, что сделка прошла успешно.
Собираем все действия пользователя за следующую неделю, неважно с поиска или с рекомендаций. При этом есть большое разнообразие событий по категориям: квартиры бронируют, запчасти покупают с доставкой, а с сантехником просто обмениваются контактами.
Так как мы не уверены в большей части сигналов — учитываем их с разными весами, пропорционально конверсии в реальную сделку. Таргет получается вероятностный, дробный, и мы используем YetiRank pairwise для обучения модели.
Калибровка. Долгой и болезненной серией экспериментов мы поняли: менять модель так, чтобы не ломать бизнес-логику поверх неё — тяжело. Поэтому зафиксировали интерфейс взаимодействия модели с эвристиками так: модель выдаёт скалиброванный скор, а эвристики тюнятся под это распределение.
Калибровка помогает и в рекламных аукционах. Аукцион — это процесс распределения мест в выдаче среди тех, кто заплатил за продвижение объявлений. На популярных и перенасыщенных объявлениями запросах первые несколько объявлений конкурируют между собой за право быть на первой позиции и собрать максимальную долю просмотров. Аукционы помогают нам ранжировать эти объявления.
Мы берём скалиброванное предсказание сделки, умножаем его на ставку за транзакцию, то есть сумму, которую заплатит продавец, — и получаем ожидаемую выручку от него. По ней уже можем ранжировать объявления.
Сейчас остановились на pairwise-обучении и изотонической регрессии поверх скоров. В будущем хотим попробовать кастомные pairwise+pointwise-лоссы.
Подробнее про устройство аукционов в поиске можно узнать из выступления моего коллеги Александра Ледовского:
Видео: «Рекламные аукционы и автобиддинг в продуктах продвижения Авито»
Валидация. Для валидации модели берём следующие дни логов — используем классические метрики ранжирования: nDCG, MAP@k.
Мы экспериментируем с кастомными метриками, которые лучше коррелировали бы с онлайн-результатами. Для nDCG мы подбираем коэффициент важности позиции, так как кликабельность падает быстрее, чем дефолтный логарифм.
Также пробуем больше учитывать клики, которые были глубже в выдаче, по примеру с Профицитом Яндекса.
Статья про метрики ранжирования в учебнике ШАД: «Задача ранжирования»
Как сочетаем релевантность и вероятность сделки. Модель релевантности не предсказывает сделку напрямую. Но если выключить её или учесть только как фичу в модели сделок, пользователям приходится дольше искать нужные объявления среди «похожего» — и мы замечаем, что глубина пролистывания выдач увеличивается.
Мы всё ещё исследуем, как правильно смешать скоры разных моделей. Пока остановились на произведении скоров моделей с разными степенями. Это позволяет учитывать экстремальные значения и показывает хорошие метрики на A/B.
Совет: релевантность не всегда предсказывает сделки, но важна сама по себе. Ранжируйте по смеси разных моделей.
Эвристики поверх ML-моделей
Из всех критериев наши ML-модели предсказывают только релевантность и конверсионность. А справедливость и скорость продажи растим за счёт эвристик.
У нас много не только категорийных особенностей, но и эвристик, например, много if→else условий. Чтобы количество if-ов не превышало число сплитов в ML-моделях, мы пришли к универсальной схеме: для каждого объявления считаем порядка десяти чисел-характеристик.
Основные характеристики:
качество с точки зрения покупателя;
свежесть объявления;
сила услуг продвижения, например, ставка в аукционе.
Всё многообразие требований мы сводим к этим характеристикам и дальше ранжируем, учитывая только эти числа.
Когда задача формализована до состояния оптимизации по десяти характеристикам — легко перебирать разные варианты оптимизаторов. Сейчас мы используем алгоритм, который жадно выбирает объявления для показа, позиция за позицией. В будущем хотим попробовать и другие способы решения задачи.
Приёмка пайплайна
Так как ранжирование состоит из большого числа компонентов, то нам помогает приёмка перед A/B-тестом всего пайплайна. Приёмка в общем смысле делается следующим образом:
метрики ML-модели на логах;
продуктовые метрики пайплайна целиком, через офлайн-обстрел;
пользовательские метрики на A/B.
Расскажу о нововведениях, которые мы добавили, чтобы получать более предсказуемый результат на A/B-тестах.
Офлайн-обстрел
В продакшене к стандартной ML-модели ранжирования добавляются ещё эвристики, и нам важно проверить, как всё работает в совокупности. Например, в нашем случае эвристики поднимают платные и свежие объявления, перемешивают их, чтобы товары не занимали одно и то же место в выдаче.
Из-за того, что эвристики постоянно дорабатываются, мы не можем воспроизвести на логах и посмотреть, как изменится финальное ранжирование. А на офлайн-обстреле можем. Вот как это работает.
Встраиваем новую модель в продовый контур ранжирования, чтобы посмотреть, как будет выглядеть поисковая выдача с ней. Мы берём запросы из логов и перезапрашиваем их в продовом поиске. Сравниваем 2 варианта: выдачи с продовой и новой моделью.
Считаем метрики. Так как мы часто вносим изменения, которые «незаметны человеческому глазу» (объявления меняются местами с соседями), то мы — собираем выдачи по тысячам запросов и смотрим на такие метрики:
средняя релевантность выдачи;
среднее предсказание вероятности сделки;
средняя доля платных объявлений;
средний возраст объявлений на выдаче.
На этом этапе мы считаем только метрики по объявлениям на выдаче. Мы не знаем, на какие объявления кликнули бы покупатели, потому что выдачу собрали сами, пусть даже по запросам из реальных пользовательских сессий. Также она могла сильно измениться по сравнению с тем, что видел пользователь на момент запроса.
Поэтому пользовательские метрики — вроде конверсии из поиска в сделку — мы оставляем на A/B.
Помимо метрик, мы также просматриваем side-by-side часть запросов с самыми большими изменениями, чтобы разобрать случаи, где алгоритм ранжирования мог сломаться.
В итоге получаем таблицу с метриками, категориями и смотрим, что конкретно поломали:
В таблице видно, что уменьшилась доля платных объявлений в поиске. Поэтому, прежде чем катить это изменение в A/B-тест, стоит поднять долю платных объявлений, чтобы не уронить метрики монетизации.
Результат: проверяем связку ML-моделей и эвристик
Метрики по большому количеству запросов позволяют увидеть даже небольшие изменения. Сравниваем выдачи side-by-side для поиска причин.
По такой промежуточной офлайн-оценке мы понимаем, что не сломали алгоритм ранжирования, и можем оценить возможный эффект на пользователей.
Например, если доля релевантных объявлений упала на 10% — мы понимаем, что пользователями придётся листать выдачу глубже. Исходя из этого предполагаем, что на A/B вырастет метрика глубины листания, и, возможно, упадёт суммарное количество сделок с поиска.
Оценка результатов A/B-тестов для ML-моделей в поиске
Если результаты офлайн-обстрела нас удовлетворили, то мы катим A/B. Расскажу про две особенности тестов для поиска:
Смотрим на множество метрик одновременно и учитываем разные разрезы. Например, конверсии в просмотр, контакт, сделку, скорость поиска, уровень детализации запросов, свежесть объявлений и другие.
Разрезы смотрим по отдельным категориям, после проведения A/B получается длинный отчёт:
Значения показателей:
Целевые метрики | Контрметрики |
contacts — нажатия на кнопку «узнать телефон» или «написать в чат». | contacts_vas — контакты на платные объявления. |
msg_first — события начала переписки. | serp_age — возраст объявлений на выдаче. |
answered_first_messages_ratio — доля отвеченных сообщений. | contacts_3d — контакты на объявления, которым меньше трёх дней. |
user_c_per_s — доля совершивших контакт среди искавших что-то в поиске. | |
contacts_rnk10 — контакты в топ-10 объявлений из выдачи. |
Наша команда старается растить метрику daily_target_buyers — количество пользователей, совершивших сделку за день. Или её аппроксимацию — контакты с поиска. Всё это при условии, что остальные метрики не просели.
Рассматриваем метрики в разных категориях. Например, отдельно смотрим метрики по крупным городам, активным и неактивным пользователям.
Не выделяем отдельных метрик на популярные и редкие запросы, так как нам интересна вся сессия пользователя, а в ней много запросов, от широких вначале до узких при уточнении деталей.
Так как у нас много метрик и разрезов — мы получаем много ложноположительных прокрасов. Поэтому в дополнение к статистическим поправкам, мы стараемся искать смысловые связи между метриками.
Например, увеличение конверсии в топе и падение доли свежих объявлений могут означать просто улучшение одного критерия качества за счёт другого. А хочется видеть улучшение по Парето: улучшение одного критерия без ухудшения остальных.
Подробнее про нашу платформу A/B-тестирования можно почитать в других публикациях от моих коллег:
Выжимка: вся статья в шести пунктах или что мы сделали для улучшения поискового ранжирования
Ввели трёхуровневую систему ранжирования: сначала отбираем топ-500 подходящих объявлений из всего массива, а затем переранжируем по более сложным фичам. В конце добавляем интересы всех сегментов пользователей и монетизации.
Растим полноту: боремся с пустыми выдачами и помогаем пользователям делать описания товаров, чтобы объявление попадало под запросы.
Увеличиваем релевантность объявлений в выдаче. Используем отдельную модель для предсказания и экспериментируем с разметкой.
Берём композитный таргет, чтобы учесть вероятность сделки.
Используем pairwise-обучение c изотонической регрессией, чтобы получить несмещённую оценку ожидаемой выручки.
Делаем приёмку модели в 3 шага. Сначала валидируем на кликовых логах, после этого добавляем офлайн-приёмку, чтобы соблюсти баланс между ML-моделью и эвристиками поиска, а в конце катим A/B.
Если у вас появились вопросы — буду рад пообщаться в комментариях или в личке. А если у вас есть истории о работе с поисковым ранжированием больших объемов данных – рассказывайте их в комментариях. Спасибо за уделенное статье время!
Подписывайтесь на канал AvitoTech в Telegram, там мы рассказываем больше о профессиональном опыте наших инженеров, проектах и работе в Авито, а также анонсируем митапы и статьи.