Через два года мы вырвали сердце SeenWant и переделали его.
Конкретно — мы переделали свайп-движок, систему, которая решает, какой фильм, сериал, книгу или игру показать вам следующей. Старый движок работал нормально. Это было то самое «нормально», которое тихо убивает продукт. Пользователи свайпали. Они не досматривали списки. Они смотрели, как растёт их Want, никогда из него не вытаскивая. Сигнал был хороший, но петля сломана.
Этот пост — что мы узнали, что построили и почему думаем, что большинство алгоритмических лент рекомендаций тихо плохи в способах, которых владельцы не видят.
Оригинальный движок
Наш v1 был прямолинейным. Он тянул кандидатов из четырёх источников:
- Совпадения по жанру — фильмы/сериалы в жанрах, которые пользователь раньше Wanted или Seen.
- Коллаборативная фильтрация — «пользователи, которым понравилось то же, что и вам, также любили...».
- Trending — глобально популярные тайтлы, взвешенные по свежести.
- Cold-start заполнители — для новых пользователей курируемый сет «очевидных» тайтлов для запуска сигнала.
Свайп-колода была взвешенным миксом этих источников. Настраиваемый. Разумный. Стандарт индустрии.
Он работал так, как работает стандарт индустрии: показывал пользователям вещи «достаточно хорошие», никогда не удивляя.
Что мы заметили
Через шесть месяцев после запуска v1 в аналитике пошли странные сигналы:
- Конверсия Want→Seen была низкой. Пользователи Wanted вещи, но не Seen. Want-список стал кладбищем.
- Время-до-первого-Seen для новых пользователей было высоким. Даже после онбординга в среднем уходило две недели, чтобы пометить хоть что-то как реально посмотренное.
- Повторные свайп-сессии были короткими. Пользователи свайпали две минуты, скучали, уходили.
- Power-юзеры были менее вовлечены в рекомендации, чем casual-пользователи. Самые активные игнорировали наши рекомендации полностью, добавляя из внешних источников (Letterboxd-списки, друзья).
У каждого сигнала было объяснение. Сложный месседж был такой: движок проваливал реальную задачу.
Что такое реальная задача
Мы дискутировали внутри несколько месяцев. Задача, под которую мы релизились, была «показывать пользователям то, что им понравится». Реальная задача была другой: «помогать пользователям решать, что потреблять следующим».
Звучит одинаково. Это не одно и то же.
«Показывать пользователям то, что им понравится» оптимизирует swipe-right rate — насколько часто следующая карточка получает Want. Это то, под что оптимизирован каждый рекомендательный движок в развлечениях. Не та цель.
«Помогать пользователям решать, что потреблять следующим» оптимизирует под нечто более сложное: скорость, с которой Want становится Seen. Скорость, с которой случайный вечер становится посмотренным фильмом. Конверсия от интереса к действию.
Когда мы сместились на эту цель, почти всё в движке должно было поменяться.
Пять вещей, которые мы делали неправильно
Оглядываясь на v1, систематические ошибки:
1. Мы оптимизировали под «больше кандидатов»
V1 предполагал, что чем больше опций — тем лучше. Поэтому мы поднимали 30 кандидатов на сессию.
Неправильно. После примерно 12 кандидатов наступала усталость. К 20 качество свайпа падало. К 30 пользователи просто чистили колоду, чтобы закончить.
Фикс v2: ограничить каждую сессию 12 кандидатами. Заставить каждого зарабатывать слот.
2. Мы относились ко всем четырём типам медиа одинаково
Книжная рекомендация работает иначе, чем фильм. Книга — 10-часовой коммит. Фильм — 2-часовой. Игра — потенциально 50-часовой.
V1 показывал все четыре рядом с одним весом. Пользователи Wanted книги с той же скоростью, что и фильмы, но Seeing только 5% книг, которые Wanted.
Фикс v2: взвешивание по типу. Книги требуют намного более сильного сигнала перед рекомендацией. Игры — ещё сильнее. Фильмы — самое быстрое решение; мы более терпимы.
3. Мы доверяли жанровой системе слишком сильно
«Драма» — жанр. И «хоррор». И «триллер». И «психологический триллер». Таксономия — каша, фильмы кросс-теггатся щедро.
V1 использовал жанровое перекрытие как primary-сигнал. Результат: тому, кому понравился медленный корейский фильм, рекомендовали каждый «Drama» тег в базе, включая американские семейные 90-х.
Фикс v2: мы заменили жанровое совпадение на эмбеддинги. Фильмы теперь размещены в выученном similarity-пространстве на основе реального поведения пользователей, не метаданных. Past Lives теперь рекомендуется рядом с Aftersun, а не с The Pursuit of Happyness.
4. Мы не моделировали комнату
Большинство наших пользователей не смотрят одни. Они смотрят с партнёрами, семьями, соседями. Движок относился к каждой рекомендации как будто пользователь — единственная аудитория.
Фикс v2: режим «смотрим вдвоём», который позволяет свайпать с учётом другого профиля. Рекомендации соответствующе подстраиваются. Над этим ещё итерируем; в v2 это фича с самым большим запасом для роста.
5. Мы переоценивали свежесть
Trending-тайтлы были слишком сильно взвешены. Фильм месячной давности всегда выигрывал у фильма 2003-го с равной реальной релевантностью.
Фикс v2: свежесть — небольшой бонус, не множитель. Фильмы любого года могут победить.
Новая архитектура
V2-движок состоит из четырёх главных частей. Стоит быть конкретным, потому что архитектурные решения — это и есть история:
Эмбеддинговое пространство
У каждого тайтла в нашей базе есть эмбеддинг — 384-мерный вектор, который ловит, очень грубо, «что это за фильм». Мы выучили пространство из ко-occurrence данных пользователей: тайтлы, которые часто Wanted одними и теми же пользователями, кластеризуются вместе в пространстве.
Это не новая технология. Что ново для нашего пространства — как эмбеддинги тренируются: на Want-к-Seen переходах, не на одних Want-сигналах. Want без следующего Seen — слабый сигнал. Want, за которым следует 5-звёздное Seen — сильный. Эмбеддинги учатся на сильных сигналах.
Per-user вектор вкуса
У каждого пользователя есть соответствующий вектор вкуса в том же 384-мерном пространстве. Это бегущее взвешенное среднее по их Seen-тайтлам. Обновляется после каждого Seen, не каждого Want.
Это второе ключевое решение: обновляем на Seen, не на Want. Большинство движков обновляются на вовлечённости. Мы — на завершении. Разница огромная.
Выбор кандидатов
Сет кандидатов для сессии — это сет тайтлов ближайших к вектору вкуса пользователя, отфильтрованный по:
- Тому, что пользователь не Wanted, не Seen, не Skipped
- Тому, что доступно на их подключённых стримингах (мы знаем, какие у них есть)
- Тому, что соответствует их настройкам (предпочитаемые языки, контент-фильтры)
Затем мы пере-ранжируем небольшим набором дополнительных сигналов: оценки друзей (если есть), бонус свежести, инъекция разнообразия (не показывать 12 триллеров подряд).
Инъекция разнообразия
Чистый embedding-similarity движок коллапсирует — будет показывать одни и те же 30 фильмов до тепловой смерти Вселенной.
Мы форсируем разнообразие, резервируя 2-3 слота в каждой 12-карточной сессии под off-vector рекомендации. Это тайтлы, в которых движок не уверен. Какие-то садятся плохо. Какие-то — блестяще. Точка — избежать алгоритмического коллапса, который делает большинство ленты рекомендаций стерильными через несколько недель.
Что мы измеряем теперь
Три метрики заменяют старую (swipe-right rate):
- Конверсия Want-к-Seen (W2S). Из фильмов, которые пользователь Wants, какой процент он реально Seen в течение 30 дней? Цель: 35%. Сейчас: 31%. Выросло с 14% до перестройки.
- Время-до-решения в активных сессиях. От «мне скучно» до «всё, смотрю это» — сколько? Цель: до 3 минут. Сейчас: 2:48. Снижено с 4:12.
- Recommendation reuse rate. Когда мы рекомендуем фильм и пользователь Wants его — насколько вероятно, что он вернётся найти его позже? Цель: 70% Want-ов получают повторное вовлечение в 60 дней. Сейчас: 64%.
Эти метрики двигать сложнее, чем swipe-right rate. Это и правильные вещи для измерения.
Что дальше
Несколько экспериментов в работе:
- Mood-conditioned рекомендации. Вместо «что вам нравится?» → «что вы хотите чувствовать прямо сейчас?» Вектор вкуса — один вход; настроение — второй.
- Улучшение холодного старта. Новые пользователи всё ещё проходят 5-10 свайпов до значимой персонализации. Хотим до 3.
- Интеграция графа друзей. Когда друг Sees фильм и оценивает 5 звёзд, это должно немедленно подняться в свайп-колоде следующей сессии. Поднимается, но сила сигнала консервативна; тюним вверх.
Урок
Главное, что мы узнали — не техническое. Это переформулировка.
Рекомендательные движки в основном плохи, потому что оптимизируют не под то — вовлечённость, swipe-rate, время-на-платформе. Эти метрики легко измерять и легко гипноз-играть. Более сложная, более честная метрика — закончил ли пользователь с лучшим опытом. Под это мы пытаемся строить теперь.
Если вы пользовались SeenWant до перестройки — вы почувствуете разницу. Если нет — свайпните и расскажите.
Интересна архитектура подробнее? Мы публикуем продолжение про пайплайн тренировки эмбеддингов. Подпишитесь на RSS, чтобы поймать.


