Как устроена архитектура факторов ранжирования в runtime поиска Ozon

Привет, Хабр! Меня зовут Лев. Я работаю в поиске Ozon. Сегодня я буду рассказывать про одну из составляющих поиска, а именно про ранжирование.

В этой статье расскажу:

  • Что такое ранжирование и факторы ранжирования.

  • Как работает поисковое ранжирование в Ozon.

  • Как мы работаем с факторами ранжирования.

Я буду переходить от простых примеров к сложным — от орехов и белок к товарам и сервисам.

Ранжирование и факторы ранжирования

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

У орешков есть свойства: цена, вес, рейтинг по мнению других белок. Эти свойства — базовые факторы, по которым можно судить об орехе.

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

Например, она посчитала среднюю цену — сложила все значения и разделила на три:

Средняя цена = (10 15 12) / 3 = 12.3

Потом — относительную цену, поделив цену конкретного ореха на среднюю.

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

Крутость ореха = рейтинг 0.1 × вес – относительная цена

Предположим, что у нас не обычная белка, а AI-белка. Она придумала ещё один фактор — ML-крутость ореха. Это уже не формула, а скор ML-модели, который показывает, насколько орех крут. ML-модель можно сравнить с чёрным ящиком: на вход подаются числовые признаки — например, вес, цена, рейтинг ореха. На выходе модель возвращает одно число — скор. Таргет беличьей модели устроен так, что чем больше значение, тем выше вероятность, что орех понравится другим белкам.

AI-белка посчитала фактор ML-крутости для каждого орешка: у первого получилось 3, у второго — 5, у третьего — 6. И отсортировала орехи по этому скору:

Подобную сортировку будем называть ранжирующей функцией. А всё, что использовалось при её вычислении, в том числе и то, что подаётся на вход ML-крутости, — факторами ранжирования.

Вычислив все факторы ранжирования и применив ранжирующую функцию, белка составила наиболее релевантный и востребованный топ для других белок, отсортировав орехи по ML-крутости. Теперь другие белки могут быть уверены, что на первом месте самый лучший орех.

Поисковое ранжирование в Ozon

Посмотрим, как пример с белкой и орехами связан с поиском в Ozon.

В поиске Ozon факторы ранжирования определяют, в каком порядке товары показываются в поисковой выдаче. То есть мы работаем не с топом орешков, а с топом товаров. Например, с топом товаров по запросу «шампунь»: 

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

В Ozon тоже есть своя белка — сервис под названием o2-midway, который занимается ранжированием товаров.

Принцип работы o2-midway такой: есть нижестоящий сервис, который присылает товары с их свойствами. Есть клиент, который делает запрос в поиск. А o2-midway, перед тем как отдать товары клиенту, меняет их порядок.

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

Развитие ранжирования

Чтобы улучшать ранжирование, необходимо:

  • Создавать больше факторов ранжирования и проверять разные гипотезы. Разные факторы ранжирования могут по-разному влиять на релевантность и персонализированность поисковой выдачи, важно проверять большое количество гипотез и оставлять только лучшие факторы.

  • Использовать более умные ранжирующие функции (постоянно дообучать и совершенствовать ML-модели).

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

Архитектура вычисления факторов ранжирования

Чтобы прийти к текущей архитектуре факторов ранжирования в Ozon, пойдём вместе с вами от простых примеров.

Предположим, что наш сервис должен уметь работать со следующими факторами:

  • P — цена товара;

  • Pavg — средняя цена товаров по запросу;

  • P / Pavg — относительная цена;

  •  — ML-модель, предсказывающая вероятность покупки;

  • R — длина запроса;

  • W — количество совпавших слов;

  • Mr — модель релевантности;

  • Mc   Mr — крутость товара (учли вероятность покупки и общее соответствие запросу).

Взаимосвязь этих факторов проиллюстрирована на схеме:

Теперь предположим, что наши ML-инженеры создали новую модель вероятности покупки Mv2:

Соответственно, также нужна новая крутость (сумма Mr и Mv2):

На картинке выше мы нарисовали схему зависимостей между всеми факторами ранжирования. Схема зависимостей представляет собой направленный ациклический граф DAG (Directed Acyclic Graph).

Любой фактор ранжирования можно представить как узел в графе.

Более того, любые сортировки, которые должен делать o2-midway, можно представить как вычисление подграфа. Например, если приходит запрос: «Отсортируй товары по цене», мы просто считаем один узел графа — цену.

Если приходит другой запрос — «Отсортируй товары по релевантности», то уже вычисляется подграф из трёх узлов.

