В AppMonet мы ежедневно предоставляем самое интересное видео и показываем контент миллионам пользователей. Рекомендация контента лежит в основе стека. Благодаря нашему уникальному положению в экосистеме мы создали систему ранжирования контента, адаптированную к нашим вариантам использования.

Наш сценарий

Несколько интересных характеристик наших вариантов использования:

  • Половина нашего контента чувствительна ко времени. Другая половина — «вечнозеленые» и остаются актуальными до 3 месяцев.
  • Мы хотим предоставлять пользователям максимально свежий контент каждый раз, когда они посещают сайт, смешивая вечнозеленый контент и контент, зависящий от времени.
  • Мы стараемся не показывать пользователю один и тот же контент дважды.
  • Для вечнозеленого контента каждый день есть только несколько очень хороших. Мы хотим убедиться, что пользователь ничего не пропустил с момента последнего посещения.

Различия между нами и другими платформами

  • Reddit — Reddit полагается на то, что пользователи переключаются между «верхним» и «горячим» режимом. Ни один из них не подходит для нашего случая смешивания чувствительного ко времени контента и вечнозеленого контента.
  • Netflix — Netflix больше ориентирован на персонализацию на основе интересов, и весь их контент всегда актуален (кстати, о тех старых фильмах!)
  • Лента Instagram/Facebook — Instagram может быть наиболее похожим. Если пользователь не посещал сайт в течение нескольких дней, Instagram выберет лучший контент за последние 3 дня. Однако, поскольку в Instagram намного больше контента, чем у нас, а издатели сами периодически публикуют вечнозеленый контент, они могут оглянуться назад только на 3 дня. Нам нужно оглянуться назад на 3 месяца.

Наше решение

Шаг 1. Создайте список самого популярного контента

Алгоритм оценки здесь учитывает каждую часть контента:

  • рейтинг кликов
  • проведенное время
  • реакция в соцсетях (если есть)
  • Свежесть: для контента, чувствительного к времени, свежесть постепенно падает ниже 1 по мере старения контента.

Ниже приведен упрощенный псевдокод конвейера данных для создания списка верхнего содержимого:

Шаг 2. Отслеживайте просматриваемый контент пользователем

Это сложный шаг. Для каждого пользователя, которого видели за последние N дней (где N = 60), мы хотим предоставить им самый популярный контент за вычетом всего, что они видели за последние N дней.

Для этого нам нужен простой способ поиска того, что видел пользователь. Вот требования:

  • быть очень быстрым, чтобы запросить
  • быть точным, по крайней мере, с момента последнего посещения пользователем (поскольку они могут посетить в любое время, это должно быть очень близко к текущему состоянию)
  • легко масштабируется (обслуживает миллионы пользователей)

Вот несколько вариантов, которые мы рассмотрели:

Вариант 1: Cloudflare Workers + KV

  • за: простой + масштабируемый.
  • Минусы: мы мало что знаем о его долговечности + не можем его контролировать (резервное копирование и т. д.), поэтому нам придется делать периодическое резервное копирование. Непонятно, как мы с этим справимся

Вариант 2. openresty (nginx) + redis

  • pro: просто + быстро, делал это раньше и работал нормально
  • pro: у нас уже есть экземпляр «монстр». Для лучшей производительности можно запустить Redis в доменном сокете с nginx на той же машине, вероятно, может обрабатывать ~ 50 000 одновременных запросов.
  • минус: приходится самим управлять инфраструктурой
  • минус: масштабирование за пределы 1 машины немного неизвестно. Мы могли бы сгруппировать Redis и поставить nginx за LB.

Вариант 3. kinesis + lambda + redis / PG
Это позволит использовать наш существующий конвейер событий для записи нового типа события в kinesis. Cleaner поместит его в новый поток, который будет читаться лямбдой. Эта лямбда записывала пакетные события в Redis, PG или даже в конвейер 🙂

  • pro: использует существующую инфраструктуру конвейера событий
  • за: всегда можно изменить уровень БД (например, начать с Redis и переключиться на конвейер)
  • pro: все компоненты размещены (если мы используем elasticache или RDS)
  • минус: нужно написать собственный лямбда-код
  • минус: стоит больше, чем рабочие CF + KV

Основываясь на нашем прошлом опыте, кажется, что вариант 3 является лучшим, поскольку он имеет наименьшее количество неизвестных и лучше всего задокументирован (все еще обеспокоен тем, что у рабочего KV есть проблемы, о которых мы пока не знаем)

Шаг 3. Вычтите просмотренный контент из списка самого популярного контента каждого пользователя

Этот шаг будет просто выполняться после шагов 1 и 2 для каждого пользователя в полночь.

Последовательность кода:

Ежедневно в полночь:

  1. (один раз) Создавать список самого популярного контента, top_content каждый день (на основе последних 30 дней)
  2. (один раз) Сгенерировать список нового контента new_content (контент, где score = null и posts_at › now() — интервал «24 часа»)
  3. Для каждого пользователя получаем user_list = top_content — user_seen_last_30
  4. Для каждого пользователя сделайте mix_list = f(last_seen_at, shuffle(new_content)[0..K]) + user_list
  5. Для каждого пользователя complete_list = space_items_by_category(mixed_list)
  6. Сохраните этот complete_list, чтобы мы могли прочитать его в API!

Будущие улучшения

Что мы хотим сделать в будущем, но не сегодня:

  • Настройка на основе интересов — для сегодняшней простой версии мы предполагаем, что лучший контент может быть интересен всем пользователям. Мы не даем никаких рекомендаций на основе интересов пользователей.
  • Машинное обучение для системы подсчета очков. Сегодня система оценки контента ручная и упрощенная. Низко висящим плодом будет линейная регрессия весов функций контента.