RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Видимость Конфигурация →

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

Блендинг: лента состоит не только из постов

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

Коротко

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

1. Почему реклама не участвует в ранжировании

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

Три довода против общей шкалы
  1. У величин нет общей единицы. Скор поста — это взвешенная сумма вероятностей действий, число порядка сотых. Ставка рекламодателя — деньги. Коэффициент пересчёта одного в другое не выводится ниоткуда: его придётся назначить, и он же станет главной ручкой всей экономики продукта.
  2. У рекламы есть обязательства. Рекламодателю обещано определённое количество показов, соседство с определённым контентом запрещено договором. Такие ограничения выражаются как правила, а не как слагаемые в скоре: скор можно перебить, обязательство — нет.
  3. Доля рекламы — решение бизнеса, а не предсказание. Сколько её в ленте, определяется стратегией компании. Отдать это модели значит потерять управление: она будет выбирать долю, оптимальную под свою метрику, а не под цели продукта.

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

2. Сколько рекламы поместится

Главная функция блендера читается почти как формула:

let spacing = compute_spacing(&ads);

let safe_count = scored_posts.iter().filter(|p| !has_avoid(p)).count();
let max_from_safe = safe_count / 2;
let expected_from_spacing = n.saturating_sub(1).checked_div(spacing.requested).unwrap_or(0);
let actual_ads = ads.len().min(expected_from_spacing).min(max_from_safe);

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

Количество рекламы — это минимум из трёх ограничений:

ОграничениеОткуда берётсяЧто означает
ads.len()АукционБольше, чем есть, не поставишь
expected_from_spacing\((n-1) / \text{шаг}\)Реклама не должна идти чаще заданного шага
max_from_safe\(\text{безопасных постов} / 2\)Не больше половины безопасного окружения занято рекламой

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

Третье ограничение — самое интересное

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

Ограничение safe_count / 2 означает: сколько бы рекламы ни было куплено и какой бы шаг ни просили, её не может быть больше половины от числа безопасных постов. Прикинем на числах: если в ленте 35 постов и 20 из них безопасны, потолок — 10 реклам, даже если аукцион отдал 30.

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

Заметьте, насколько это сильнее, чем «стараться не ставить рекламу рядом с плохим». Здесь плохой контент уменьшает общее число рекламы, а не только её расстановку.

3. Три блендера на выбор

Реализаций несколько, и переключаются они параметром. По умолчанию работает partition_organic_low_risk.

БлендерПринцип
PartitionOrganicAdsBlenderРазбивает органическую ленту на участки и вставляет рекламу между ними. Работает по умолчанию
SafeGapAdsBlenderСледит за безопасным разрывом между рекламами
TimeGapAdsBlenderСчитает разрыв не в постах, а во времени просмотра: параметры задают целевой интервал в секундах и минимальный органический разрыв

Третий заслуживает комментария. Его параметры — целевой интервал 4 секунды, минимальный органический разрыв 3 поста, ограничения множителя от 0.5 до 2.0 — показывают, что расстояние между рекламами меряется не в позициях, а в предполагаемом времени, которое человек потратит на промежуток.

Логика понятна: три коротких текстовых поста человек пролистывает за секунду, а три видео смотрит минуту. Одинаковый разрыв в позициях даёт совершенно разную частоту рекламы по ощущениям. Меряя во времени, блендер выравнивает именно ощущение.

4. Модули на фиксированных позициях

Блок «кого читать» вставляется не по скору, а на заранее заданное место:

fn insert_who_to_follow(blended: &mut Vec<FeedItem>, wtf_modules: Vec<WhoToFollowModule>) {
    let Some(wtf) = wtf_modules.into_iter().next() else { return; };
    let insert_idx = WHO_TO_FOLLOW_POSITION.saturating_sub(1).min(blended.len());
    blended.insert(insert_idx, FeedItem { position: WHO_TO_FOLLOW_POSITION as i32, ... });
}

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

Константа WHO_TO_FOLLOW_POSITION равна 6. То есть блок с рекомендациями аккаунтов всегда шестой, если он вообще показывается. Рядом лежит параметр усталости: WhoToFollowFatigueHours = 30 — не чаще раза в тридцать часов.

