← к модулям

Системный дизайн — двухстадийная воронка

Как устроены промышленные рекомендации: retrieval → ranking → re-rank. Ниже — живая воронка на наших моделях: дёшево достаём кандидатов из нескольких источников, переранжируем их NeuMF, диверсифицируем через MMR. Крути ручки — пул пересчитывается.

⚠ Тестовая версия на маленьком каталоге (9724 фильма): retrieval — точный перебор, и он честно быстрее ANN. Урок — в форме воронки, а латентность иллюстративна. ANN-поиск и feature-rich ranking — отдельные следующие фазы.

Источники retrieval
200
200
100

прогоняем запрос через воронку…

📊 Диагностика воронки по стадиям (по всему тесту)

Прогоняет текущий конфиг воронки по всему тесту и считает метрики по стадиям. Главное — recall-потолок: hit-rate ранкинга не может превысить recall ретривала.

Теория простым языком

Главная ментальная модель промышленных рекомендаций — воронка: retrieval → ranking → re-rank. Нельзя прогнать тяжёлую модель по миллионам айтемов на каждый запрос, поэтому сначала дёшево достаём сотни кандидатов, потом дорого переранжируем десятки, потом накладываем бизнес-правила. Выше — эта воронка вживую на наших моделях.

Почему нельзя одной моделью

Пусть на ранжирование одного айтема уходит 1 мс. Для каталога в 10 млн это 10 000 секунд на запрос — нереально. Значит нужен дешёвый первый этап, который отбросит 99.99% каталога, и только остаток отдаст тяжёлой модели.

Почему появилась многостадийная воронка
БылоМожно было скорить весь каталог одной моделью.
ПроблемаВ проде каталог огромный — тяжёлый ranker нельзя прогнать по миллионам айтемов на запрос.
ИдеяRetrieval быстро собирает кандидатов, ranker точно сортирует, re-rank накладывает diversity/бизнес-правила.
Стало лучшеМасштабируемость, контроль latency, гибкость (источники и правила меняются независимо).
Осталось слабымRetrieval ceiling, train/serving skew, сложнее отлаживать (ошибка может быть на любой стадии).
ДальшеANN, feature store, monitoring; богатый ранкер на реальных данных — feature-rich ranking (D2b).

Этап 1 — Retrieval (candidate generation)

Retrievalдёшево отобрать сотни-тысячи кандидатов из всего каталога. Оптимизируем recall (не потерять хорошее), а не точный порядок. Часто это несколько источников сразу: эмбеддинговый (two-tower), co-visitation (item-CF), популярное, свежее.

У нас retrieval — это объединение кандидатов от two-tower, item-CF и popularity (union + dedup). Каждый кандидат помнит происхождение (provenance): какой источник его предложил и с каким сырым/нормированным скором. Порядок на этом этапе ещё не финальный — его задаёт ранкер.

Пример. Кликни «показать кандидатов» на стадии Retrieval: один и тот же фильм часто приходит сразу из нескольких источников (бэйджи tt/cf/pop). Выключи источник слева — пул кандидатов меняется на глазах.

Этап 2 — Ranking

Rankingдорогая модель переоценивает уже отобранные кандидаты и старается лучше упорядочить верх списка под целевую метрику: CTR, watch-time, CVR, rating relevance, NDCG/precision или прямую бизнес-цель (expected value, multi-task score). В проде это редко «просто precision».

У нас ранкер — NeuMF: он переоценивает объединённый пул по парам (пользователь, кандидат) и оставляет top-KrankK_{rank}. Кандидаты ниже отсечки помечаются как below_rank_k — видно в раскрытой стадии.

Пример. Recall-потолок вживую. Если спрятанный (held-out) фильм не попал в 291291 кандидата на retrieval, то максимальный hit@10\text{hit}@10 после ranking = 0 — ранкер не вернёт то, чего ему не дали. В нашем замере retrieval-recall ≈ 0.52, а hit-rate ранкинга ≈ 0.06: потолок высоко, но из доставленного в топ-10 попадает мало. Нажми «🎯 Спрятанный фильм» в трассе — увидишь это для конкретного пользователя.

Этап 3 — Re-rank / бизнес-правила

Голый топ ранкера часто «слипшийся»: пять почти одинаковых боевиков подряд. Этап re-rank накладывает правила: диверсификация, кап на категорию, свежесть, дедуп, бизнес-ограничения.

Почему top по score не всегда лучший top
БылоRanker сортировал айтемы строго по предсказанной релевантности.
ПроблемаВерх может быть однообразным, слишком популярным или нарушать бизнес-ограничения.
ИдеяRe-rank оптимизирует не только relevance, но и diversity, constraints, fairness, свежесть.
Стало лучшеБолее здоровая выдача и контроль продуктовых рисков.
Осталось слабымTrade-off с точностью, тонкая настройка λ/капов, сложнее оценивать эффект офлайн.
ДальшеMulti-objective оптимизация и online guardrails (в следующих модулях).
MMR (Maximal Marginal Relevance)жадно набираем список, балансируя релевантность и непохожесть на уже выбранное: каждый следующий айтем максимизирует λrel(1λ)maxjSsim(i,j)\lambda\cdot rel - (1-\lambda)\cdot \max_{j\in S} sim(i,j).
Формула MMR и как мы её считаем

На каждом шаге из оставшихся кандидатов выбираем

i=argmaxiS[λrel(i)(1λ)maxjSsim(i,j)]i^\star = \arg\max_{i\notin S}\Big[\lambda\cdot rel(i) - (1-\lambda)\max_{j\in S} sim(i,j)\Big]