В случае запроса «Отсортируй товары по крутости» мы вычислим весь граф:

В коде o2-midway реализован граф (DAG). Эта абстракция обладает рядом важных особенностей:

  • Автоматическое вычисление зависимостей. При регистрации нового узла достаточно просто указать его входы, и узел сам встроится в граф.

  • Вычисление независимых узлов в параллель. Например, посчитать два ML-скора можно параллельно, чтобы сократить время ответа поиска.

  • Граф не строится во время исполнения запроса. Это происходит на старте приложения, а также при добавлении новых факторов ранжирования в граф.

После того как мы написали код построения графа, любое добавление фактора в коде o2-midway выглядит примерно так (сам o2-midway написан на Java, ниже представлен схожий псевдокод):

graph.push(
  factor(”delivery_ts")
        .withInput("item_ids")
        .withOutput(
            itemIds => deliveryService.get(itemIds)
        )
);

 
graph.push(
  factor(”delivery_time")
        .withInput("delivery_ts")
        .withOutput(
  			deliveryTs => {
				var now = System.currentTimeMillis();
				return deliveryTs.stream()
					.map(deliveryTsForOneItem => deliveryTsForOneItem - now)
					.toList();
			}
        )
);

Это декларативный код:

  • Вы делаете graph.push.

  • Объявляете название фактора ранжирования — в данном случае это timestamp доставки delivery_ts.

  • Объявляете input, от какого фактора ранжирования зависит — например, от item_ids(массив с id товаров, которые надо отранжировать).

  • Передаёте лямбда-функцию, которая описывает, как получить delivery_ts, — в данном случае пойти в deliveryService и забрать timestamp доставки.

Следом идёт другой фактор — уже время доставки. У него:

  • input: delivery_ts;

  • output: delivery_tsnow.

На выходе получаем время доставки.

Разработчик фактора ранжирования не описывает, как именно всё будет вычисляться. Он задаёт, что нужно получить. Это сильно упрощает разработку и позволяет делать оптимизации, не меняя код вычисления факторов.

Итог:

  • Факторы ранжирования отлично ложатся на структуру направленного ациклического графа (DAG).

  • Для любой задачи ранжирования o2-midway достаточно вычислить подграф.

  • Такая абстракция позволяет писать декларативный код и ускорить любое добавление фактора, не думая о деталях вычисления.

Архитектура доставки факторов ранжирования

Здесь также постараемся прийти к решению от примеров. 

Предположим, что есть три фактора ранжирования:

  • текущая цена товара (price);

  • средняя цена товара за 7 дней (avg_price);

  • количество заказов товара (orders).

Допустим, все эти факторы хранятся в одном хранилище, в нашем коде, представленном объектом featureStoreService. Тогда код вычисления этих факторов ранжирования будет выглядеть так:

graph.push(
  factor("price")
        .withInput("item_ids")
        .withOutput(
            itemIds => featureStoreService.get("price", itemIds)
        )
);

graph.push(
  factor("avg_price")
        .withInput("item_ids")
        .withOutput(
            itemIds => featureStoreService.get("avg_price", itemIds)
        )
);

graph.push(
  factor("orders")
        .withInput("item_ids")
        .withOutput(
            itemIds => featureStoreService.get("orders", itemIds)
        )
);

В нашем примере все факторы вычисляются одинаково (происходит вызов featureStoreService с нужным полем). Для таких факторов ранжирования не хотелось бы писать новый код и катить релиз o2-midway, чтобы фактор ранжирования попал в граф и мог быть вычислен.

Примеры, где проблема необходимости релиза для создания нового фактора особенно проявляется:

  1. Статистики из хранилища. Например, вес, рейтинг, цена. Логика у них похожая, как в примерах выше: сходить в featureStoreService и забрать нужное поле по item_id.

  2. Функции от других факторов. Например: относительная цена = цена / средняя цена.

  3. Новые ML-модели.

По сути, такие факторы ранжирования могут описываться конфигами с различающимися параметрами от фактора к фактору. Мы приняли решение вынести эти конфиги в отдельный сервис — feature-meta-store. feature-meta-store позволяет добавлять типовые факторы ранжирования без ручного кода.

Прорастание типовых факторов из feature-meta-store в граф на o2-midway выглядит так:

  1. o2-midway запрашивает все доступные факторы из feature-meta-store.

  2. Проходит циклом for по каждому полученному фактору.

  3. Для каждого делает graph.push, где указывает название фактора, входные параметры, которые тоже берутся из метаинформации, и описывает, как вычислить результат.

Таким образом, мы получаем типовые факторы из feature-meta-store и автоматически добавляем их в граф. В результате нам больше не нужно писать шаблонный код и пересобирать o2-midway, например, для каждой новой ML-модели.

Под капотом у feature-meta-store

Теперь посмотрим, как устроена система feature-meta-store и какая у неё доменная модель.

Первая структура данных внутри — это таблицы с метаинформацией о факторах ранжирования. Для каждого типа фактора используется своя таблица. Ниже рассмотрим пример для трёх таких типов:

  1. Свойства по item_id — обычные статистики из хранилища. Здесь достаточно метаинформации о названии поля (цена, заказы).

  2. ML-модели — здесь нужна информация о входных факторах и название исполняемого файла, например, бинарника CatBoost.

  3. Функции — математические формулы, например: крутость = цена заказы.

Вторая важная часть доменной модели feature-meta-store — это ревизии, или подмножества факторов ранжирования.

Например:

  • prod_revision состоит из двух свойств (цена и заказы), одной ML-модели и функции крутости;

  • test_revision состоит из одной ML-модели.

Если упростить, факторы — это таблицы с метаинформацией о признаках, а ревизии — наборы факторов.

Ревизии нужны для разных окружений
Допустим, есть тестовое окружение o2-midway-test и продакшен — o2-midway. Для них можно настроить разные ревизии — например, test_revision и prod_revision.

Ревизии нужны для валидации конфигураций
Предположим, у сервиса feature-meta-store есть клиент formula-generator, добавляющий новый фактор ранжирования f1. Уже на этом этапе можно проверить корректность формулы, по которой рассчитывается фактор. Если f1 — это сумма трёх других факторов (s1 s2 s3), а в текущей ревизии отсутствует s3, мы сразу можем вернуть ошибку, не добавляя фактор в ревизию: «Фактор s3 не найден в ревизии — перепроверь формулу».

Так мы отлавливаем большинство ошибок конфигурации на раннем этапе — до того, как они попадут в o2-midway.

Использование feature-meta-store

Добавлять типовые факторы ранжирования и метаинформацию в feature-meta-store могут несколько разных клиентов.

Веб-интерфейс, UI. Позволяет описывать типовые факторы с помощью некоторого DSL. Эти формулы DSL сначала валидируются в feature-meta-store — проверяется корректность самой формулы, а затем интерпретируются на стороне o2-midway, и факторы вставляются в граф.

ml-pipeline. Классический пайплайн обучения моделей, состоящий из шести шагов:

  • Collect data — сбор данных;

  • Learn model — обучение модели;

  • Save to S3 — сохранение бинарника модели в S3;

  • Registrate model in feature-meta-store — регистрация новой модели;

  • Check model perf — проверка перформанса модели;

  • Check model quality — проверка качества модели.

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

Как защитить прод

ML- и Data-Science-инженеры могут напрямую влиять на продакшен через клиентов feature-meta-store. Это позволяет создавать функции ранжирования без релизов и ручного кода.

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

  1. Клиенты могут добавлять факторы только в тестовый контур (тестовую ревизию).

  2. В ml-pipeline встроен шаг проверки нового фактора-модели на перформанс (проверка происходит в тестовом контуре до попадания в продакшен).

При каждом запуске ml-pipeline формируется отчёт со сравнением созданной ML-модели и baseline-модели.

Если модель прошла проверку в тестовом контуре, её можно выкатывать в продакшен. A/B-тестирование и раскатка на пользователей происходят именно там.

Обычно ML-инженер присылает отчёт по перформансу, а разработчик проверяет его и через UI добавляет модель в prod_revision — продакшен-ревизию в feature-meta-store.

Вот как выглядит итоговая схема:

В центре — сервис feature-meta-store и две ключевые сущности:

  • факторы;

  • и ревизии (подмножества факторов, привязанные к окружениям).

Для каждого окружения (o2-midway-test, o2-midway) используется своя ревизия и подмножество факторов.

Снизу — клиенты feature-meta-store: ml-pipeline, UI.

Слева — блок overload, который подаёт нагрузку на тестовый контур для оценки перформанса моделей.

Эпилог

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

Также мы рассмотрели ключевые особенности вычисления факторов ранжирования в поиске Ozon:

  • DAG и декларативный код создания фактора в o2-midway.

  • Типовые факторы без релизов и нового кода с помощью feature-meta-store.

  • Репорты по качеству и перформансу ещё до продакшена для каждой новой ML-модели с помощью шага в ml-pipeline.

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

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

Read More

LEAVE A REPLY

Please enter your comment!
Please enter your name here