Системный дизайн — двухстадийная воронка
Как устроены промышленные рекомендации: retrieval → ranking → re-rank. Ниже — живая воронка на наших моделях: дёшево достаём кандидатов из нескольких источников, переранжируем их NeuMF, диверсифицируем через MMR. Крути ручки — пул пересчитывается.
⚠ Тестовая версия на маленьком каталоге (9724 фильма): retrieval — точный перебор, и он честно быстрее ANN. Урок — в форме воронки, а латентность иллюстративна. ANN-поиск и feature-rich ranking — отдельные следующие фазы.
прогоняем запрос через воронку…
Прогоняет текущий конфиг воронки по всему тесту и считает метрики по стадиям. Главное — recall-потолок: hit-rate ранкинга не может превысить recall ретривала.
Теория простым языком
Главная ментальная модель промышленных рекомендаций — воронка: retrieval → ranking → re-rank. Нельзя прогнать тяжёлую модель по миллионам айтемов на каждый запрос, поэтому сначала дёшево достаём сотни кандидатов, потом дорого переранжируем десятки, потом накладываем бизнес-правила. Выше — эта воронка вживую на наших моделях.
Почему нельзя одной моделью
Пусть на ранжирование одного айтема уходит 1 мс. Для каталога в 10 млн это 10 000 секунд на запрос — нереально. Значит нужен дешёвый первый этап, который отбросит 99.99% каталога, и только остаток отдаст тяжёлой модели.
▸Почему появилась многостадийная воронка
Этап 1 — Retrieval (candidate generation)
У нас retrieval — это объединение кандидатов от two-tower, item-CF и popularity (union + dedup). Каждый кандидат помнит происхождение (provenance): какой источник его предложил и с каким сырым/нормированным скором. Порядок на этом этапе ещё не финальный — его задаёт ранкер.
Этап 2 — Ranking
У нас ранкер — NeuMF: он переоценивает объединённый пул по парам (пользователь, кандидат) и оставляет top-. Кандидаты ниже отсечки помечаются как below_rank_k — видно в раскрытой стадии.
Этап 3 — Re-rank / бизнес-правила
Голый топ ранкера часто «слипшийся»: пять почти одинаковых боевиков подряд. Этап re-rank накладывает правила: диверсификация, кап на категорию, свежесть, дедуп, бизнес-ограничения.
▸Почему top по score не всегда лучший top
▸Формула MMR и как мы её считаем
На каждом шаге из оставшихся кандидатов выбираем
— скор ранкера (min-max-нормированный), — косинус в пространстве замороженных two-tower эмбеддингов, — уже выбранные. → чистый порядок ранкера; → максимум разнообразия. Подвигай ползунок 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
Кэширование держит бюджет: item-эмбеддинги предсчитаны, недавняя выдача и фичи пользователя кэшируются. Холодный старт решается на уровне системы: новому пользователю — popularity/контентный retrieval, пока не накопится история.
Честный замер: двухстадийность vs одна стадия
Сравнили на нашем тесте (K=10) одностадийный two-tower против воронки. Цифры — как есть:
▸Таблица: precision / ndcg / hit_rate / coverage / diversity
модель 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 помогает заметнее всего. Урок не «стало лучше по всем метрикам», а в самой архитектуре и её компромиссах.
Что упрощено в этой лаборатории
⚠️ Что может пойти не так
- Нет 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.