Разработка алгоритмической ленты рекомендаций в мобильном приложении
Алгоритмическая лента — это не список постов по дате, отсортированный в обратном порядке. Это ранжирующая система, которая в реальном времени решает: какой контент показать конкретному пользователю в конкретный момент. TikTok, Instagram Reels, YouTube Shorts используют один принцип: максимизация времени взаимодействия через predicted engagement score для каждой единицы контента.
Архитектура: candidate generation → ranking → serving
Алгоритмическая лента работает в два этапа, и мобильное приложение критически зависит от обоих.
Candidate generation — из миллионов единиц контента отбираем несколько сотен кандидатов для конкретного пользователя. Обычно это облегчённая модель (Approximate Nearest Neighbor по user embedding) или набор правил: followed users, trending in geo, topic affinity. Этот этап должен укладываться в 50–100ms.
Ranking — кандидаты ранжируются тяжёлой моделью, которая предсказывает вероятность взаимодействия (like, share, comment, completion rate). Gradient boosted trees (XGBoost, LightGBM), two-tower neural networks или DLRM. Результат — упорядоченный список с scores.
Serving — мобильное приложение запрашивает N следующих элементов ленты, получает их с предрасчитанным порядком. Prefetch следующей страницы до того, как пользователь дойдёт до конца текущей.
Сигналы ранжирования: что собираем в приложении
Качество алгоритма определяется качеством сигналов. Мобильное приложение — главный источник:
Engagement signals:
-
like,share,comment,save— явные сигналы с высоким весом -
video_completion_rate— досмотрел ли пользователь видео до конца, на каком моменте вышел -
dwell_time— время на карточке/посте, но не полное время (может быть открыт другой app) -
swipe_away_velocity— быстрый свайп вниз без остановки это негативный сигнал
Implicit negative signals:
- Пользователь промотал мимо за < 0.5 секунды — вероятный skip
- Report / Hide content — сильный негативный сигнал
- App backgrounded сразу после показа контента
Трекинг video completion на iOS с AVPlayer:
class VideoProgressTracker {
private var timeObserver: Any?
private let player: AVPlayer
private let itemId: String
private var maxProgress: Float = 0
init(player: AVPlayer, itemId: String) {
self.player = player
self.itemId = itemId
setupObserver()
}
private func setupObserver() {
let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
guard let self,
let duration = self.player.currentItem?.duration.seconds,
duration > 0 else { return }
let progress = Float(time.seconds / duration)
if progress > self.maxProgress {
self.maxProgress = progress
}
}
}
func reportCompletion() {
Analytics.track(.videoProgress(itemId: itemId,
completionRate: maxProgress,
source: .algorithmicFeed))
}
}
На Android — ExoPlayer c AnalyticsListener.onPlaybackStateChanged() и Player.Listener.onPositionDiscontinuity().
Prefetch и бесконечная лента
Пользователь не должен видеть лоадер при скролле. Стандартный подход: подгружаем следующую страницу, когда пользователь дошёл до предпоследнего элемента текущей.
// Android, RecyclerView + ViewModel
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisible = layoutManager.findLastVisibleItemPosition()
val total = layoutManager.itemCount
if (total - lastVisible <= PREFETCH_THRESHOLD) {
viewModel.loadNextPage()
}
}
})
PREFETCH_THRESHOLD — обычно 3–5 элементов. Для видео-ленты увеличиваем до 7–10, потому что загрузка видео дольше.
Дедупликация: сервер может вернуть один и тот же item в двух пагинированных ответах. Клиент хранит Set<String> показанных ID и фильтрует дубли перед добавлением в список.
Контекстные сигналы от устройства
Время суток, день недели, тип сети (WiFi vs cellular), заряд батареи — всё это контекст, который улучшает ранжирование. На iOS — CTTelephonyNetworkInfo для типа сети, UIDevice.current.batteryLevel для заряда. Не нужно отправлять эти данные с каждым запросом — достаточно при открытии сессии.
Объяснимость: почему этот контент
Пользователи хотят понимать, почему лента показывает то, что показывает. Минимум — тег «Потому что вам понравился X» или «Популярно в вашем регионе». Это и UX, и доверие. Сервер возвращает explanation_key вместе с контентом, мобильное приложение рендерит соответствующую метку.
A/B тестирование алгоритма
Новую версию ранжирующей модели не выкатывают сразу на всех. Типичная схема: 5% трафика → 20% → 50% → 100%, с мониторингом метрик сессии (retention D1/D7, среднее время в приложении, engagement rate) на каждом шаге.
Feature flags управляются через Firebase Remote Config или собственную систему. Клиент передаёт experiment_variant в каждом запросе к feed API — это позволяет серверу выбрать нужный ранкер.
Процесс работы
Аудит текущего трекинга: что уже собирается, насколько точны данные о просмотрах.
Проектирование event schema для ленты: completion rate, dwell time, explicit signals.
Разработка клиентской части: бесконечный скролл, prefetch, дедупликация, видео-плеер с трекингом.
Интеграция с feed API: постраничная загрузка, обработка ошибок, offline fallback (кешированная лента).
Реализация explanation labels и UI-элементов контроля (скрыть контент, не интересно).
Мониторинг и A/B инфраструктура.
Ориентиры по срокам
Клиентская часть с правильным трекингом для существующего feed API — 2–3 недели. Полная система с серверным ранкером, pipeline обучения и мобильной интеграцией — 2–3 месяца. Стоимость зависит от объёма контента, требуемой латентности и наличия готовой аналитической инфраструктуры.