Почему фиксированная позиция, а не скор

Потому что задача другая. Скор отвечает на вопрос «насколько этот элемент лучше того». Для блока «кого читать» такой вопрос бессмыслен: он не конкурирует с постами, он делает другое — расширяет граф подписок, от которого зависит вся будущая лента.

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

Усталость в 30 часов решает вторую половину задачи. Ценность блока не в том, чтобы показать его как можно чаще, а в том, чтобы он сработал хоть раз. Показывать каждый заход — гарантированно выработать слепоту к нему. Обратите внимание на нечётность числа: 30 часов, а не 24. Кратный суткам интервал привязал бы показ к одному и тому же времени дня; 30 часов дают дрейф по времени, и блок попадает в разные контексты.

5. Побочные эффекты: тринадцать задач после ответа

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

ЗадачаКому это нужно
Публикация показанных идентификаторовСледующему запросу — чтобы не показать то же самое
Кеш отранжированных постовСледующему экрану того же скролла
Кеш запроса к моделиЭкономия вызовов при повторе
События для логов переранжированияОбучению будущих моделей
Логи размещения рекламыБиллингу и отчётности
Статистика по авторамАналитике охватов
Статистика по взаимным подпискамОценке эффекта соответствующего буста
Метрики скоров и размеров ответаМониторингу
Логи экспериментовАнализу A/B-тестов

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

Что здесь легко упустить

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

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

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

На чём спотыкаются
  • Сводить рекламу и органику в один аукцион «потому что так эффективнее». У величин нет общей единицы, у рекламы есть договорные обязательства, а доля рекламы — решение бизнеса, а не предсказание модели.
  • Ставить модули по скору. Блок «кого читать» не конкурирует с постами: он расширяет граф подписок, от которого зависит вся будущая лента.
  • Мерить разрыв между рекламами в позициях. Три текстовых поста и три видео — совершенно разное время. Именно поэтому один из блендеров считает разрыв в секундах.
  • Забывать про усталость модуля. Показ при каждом заходе вырабатывает слепоту. И интервал лучше делать некратным суткам, иначе он привяжется к одному времени дня.
  • Считать логирование надёжным. Обучающие данные пишутся в фоне без проверки результата; потери смещают выборку для будущих моделей и не видны в онлайн-метриках.

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

Как встроить рекламу в ранжированную ленту?

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

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

Зачем ограничивать рекламу долей от «безопасных» постов?

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

Экономически это означает, что лента с проблемным содержимым автоматически теряет способность зарабатывать. Такой контур обратной связи работает надёжнее любых деклараций, потому что встроен в код, а не в политику.

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

Не по скору, а по фиксированной позиции с механизмом усталости. В разобранной системе позиция шестая — в пределах первого экрана, но не на первом месте, — а интервал повторного показа 30 часов.

Некратность суткам здесь не случайна: интервал в 24 часа привязал бы показ к одному и тому же времени дня, а 30 часов дают дрейф, и блок попадает в разные контексты. Смысл ограничения в том, что ценность модуля не в частоте показа, а в том, чтобы он сработал хотя бы раз; ежедневный показ выработал бы слепоту.

Что должно происходить после отправки ответа пользователю?

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

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

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

Реклама

Ставится правилами после ранжирования, а не общим скором с постами.

Сколько её

Минимум из трёх: сколько есть · шаг расстановки · половина безопасных постов.

Порог

Меньше пяти постов — рекламы нет вовсе.

Безопасность бренда

Плохой контент уменьшает общее число реклам, а не только их расстановку.

Три блендера

По участкам (по умолчанию), по безопасному разрыву, по времени просмотра.

Разрыв во времени

Целевой интервал 4 с, минимум 3 органических поста между рекламами.

Кого читать

Фиксированная позиция 6, усталость 30 часов — некратно суткам намеренно.

Побочные эффекты

13 задач в фоне после ответа. Результат не проверяется.

Скрытый риск

Обучающие логи пишутся так же ненадёжно; их потеря смещает будущие модели.

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