RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Пайплайн Retrieval →

Дополнение · X-алгоритм · страница 3 из 11

Источники: откуда вообще берутся посты

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

Коротко

  • Главное деление — по подпискам. Посты тех, на кого вы подписаны, ищутся принципиально иначе, чем посты тех, на кого не подписаны.
  • Thunder держит свежие посты прямо в оперативной памяти, разложенные по авторам. Никакого поиска: получить посты подписок — это пройтись по списку подписок и взять готовые очереди.
  • SimClusters кластеризует социальный граф на 145 тысяч сообществ разреженной бинарной факторизацией, а потом ищет кандидатов в пространстве кластеров.
  • У каждого источника свой лимит: retrieval отдаёт до 1000 постов, Thunder — до 1200, остальные меньше. Лимиты — это распределение бюджета между способами поиска.
  • Ни один источник не заменяет остальные, потому что каждый видит своё подмножество и слеп к остальному. Это тот же аргумент, что и в многомодальном поиске.

1. Почему источников несколько

Вопрос законный: если есть обученная модель, которая умеет находить релевантные посты, зачем к ней что-то ещё?

Ответ проще, чем кажется: у каждого способа поиска своя слепая зона, и они не пересекаются.

ИсточникЧто находитЧего не видит
ThunderВсё, что опубликовали ваши подписки за последние часыВообще ничего за пределами подписок
Retrieval-модельПосты, похожие на то, что вам заходилоСовсем свежие посты, не попавшие в индекс; темы, по которым у вас нет истории
SimClustersПосты, популярные в сообществах, к которым вы принадлежитеНишевое и новое: кластеры пересчитываются раз в неделю
Тематические источникиПосты по темам, на которые вы подписались явноВсё, что вне названных тем
Кеш отранжированногоТо, что уже посчитали в прошлый разВсё новое с момента кеширования

Особенно показательна вторая строка. Модель retrieval прекрасна, но её индекс, как мы разбирали на странице про retrieval, обновляется только при сохранении чекпоинта. Пост, опубликованный пять минут назад, в нём физически отсутствует — а именно такие посты и составляют ленту соцсети.

Это тот же довод, что и многомодальный поиск

в главе «Виды кандидатогенераторов» мы говорили, что кандидатогенераторы комбинируют не от бедности, а потому что каждый оптимизирован под свой тип связи: коллаборативный ловит «похожие люди смотрели», контентный — «похоже по содержанию», популярный — «это просто хорошо».

Здесь то же самое, но ось другая — по свежести и по типу связи одновременно. Thunder закрывает «свежее и от знакомых», retrieval — «похожее на мои интересы», SimClusters — «популярное в моём сообществе». Убери любой, и появится класс постов, который система перестанет находить в принципе.

2. Thunder: посты подписок в оперативной памяти

Самый прямолинейный и самый поучительный источник. Задача: по списку подписок отдать их свежие посты. Решение: не искать вовсе, а держать всё нужное в памяти, уже разложенным по авторам.

Что именно лежит в памяти

pub struct PostStore {
    posts: Arc<DashMap<i64, Arc<CompactPost>>>,
    original_posts_by_user: Arc<DashMap<i64, VecDeque<TinyPost>>>,
    secondary_posts_by_user: Arc<DashMap<i64, VecDeque<TinyPost>>>,
    video_posts_by_user: Arc<DashMap<i64, VecDeque<TinyPost>>>,
    deleted_posts: Arc<DashMap<i64, bool>>,
    retention_seconds: u64,
    request_timeout: Duration,
}

Фрагмент из post_store.rs · код X, Apache 2.0, коммит 28e414f

Пять структур, и каждая объяснима.

Почему три индекса вместо одного

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

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

Это ровно тот приём, который в главе «Порядок фильтрации» назывался «фильтровать на уровне индекса, а не после выборки». Здесь видно, во что он выливается на практике: не в условие в коде, а в решение о раскладке данных.

Арифметика памяти

TinyPost — это два восьмибайтовых целых, то есть 16 байт. CompactPost — восемь целых и три флага, порядка 72 байт с выравниванием.

Прикинем: пусть в системе 500 миллионов постов за окно хранения. Основная карта — около 36 гигабайт, индексы по авторам — ещё по 8 гигабайт на каждый. Много, но это реально для одного парка машин, и на порядки быстрее любого обращения к диску.

А теперь представим, что мы решили хранить и текст. Средний пост — сотни байт, и оценка вырастает в разы, до сотен гигабайт. Именно поэтому текста в структуре нет: Thunder отвечает на вопрос «какие посты», а не «что в них написано». Текст дотягивается позже, гидратором, и только для тех кандидатов, которые дожили до этой стадии.

Как это наполняется

Thunder подписан на поток событий Kafka: опубликован пост, удалён пост, изменилась видимость. Никаких периодических выгрузок — данные приходят непрерывно, и задержка между публикацией и появлением поста в памяти измеряется секундами.