relrel — скор ранкера (min-max-нормированный), simsim — косинус в пространстве замороженных two-tower эмбеддингов, SS — уже выбранные. λ=1\lambda=1 → чистый порядок ранкера; λ=0\lambda=0 → максимум разнообразия. Подвигай ползунок MMR выше — на малых λ в финале растёт число разных жанров.

Псевдокод воронки
# на запрос (user u)
cands = ∅
для каждого источника s:               # RETRIEVAL
    cands ∪= s.retrieve(u, n_s)         # union + dedup, помним provenance

scored = ranker.score(u, cands)         # RANKING (NeuMF по парам)
top = top_k(scored, K_rank)

S = []                                   # RE-RANK (MMR + кап жанра)
пока |S| < k и есть кандидаты:
    i* = argmax λ·rel(i) − (1−λ)·max sim(i, S)
    если жанр(i*) не превысил кап: S.append(i*)
вернуть S

Где что считается: online / near-line / offline

Offlineтяжёлые пакетные расчёты заранее: обучение моделей, предсчёт item-эмбеддингов, построение ANN-индекса. Часы, по расписанию.
Near-lineреакция на события за секунды-минуты: обновить недавнюю историю пользователя, пересчитать фичи. Между offline и online.
Onlineто, что считается на сам запрос за миллисекунды: вектор пользователя, ANN-поиск, ранжирование сотни кандидатов. Жёсткий latency-бюджет (десятки мс).
Feature storeединое хранилище фич с согласованностью online/offline: обучали и применяем на одних и тех же признаках (иначе train/serving skew). Примеры: Feast, Tecton.

Кэширование держит бюджет: item-эмбеддинги предсчитаны, недавняя выдача и фичи пользователя кэшируются. Холодный старт решается на уровне системы: новому пользователю — popularity/контентный retrieval, пока не накопится история.

Честный замер: двухстадийность vs одна стадия

Сравнили на нашем тесте (K=10) одностадийный two-tower против воронки. Цифры — как есть:

Таблица: precision / ndcg / hit_rate / coverage / diversity
test split, K=10
модель                       prec    ndcg   hit    cover  diver
single-stage two-tower      0.0052  0.0282  0.052  0.043  0.781
two-stage (tt→NeuMF→MMR)    0.0058  0.0274  0.058  0.034  0.802
two-stage multi-source      0.0061  0.0291  0.061  0.032  0.800

Двухстадийность чуть поднимает precision / hit-rate / разнообразие, но роняет coverage (ранкер + MMR концентрируют выдачу). NDCG почти не меняется — NeuMF у нас ≈ baseline. Мульти-источник retrieval помогает заметнее всего. Урок не «стало лучше по всем метрикам», а в самой архитектуре и её компромиссах.

Пример. Важная оговорка про масштаб. У нас 9724 фильма, поэтому retrieval — точный перебор, и он честно быстрее ANN. Латентность стадий тут доли мс — урок в форме воронки (пул сжимается), а не в абсолютных числах. ANN — в модуле B3. И ranking у нас — это CF-модель (NeuMF); настоящий ranking богат фичами (user×item×контекст) — это Фаза 8.

Что упрощено в этой лаборатории

⚠️ Что может пойти не так

  • Нет real impression logs: MovieLens — это ratings, а не показы/клики, поэтому нет настоящего CTR-сигнала и position bias.
  • Нет feature-rich ranker: ranking пока NeuMF (id-эмбеддинги), а не модель на user/item/context-фичах (это Фаза 8).
  • Нет ANN: каталог мал (9724) → точный перебор, и он честно быстрее приближённого поиска (ANN — отдельная фаза).
  • Нет online serving: latency здесь иллюстративная, нет реального latency-бюджета и инфраструктуры.
  • Нет A/B: всё сравнение offline на leave-last-out, без онлайн-эксперимента.

Сильные стороны

  • Масштаб: дешёвый retrieval отбрасывает 99.99% каталога, тяжёлая модель видит сотни, а не миллионы.
  • Гибкость: несколько источников retrieval (эмбеддинги + co-visitation + свежее) дают recall.
  • Управляемость: бизнес-правила и диверсификация живут в отдельном re-rank-этапе, не ломая модель.

Слабые стороны

  • Сложность: несколько моделей, индексов и слоёв (online/near-line/offline) — много движущихся частей.
  • Recall-потолок: что retrieval не достал, ranking уже не вернёт.
  • Компромиссы метрик: диверсификация и концентрация ранкера могут ронять coverage (видели в замере).

⚠️ Что может пойти не так

  • Retrieval — это recall, а не порядок: оценивать первый этап по NDCG некорректно (его дело — не потерять хорошее).
  • Точность ≠ бизнес: двухстадийность может улучшить precision, но уронить coverage/новизну — смотри на компромисс.
  • Train/serving skew: фичи на обучении и на инференсе должны совпадать — для этого feature store.
  • ANN — приближённый: за скорость платим recall@k; на маленьком каталоге ANN не нужен (у нас точный перебор).
  • Latency-бюджет реален: каждый слой ест миллисекунды; кэш и предсчёт эмбеддингов — не роскошь.

🧠 Проверь себя: Зачем нужен отдельный дешёвый этап retrieval, если есть точная ranking-модель?

🧠 Проверь себя: Почему нельзя оценивать retrieval по NDCG@10?

Где встречается в жизни

Эта воронка — стандарт индустрии: YouTube (Covington 2016: deep retrieval → deep ranking), ленты соцсетей, маркетплейсы, реклама. Дальше по нашей дорожной карте: ANN-поиск (FAISS/HNSW/ScaNN) отдельной фазой и feature-rich ranking (LambdaMART, DCN-v2) в Фазе 8.