Лента в ЧатЯдре была строго хронологической: ORDER BY id DESC LIMIT 20.
Потянул вниз, чтобы обновить, — те же посты. Ощущение, что ничего не происходит, хотя
контента хватало: 1130 публичных постов, 258 за последнюю неделю.
Диагноз по данным, а не по ощущениям
Разбор живых данных показал две вещи. Во-первых, ленту почти целиком заполняли каналы-агрегаторы: «Интересное», «Новости», «Кухня», «Юмор», «Дом и дача», «Спорт», сотни постов в месяц каждый. Во-вторых, и это ключ, посты шли пачками по автору: канал публикует пять записей одной минутой, и первый экран выглядел как «5 × Интересное, 3 × Спорт, 3 × Новости». Это и читалось как «нет разнообразия».
План был на три уровня, и сразу решили, что полноценную алгоритмическую ленту как у больших соцсетей не делаем: на небольшой аудитории она будет гадать, а не предсказывать.
- Непросмотренное вперёд.
- Расшивка пачек: не больше двух постов подряд от одного автора.
- Лёгкое ранжирование по свежести, реакциям и комментариям.
Первые два сделаны за день. Третий пока не понадобился.
Уровень 1: непросмотренное вперёд
Фундамент уже был: таблица просмотров post_views, заведённая для счётчика «сколько человек
видели». Запрос ленты стал двухступенчатым: сначала посты, которых человек не видел, потом всё
остальное, внутри каждой группы новое первым.
CASE WHEN NOT seen_by_me AND is_fresh THEN 0 ELSE 1 END AS rank
…
ORDER BY rank ASC, id DESC
Курсор пагинации составной: приоритет:id. Порядок однозначен, поэтому «загрузить ещё» не даёт
ни дублей, ни пропусков. Старые клиенты, которые не передают параметр, получают прежнюю
хронологию.
Первый баг нашёлся сразу: пост нового человека не находился в ленте. Карточка поста одна и та
же в ленте и в профиле, и при показе она отмечает просмотр. Достаточно было заглянуть в чужой
профиль, чтобы его запись исчезла из ленты, ни разу в ней не показавшись. Разделили: флаг
in_feed в таблице просмотров; порядок двигают только просмотры из ленты, а счётчик «сколько
видели» считается везде.
Окно свежести: замер, а не прикидка
Второй эффект был неожиданным. У активного человека накапливается хвост из тысячи с лишним непрочитанных записей агрегаторов за месяцы, и «непросмотренное вперёд» открывало ленту постами двухмесячной давности. Ровно противоположно ощущению обновления, ради которого всё затевалось.
Значит, наверх должно подниматься не всё непрочитанное, а только свежее. Сколько дней считать свежими, решили замером на живой ленте: 1163 непрочитанных против 277 прочитанных.
| Окно | Чем открывается лента |
|---|---|
| 3, 5, 7 дней | записями за вчера |
| 14 дней | записями двухнедельной давности |
Причина понятна: непрочитанное двухнедельной давности старше прочитанного вчерашнего, и при окне в 14 дней оно всё равно лезет наверх. Семь дней — верхняя граница, при которой лента ещё начинается со свежего, и при этом человек, заходящий раз в неделю, ничего не пропускает.
const UNSEEN_WINDOW_DAYS = 7;
Уровень 2: не больше двух подряд от автора
Расшивка пачек делается уже над выданной страницей: в массиве из двадцати постов переставляем так, чтобы один автор не шёл больше двух раз подряд. Если альтернативы в остатке нет, оставляем как есть: потерять пост хуже, чем показать третий подряд.
Важная тонкость: курсор для следующей страницы считается до перестановки, по последнему элементу в порядке сортировки. Иначе пагинация поехала бы. А поскольку формат ответа — голый массив, который ждут старые клиенты, курсор кладётся в каждый пост.
Баг, который не был виден с экрана
Через неделю после выката выяснилось, что «загрузить ещё» в ленте не работало ни разу.
Курсор приоритет:id разбирался в булево значение seen: true/false, и оно уходило в запрос на
сравнение с колонкой rank, которая целочисленная. Postgres пытался прочитать слово «true» как
число и отбрасывал запрос целиком. На экране просто ничего не догружалось; ошибка была видна
только в журнале сервера. Исправление — одна строка: возвращать число.
Мораль здесь не про типы, а про наблюдаемость: ошибка запроса, которая заканчивается пустым экраном без единого сообщения, будет жить неделями. Такие ошибки должны стрелять в журнал громко.
Попутные находки
Пока проверяли новые правила, нашлись две проблемы, не связанные с ранжированием. Записи удалённых сообществ оставались в таблице постов и показывались в ленте призраками с пустым именем и аватаркой, 48 штук. И шесть записей из закрытых семейных сообществ попали в общую витрину «Интересное» у постороннего человека: флаг публичности на посте оказался слабее, чем закрытость сообщества, и теперь проверяется именно она.
Оба бага нашлись только потому, что ленту стали читать глазами после каждого изменения, а не только тестами.