Старое вычищается по времени хранения: есть фоновая задача trim_old_posts, которая обходит очереди и выбрасывает то, что вышло за окно. Плюс sort_all_user_posts — периодическая пересортировка, потому что события Kafka приходят не строго по порядку.

Почему это не противоречит лямбда-архитектуре

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

Обратите внимание на разделение труда. Thunder знает, какие посты вообще существуют у ваших подписок прямо сейчас. Он ничего не знает о том, какие из них хорошие, — это работа ранжирования. Каждая система решает ровно одну задачу, и именно поэтому обе получаются простыми.

3. SimClusters: кластеризация социального графа

Самый старый и самый концептуально интересный источник. Идея: если разбить пользователей на сообщества по тому, кто за кем следит и кто с кем взаимодействует, то «интересное для вас» можно искать в пространстве сообществ, а не в пространстве постов.

В названиях датасетов зашита конфигурация модели: Model20M145K202020 миллионов пользователей раскладываются по 145 тысячам кластеров.

1. Граф похожести берутся заметные аккаунты, считается похожесть по тому, кто на них подписан 2. KnownFor разреженная бинарная факторизация: автор → сообщества, за которые известен 3. InterestedIn интересы пользователя = сумма сообществ тех, на кого он подписан 4. Поиск кандидатов пост тоже получает вектор над сообществами; ищем ближайшие к вектору интересов Что такое разреженная бинарная факторизация Матрица «пользователь × сообщество» из нулей и единиц: аккаунт либо принадлежит сообществу, либо нет, и принадлежит немногим. Ищется такое разбиение, которое лучше всего восстанавливает граф похожести. Пересчитывается раз в неделю Отсюда сильные и слабые стороны: сообщества устойчивы и интерпретируемы, но новый аккаунт попадёт в них только через неделю, а свежий пост получает вектор через свежие взаимодействия.
Четыре шага: граф → сообщества авторов → интересы пользователя → поиск постов в пространстве сообществ.

Разреженная бинарная факторизация

В коде метод называется SBF, и в его основе лежит SparseBinaryMatrix. Разберём, чем это отличается от привычной матричной факторизации из главы «Матричная факторизация».

Обычная факторизацияРазреженная бинарная
Значения фактороввещественные числанули и единицы
Сколько ненулевыхвсе \(d\) координатнемного из 145 тысяч
Смысл координатынепонятенконкретное сообщество
Что раскладываетсяматрица взаимодействийграф похожести аккаунтов

Ключевое отличие — интерпретируемость. Координата обычного эмбеддинга не означает ничего; про неё нельзя сказать «это про астрономию». Кластер SimClusters означает конкретное сообщество, у него есть состав, его можно посмотреть глазами, ему можно дать имя, на него можно пожаловаться.

Второе отличие — разреженность. Аккаунт принадлежит нескольким сообществам из 145 тысяч, а не имеет 145 тысяч слабых весов. Это даёт и экономию, и инвертированный индекс: по сообществу можно быстро получить всех, кто в него входит.

Зачем это нужно, если есть нейросетевой retrieval

Вопрос естественный: SimClusters — технология прошлого десятилетия, а рядом стоит трансформер. Три причины, по которым её не выкинули.

  1. Она видит другое. Retrieval обучен на ваших лайках и находит похожее на то, что вам заходило. SimClusters опирается на структуру социального графа: сообщество, к которому вы принадлежите, определяется тем, на кого вы подписаны, а не тем, что вы лайкали. Это разные сигналы, и они расходятся чаще, чем кажется.
  2. Она объяснима. «Этот пост популярен в сообществе, к которому вы относитесь» — фраза, которую можно показать пользователю и предъявить регулятору. «Скалярное произведение эмбеддингов равно 0.83» — нельзя.
  3. Она устойчива. Кластеры пересчитываются раз в неделю и меняются медленно. Модель переобучается чаще и может резко изменить поведение. Иметь в системе источник, который двигается медленно и предсказуемо, — это страховка.

Заодно это иллюстрация мысли из главы «Как проектировать систему с нуля»: в проде рядом живут технологии разных поколений, и это нормально. Старое не выкидывают, пока оно закрывает свою нишу.

4. Лимиты источников: распределение бюджета

У каждого источника в конфигурации стоит потолок на число возвращаемых постов.

ИсточникМаксимум постовКомментарий
Thunder1200Больше всех: посты подписок дёшевы и отдаются из памяти
Phoenix retrieval1000Основной источник постов за пределами подписок
Tweet Mixer800
Phoenix MOE200Отдельная разновидность retrieval

Эти числа — не технические ограничения, а продуктовое решение, выраженное в конфиге. Они задают, из какого материала система вообще может собрать ленту.

Прикиньте пропорцию: 1200 постов от подписок против примерно 2000 из всех прочих источников. Если бы Thunder отдавал 5000, а retrieval 200, лента была бы принципиально другой — почти целиком из подписок, — при абсолютно тех же весах ранжирования и той же модели.

Где обычно ищут не там

