
Привет, Хабр! Меня зовут Лев. Я работаю в поиске 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 — относительная цена;
Mс — 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_ts–now.

На выходе получаем время доставки.
Разработчик фактора ранжирования не описывает, как именно всё будет вычисляться. Он задаёт, что нужно получить. Это сильно упрощает разработку и позволяет делать оптимизации, не меняя код вычисления факторов.
Итог:
Факторы ранжирования отлично ложатся на структуру направленного ациклического графа (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, чтобы фактор ранжирования попал в граф и мог быть вычислен.
Примеры, где проблема необходимости релиза для создания нового фактора особенно проявляется:
Статистики из хранилища. Например, вес, рейтинг, цена. Логика у них похожая, как в примерах выше: сходить в
featureStoreServiceи забрать нужное поле поitem_id.Функции от других факторов. Например: относительная цена = цена / средняя цена.
Новые ML-модели.
По сути, такие факторы ранжирования могут описываться конфигами с различающимися параметрами от фактора к фактору. Мы приняли решение вынести эти конфиги в отдельный сервис — feature-meta-store. feature-meta-store позволяет добавлять типовые факторы ранжирования без ручного кода.
Прорастание типовых факторов из feature-meta-store в граф на o2-midway выглядит так:
o2-midway запрашивает все доступные факторы из feature-meta-store.
Проходит циклом
forпо каждому полученному фактору.Для каждого делает
graph.push, где указывает название фактора, входные параметры, которые тоже берутся из метаинформации, и описывает, как вычислить результат.

Таким образом, мы получаем типовые факторы из feature-meta-store и автоматически добавляем их в граф. В результате нам больше не нужно писать шаблонный код и пересобирать o2-midway, например, для каждой новой ML-модели.
Под капотом у feature-meta-store
Теперь посмотрим, как устроена система feature-meta-store и какая у неё доменная модель.
Первая структура данных внутри — это таблицы с метаинформацией о факторах ранжирования. Для каждого типа фактора используется своя таблица. Ниже рассмотрим пример для трёх таких типов:
Свойства по
item_id— обычные статистики из хранилища. Здесь достаточно метаинформации о названии поля (цена, заказы).ML-модели — здесь нужна информация о входных факторах и название исполняемого файла, например, бинарника CatBoost.
Функции — математические формулы, например: крутость = цена заказы.

Вторая важная часть доменной модели 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. Это позволяет создавать функции ранжирования без релизов и ручного кода.
Но вместе с этим подходом появляется и риск. Например, можно случайно добавить в прод медленную модель, которая будет тормозить работу сервиса. Чтобы этого избежать, мы встроили защиту:
Клиенты могут добавлять факторы только в тестовый контур (тестовую ревизию).
В 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 быстро и безопасно.
Надеюсь, вам было интересно и полезно это прочитать, с нетерпением жду обсуждения в комментариях.


