Архитектура продакшн-рекомендера

Урок собирает изученное в единый промышленный конвейер: генерация кандидатов, ранжирование, бизнес-правила и real-time обслуживание.

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

Зачем нужны ступени

Прогнать дорогую модель ранжирования по всем миллионам товаров на каждый запрос невозможно — не хватит ни времени, ни ресурсов. Поэтому работу делят на этапы: сначала дёшево и грубо отбирают кандидатов, потом дорого и точно ранжируют немногих. Это классическая воронка retrieval → ranking, на которой стоят рекомендатели YouTube, TikTok и крупных маркетплейсов.

миллионы товаров
      |
  [ 1. Генерация кандидатов ]   быстро, грубо: CF, two-tower (ANN), популярное
      |  ~сотни кандидатов
  [ 2. Ранжирование ]           дорого, точно: бустинг/нейросеть на богатых признаках
      |  ~десятки
  [ 3. Бизнес-правила ]         дедуп, разнообразие, фильтры, буст/пессимизация
      |  ~10
   выдача пользователю

Этап 1: генерация кандидатов

Несколько источников параллельно поставляют кандидатов: ANN-поиск по эмбеддингам two-tower, item-based CF от недавней истории, «вместе покупают», популярное, свежее. Их объединяют. Задача — высокая полнота (не упустить хорошее) при низкой стоимости; точность тут вторична.

Этап 2: ранжирование

На сотнях кандидатов работает точная модель ранжирования (LTR-бустинг или нейросеть) с богатыми признаками пользователя, товара, пары и контекста. Она расставляет финальный порядок, оптимизируя ранжирующую метрику.

Этап 3: бизнес-правила и переранжирование

Поверх «чистого» ранжирования накладывают правила реального мира: убрать уже купленное и дубли, обеспечить разнообразие (не десять товаров одного бренда), скрыть товары не в наличии, учесть промо и контрактные обязательства, применить фильтры безопасности.

def apply_business_rules(ranked, in_stock, already_bought, max_per_brand=2):
    brand_count = {}
    result = []
    for item in ranked:
        name, brand = item["name"], item["brand"]
        if name in already_bought or name not in in_stock:
            continue
        if brand_count.get(brand, 0) >= max_per_brand:
            continue
        brand_count[brand] = brand_count.get(brand, 0) + 1
        result.append(name)
    return result

ranked = [
    {"name": "A", "brand": "X"}, {"name": "B", "brand": "X"},
    {"name": "C", "brand": "X"}, {"name": "D", "brand": "Y"},
    {"name": "E", "brand": "Z"},
]
print(apply_business_rules(
    ranked, in_stock={"A","B","C","D","E"},
    already_bought={"A"}, max_per_brand=2))

Вывод:

['B', 'C', 'D', 'E']

«A» убран как уже купленный; третий товар бренда X отсечён лимитом на бренд — выдача стала разнообразнее.

Real-time слой

Тяжёлые вещи считают офлайн (эмбеддинги товаров, item-item похожести, признаки), а онлайн остаётся быстрая сборка: взять свежий контекст пользователя, дёрнуть ANN-индекс, прогнать ранжирование, наложить правила — всё за десятки миллисекунд. Свежие действия (последний клик минуту назад) подмешивают через быстрый стрим, чтобы лента реагировала мгновенно.

Как работает под капотом

Эмбеддинги и индексы пересчитывают по расписанию (батч), фичи кешируют в быстром хранилище, модель ранжирования обновляют регулярно. Архитектура lambda/kappa разделяет батч-слой (точный, медленный) и стрим-слой (свежий, лёгкий). Мониторинг следит за задержкой, покрытием и метриками качества в реальном времени.

Частые ошибки

  • Один этап вместо воронки. Точная модель по всему каталогу не уложится в задержку; нужны retrieval и ranking раздельно.
  • Бизнес-правила внутри модели. Жёсткие правила (наличие, дедуп) проще и надёжнее как отдельный пост-слой.
  • Только батч без real-time. Лента, не реагирующая на свежие действия, ощущается мёртвой.

Итоги

  • Продакшн-рекомендер — воронка: кандидаты → ранжирование → бизнес-правила.
  • Генерация кандидатов дёшева и полна; ранжирование дорого и точно.
  • Бизнес-правила и разнообразие накладывают отдельным пост-слоем.
  • Тяжёлое считают офлайн, а онлайн — быстрая сборка с учётом свежего контекста.
Проверьте себя
1. Зачем продакшн-рекомендер разбивают на этапы «кандидаты → ранжирование»?
AДля красоты архитектуры
BТочная модель не может работать по всем миллионам товаров на каждый запрос, поэтому сначала дёшево отбирают, потом дорого ранжируют немногих
CЧтобы не использовать эмбеддинги
DЧтобы убрать бизнес-правила
2. Где правильнее всего применять жёсткие бизнес-правила (наличие, дедуп, лимит на бренд)?
AВнутри модели ранжирования как признаки
BОтдельным пост-слоем после ранжирования
CНа этапе генерации кандидатов вместо отбора
DИх вообще не применяют