Когда спрашивают «почему лента показывает мне столько незнакомых аккаунтов», инстинктивно смотрят на веса действий и на дисконт за пределы подписок. Но лимиты источников влияют на состав ленты раньше и сильнее: то, что не попало в кандидаты, не может быть отранжировано никак.

Это общее правило многостадийных систем из главы «Многостадийная воронка»: потолок задаёт первая стадия. Виджет воронки там показывает то же самое численно — сквозная полнота есть произведение полнот стадий, и потери кандидатогенерации не отыгрывает никакой ранкер.

5. Кеш отранжированного как источник

Отдельного внимания заслуживает CachedPostsSource. Он возвращает посты, уже отранжированные в предыдущем запросе.

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

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

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

Частые ошибки и подводные камни

На чём спотыкаются
  • Считать, что нейросетевой retrieval делает остальные источники ненужными. Его индекс обновляется при сохранении чекпоинта, поэтому совсем свежих постов в нём нет. А в ленте соцсети именно они и составляют основной материал.
  • Думать, что Thunder — это база данных. Он не отвечает на произвольные запросы. Он умеет одно: по автору отдать его недавние посты. Всё остальное вынесено наружу.
  • Хранить текст там, где нужны только идентификаторы. В структуре Thunder текста нет вовсе — это разница между десятками и сотнями гигабайт памяти.
  • Не замечать лимиты источников. Состав ленты определяется ими раньше, чем весами: непопавший кандидат не может быть отранжирован никак.
  • Считать SimClusters устаревшим. Он опирается на структуру графа, а не на историю лайков; даёт объяснимость и меняется медленно. Это отдельная ниша, а не отставшая версия retrieval.

Вопросы с собеседований

Зачем в системе несколько кандидатогенераторов, если один из них — обученная модель?

У каждого своя слепая зона. Модель retrieval ищет похожее на историю пользователя, но её индекс обновляется только при сохранении чекпоинта — совсем свежих постов там нет. Источник постов подписок видит только подписки. Кластерный источник опирается на структуру графа и не знает про новое, потому что кластеры пересчитываются раз в неделю.

Убрав любой, получаем класс постов, который система перестаёт находить в принципе. Это ровно тот довод про многомодальный поиск, что и в главе «Виды кандидатогенераторов»: разные генераторы оптимизированы под разные типы связи, и объединение покрывает больше, чем лучший из них.

Как отдавать посты подписок за единицы миллисекунд?

Не искать, а держать в памяти, разложенным по авторам. В разобранной системе это отдельный сервис: карта «идентификатор поста → компактная запись» плюс индексы «автор → очередь его недавних постов». Запрос сводится к обходу списка подписок и чтению готовых очередей.

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

Наполняется это потоком событий, а не периодической выгрузкой: задержка от публикации до появления в памяти — секунды.

Что такое SimClusters и чем он отличается от матричной факторизации?

Это кластеризация социального графа: 20 миллионов заметных аккаунтов раскладываются по 145 тысячам сообществ разреженной бинарной факторизацией графа похожести. Автор получает набор сообществ, за которые он известен; пользователь — набор сообществ, которыми интересуется, как сумму по тем, на кого подписан; пост — вектор над сообществами.

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

Зачем держать в системе источник, пересчитываемый раз в неделю?

Ради устойчивости и объяснимости. Медленно меняющийся источник — страховка на случай, когда модель после переобучения начинает вести себя неожиданно: часть ленты остаётся предсказуемой.

Объяснимость важна не меньше: «этот пост популярен в сообществе, к которому вы относитесь» можно показать пользователю и предъявить регулятору, а «скалярное произведение эмбеддингов 0.83» — нельзя.

Цена — недельная задержка: новый аккаунт попадёт в кластеры не сразу. Поэтому источник и не единственный.

Можно ли переиспользовать посчитанный скор при следующем запросе?

Только если скор зависит от пары «пользователь и пост» и ни от чего больше. В разобранной системе это обеспечено маской внимания: кандидаты не видят друг друга, поэтому скор не зависит от состава батча. Благодаря этому в пайплайне есть отдельный источник, отдающий уже отранжированные посты из кеша.

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

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

Шпаргалка одним экраном

Главное деление

Внутри подписок и за их пределами. Ищутся принципиально разными способами.

Thunder

Свежие посты подписок в оперативной памяти, разложенные по авторам. Поток из Kafka.

Компактность

Без текста: 11 полей фиксированного размера. Текст дотягивается позже и только выжившим.

Три индекса

Оригинальные, вторичные, видео. Фильтрация как выбор структуры данных.

SimClusters

20 млн аккаунтов → 145 тыс. сообществ разреженной бинарной факторизацией графа.

KnownFor / InterestedIn

Автор известен за сообщества; интересы пользователя — сумма по подпискам.

Лимиты

Thunder 1200, retrieval 1000, Tweet Mixer 800, MOE 200. Это продуктовое решение.

Кеш как источник

Возможен только благодаря изоляции кандидатов в модели.

Почему не один источник

У каждого своя слепая зона: свежесть, история, структура графа.

Первоисточники