Вечер, компания друзей, желание посмотреть кино — и сорок минут листания каталогов стримингов. Персональные рекомендации не работают, когда фильм выбирает группа. В этой статье разберём, как устроен каталог фильмов и сериалов на 900 000 тайтлов с рекомендациями без нейросетей и ML-инфраструктуры: разрежённые векторы, косинусное сходство, инвертированный индекс, real-time комнаты кино-свайпера на SSE и честное SEO. Вы получите конкретные шаги, критерии выбора и описание типичных ошибок.

Архитектура и выбор технологий

Сервис построен на Node.js и SQLite. Такой стек позволяет быстро прототипировать и обрабатывать до 900 000 записей без отдельного сервера баз данных. Ключевые компоненты:

  • Каталог тайтлов — таблица с метаданными (название, год, жанры, режиссёр, актёры, рейтинг).
  • Разрежённые векторы — для каждого фильма строится вектор признаков (жанры, ключевые слова, актёры). Разрежённость экономит память и ускоряет вычисления.
  • Косинусное сходство — метрика для поиска похожих фильмов. Вычисляется между векторами в реальном времени или кэшируется.
  • Инвертированный индекс — для быстрого поиска по ключевым словам и жанрам.
  • Real-time комнаты — на Server-Sent Events (SSE) для синхронизации свайпов между участниками.

Критерии выбора: простота развёртывания, отсутствие внешних зависимостей, скорость разработки. SQLite подходит для чтения-ориентированных нагрузок, но требует аккуратной работы с транзакциями при записи.

Построение рекомендаций на разрежённых векторах

Каждый фильм представляется вектором в пространстве признаков. Например, жанры кодируются как бинарные признаки, актёры и режиссёры — как взвешенные. Вектор разрежён: большинство признаков нулевые. Для хранения используйте разрежённые структуры (например, объекты с ненулевыми индексами).

Косинусное сходство между двумя векторами вычисляется как скалярное произведение, делённое на произведение длин. Для разрежённых векторов скалярное произведение эффективно вычислять через пересечение ненулевых индексов. Шаги:

  • Нормализуйте векторы (приведите к единичной длине) при индексации.
  • Для поиска похожих фильмов вычислите сходство с целевым вектором, отсортируйте и возьмите топ-N.
  • Кэшируйте результаты для популярных запросов, чтобы избежать повторных вычислений.

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

Инвертированный индекс для поиска и фильтрации

Инвертированный индекс сопоставляет каждое ключевое слово (жанр, актёр, тег) списку идентификаторов фильмов. Это позволяет быстро отвечать на запросы вроде «все фильмы с жанром комедия и актёром X». В SQLite можно реализовать через отдельную таблицу связей или использовать встроенный полнотекстовый поиск (FTS5).

Пример структуры: таблица inverted_index с полями term и movie_ids (JSON-массив). При поиске объединяйте списки для нескольких терминов (пересечение). Для больших объёмов данных храните инвертированный индекс в памяти при старте сервера — это ускорит запросы.

Критерии выбора: если поиск по ключевым словам критичен, используйте FTS5; если нужна гибкость с весами — самодельный индекс. Не забывайте про обновление индекса при добавлении новых фильмов.

Real-time комнаты на SSE и честное SEO

Для совместного выбора фильма используйте комнаты, где участники свайпают карточки. SSE (Server-Sent Events) — лёгкая альтернатива WebSocket для однонаправленной передачи событий от сервера к клиенту. Реализация:

  • При подключении клиент получает уникальный ID комнаты.
  • Сервер хранит состояние комнаты (список участников, текущий фильм, свайпы) в памяти или в SQLite.
  • При действии одного участника сервер рассылает событие всем подключённым через SSE.

SSE проще в реализации, но не поддерживает двустороннюю связь — для отправки свайпов используйте обычные HTTP-запросы. Ошибка: держать состояние комнаты только в памяти — при перезапуске сервера данные теряются. Решение: периодически сохраняйте состояние в SQLite.

SEO: для индексации поисковиками используйте серверный рендеринг (SSR) или предварительную генерацию страниц фильмов с уникальными заголовками и описаниями. Избегайте дублирования контента, добавляйте микроразметку Schema.org. Не полагайтесь на клиентский рендеринг — поисковики могут не выполнять JavaScript.

Практические шаги и типичные ошибки

Чтобы повторить проект, начните с малого:

  • Соберите каталог из открытых источников (например, IMDb datasets) и загрузите в SQLite.
  • Реализуйте построение разрежённых векторов для каждого фильма (жанры, ключевые слова).
  • Напишите функцию косинусного сходства и протестируйте на нескольких примерах.
  • Добавьте инвертированный индекс для поиска по ключевым словам.
  • Создайте простой интерфейс для свайпов и комнаты на SSE.

Типичные ошибки: избыточная нормализация признаков, игнорирование кэширования, отсутствие обработки ошибок при работе с SSE (обрывы соединения), пренебрежение SEO при динамическом контенте. Пример из практики: без кэширования косинусного сходства время ответа на запрос рекомендаций достигало 2 секунд; после кэширования топ-N для каждого фильма — снизилось до 50 мс.

В результате получается работающий сервис без ML-инфраструктуры, который решает проблему выбора фильма для компании. Главное — не усложнять и использовать простые математические инструменты.