Под капотом

Под капотом: как устроен поиск

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

Фильмы

  1. Каталог TMDB
  2. Обогащение
  3. Текст фильма
  4. Вектор
  5. pgvector

Запрос

  1. Ваша фраза
  2. Нормализация
  3. Вектор той же моделью
  4. Ближайшие фильмы

Каталог

Основа — каталог TMDB: название, год, описание сюжета, слоган, жанры, ключевые слова, коллекция, режиссёры и актёры. Рейтинги и число голосов — из некоммерческих датасетов IMDb. Каждый фильм хранится на двух языках; если у TMDB нет русского названия, описания или тегов, их переводит языковая модель, и такие тексты помечены на сайте.

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

Обогащение

Описание из TMDB — это пересказ сюжета, а ищут обычно по ощущению. Поэтому для каждого фильма языковая модель (OpenAI gpt-4o-mini) пишет отдельный абзац — не о том, что происходит, а о том, каково это смотреть: настроение, атмосфера, темы, визуальный стиль, темп, место и эпоха, ситуации, в которых этот фильм хочется включить.

На вход модель получает только название и описание фильма. Пересказывать сюжет и спойлерить ей запрещено; нужен один связный абзац на 450–800 символов, а не список ключевых слов. Русская и английская версии пишутся одним запросом, но независимо — это два самостоятельных текста, а не перевод одного в другой.

Этот абзац виден на странице фильма в блоке «Атмосфера и настроение» — с пометкой, что его написала модель.

Текст для эмбеддинга

Из всех этих данных для каждого фильма собирается один текст — по шаблону, отдельно для каждого языка. Строки без данных пропускаются, русские и английские поля никогда не смешиваются, имена людей остаются как есть. Так выглядит русский текст «Трудностей перевода»:

Трудности перевода (2003)
Жанры: драма, комедия, мелодрама
Теги: одиночество, джетлаг, караоке, конфликт культур, кризис среднего возраста, тоска по дому, Токио, Япония, …
Режиссёр: Sofia Coppola. В ролях: Bill Murray, Scarlett Johansson, Giovanni Ribisi, Anna Faris, …
«Каждый ищет понимания»
Описание: Токио, ночь, фешенебельный бар дорогого отеля. Спасаясь от бессонницы, здесь встречаются двое американцев: немолодой телеактер Боб Харрис и симпатичная девушка Шарлотта. Он снимается в японской рекламе виски, а она приехала вместе с мужем, оба - потерялись в этом незнакомом городе.
Фильм о: Погрузитесь в спокойную и меланхоличную атмосферу Токио, где неоновые огни танцуют на фоне дождливых улиц, создавая уютный и рефлексивный настрой. Этот фильм запечатлевает суть одиночества, переплетенного с надеждой, когда две потерянные души ищут утешение в мимолетных моментах связи. Прекрасная кинематография вызывает ностальгию, напоминающую о прошлом, а завораживающий саундтрек витает в воздухе, отражая сложности идентичности и мечты. Идеален для тихих вечеров, когда вы ищете утешение в эмоциональной глубине и чувстве свободы, этот опыт разворачивается как поэтическое исследование дружбы и самопознания на фоне яркого хаоса шумного города.
Теги и актёры сокращены: в настоящем тексте — все теги фильма и до десяти актёров в порядке титров. Строка «Фильм о» — результат обогащения.

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

Шаблон версионирован, а у каждого текста хранится хеш SHA-256: вектор пересчитывается, только когда текст действительно изменился — например, после обогащения или нового перевода.

Вектор

Текст превращается в вектор моделью OpenAI text-embedding-3-large: 3072 числа — координаты смысла. Близкие по смыслу тексты получают близкие векторы, даже если написаны разными словами и на разных языках.

Модель
OpenAI text-embedding-3-large, 3072 измерения. Векторы нормализуются до единичной длины.
Хранение
PostgreSQL с расширением pgvector, тип halfvec: 16-битные числа вдвое экономят место практически без потери точности.
Индекс
HNSW по косинусному расстоянию (m = 16, ef_construction = 64) — граф ближайших соседей, который позволяет не сравнивать запрос с каждым фильмом подряд.
Пространства
Отдельное для каждой пары «модель × язык»: русские и английские векторы не смешиваются, а векторы новой модели можно построить рядом, не трогая рабочие.

Запрос

Фраза из строки поиска проходит тот же путь, только короче. Сначала она нормализуется: Unicode NFC, нижний регистр, «ё» → «е», лишние пробелы схлопываются — чтобы «Космос Выживание» и «космос выживание» были одним и тем же запросом.

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

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

Выдача

База находит до 240 фильмов, чьи векторы ближе всего к вектору запроса по косинусному расстоянию; релевантность — это единица минус расстояние. Фильтры — жанр, тег, актёр, годы, оценка IMDb — применяются в том же SQL-запросе, поэтому в этот пул попадают только подходящие фильмы. Дальше выдачу можно оставить по релевантности или пересортировать по рейтингу, популярности или году.

Режимов поиска три, и смысловой — только один из них:

По смыслу
Только векторная ветка, описанная выше.
По словам
Полнотекстовый поиск Postgres: websearch_to_tsquery и ts_rank с языковой конфигурацией.
Гибрид
Обе ветки, по 240 кандидатов в каждой, слитые методом Reciprocal Rank Fusion: оценка фильма — 1 / (60 + место по словам) + 1 / (60 + место по смыслу). Выше оказываются фильмы, которые высоко в обоих списках. Если провайдер эмбеддингов недоступен, гибрид работает как поиск по словам.

Стек

Бэкенд и конвейер данных написаны на Go, база — PostgreSQL с pgvector, фронтенд — React с серверным рендерингом. Перевод, обогащение и эмбеддинги фильмов считаются заранее отдельными командами конвейера, а не на лету: во время поиска к OpenAI уходит только эмбеддинг самого запроса — и то если его нет в кеше.