Архитектура продакшн-рекомендера
Урок собирает изученное в единый промышленный конвейер: генерация кандидатов, ранжирование, бизнес-правила и 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. Лента, не реагирующая на свежие действия, ощущается мёртвой.
Итоги
- Продакшн-рекомендер — воронка: кандидаты → ранжирование → бизнес-правила.
- Генерация кандидатов дёшева и полна; ранжирование дорого и точно.
- Бизнес-правила и разнообразие накладывают отдельным пост-слоем.
- Тяжёлое считают офлайн, а онлайн — быстрая сборка с учётом свежего контекста.