Дополнение · X-алгоритм · страница 9 из 11
Блендинг: лента состоит не только из постов
Ранжирование закончилось, посты упорядочены. Но на экране будут ещё реклама, блок «кого читать» и промо-подсказки — то, что моделью не ранжируется и ранжироваться не должно. Разбираем, как это смешивается: почему реклама ставится правилами, а не скором, откуда берётся ограничение на её количество и что происходит после отправки ответа.
Коротко
- Реклама размещается правилами, а не скором. Её место определяется расстановкой и ограничениями, а не сравнением с органическими постами.
- Число реклам ограничено тремя вещами сразу: сколько её вообще есть, какой шаг расстановки и сколько в ленте безопасных для бренда постов.
- Не больше половины безопасных постов может соседствовать с рекламой — жёсткий потолок в коде.
- Если постов меньше пяти, рекламы не будет вовсе. Пустую ленту рекламой не наполняют.
- Модули стоят на фиксированных позициях: «кого читать» — шестым, с усталостью в 30 часов.
1. Почему реклама не участвует в ранжировании
Соблазн очевиден: у поста есть скор, у рекламы есть ставка — приведём к общей шкале и отсортируем вместе. Так иногда делают, и это называется единым аукционом. Здесь пошли другим путём, и на то есть причины.
- У величин нет общей единицы. Скор поста — это взвешенная сумма вероятностей действий, число порядка сотых. Ставка рекламодателя — деньги. Коэффициент пересчёта одного в другое не выводится ниоткуда: его придётся назначить, и он же станет главной ручкой всей экономики продукта.
- У рекламы есть обязательства. Рекламодателю обещано определённое количество показов, соседство с определённым контентом запрещено договором. Такие ограничения выражаются как правила, а не как слагаемые в скоре: скор можно перебить, обязательство — нет.
- Доля рекламы — решение бизнеса, а не предсказание. Сколько её в ленте, определяется стратегией компании. Отдать это модели значит потерять управление: она будет выбирать долю, оптимальную под свою метрику, а не под цели продукта.
Тот же вывод мы формулировали в главе «Как проектировать систему с нуля»: величины с разными целевыми функциями не сводят в один скор, их разносят по стадиям и связывают правилами.
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 задач в фоне после ответа. Результат не проверяется.
Скрытый риск
Обучающие логи пишутся так же ненадёжно; их потеря смещает будущие модели.
Первоисточники
- blender_selector.rs — смешивание, фиксированные позиции модулей.
- partition_organic_blender.rs — три ограничения на число реклам.
- time_gap_blender.rs — разрыв, измеряемый во времени просмотра.
- side_effects/ — все тринадцать фоновых задач.
- Курс: глава «Как проектировать систему с нуля» о слоях с разными целевыми функциями, глава «Логирование и его разрывы» о логировании и его разрывах.