Под капотом
Под капотом: как устроен поиск
Путь от каталога TMDB до списка фильмов на ваш запрос — по шагам: какие данные берутся, что именно превращается в вектор, где он хранится и как с ним сравнивается запрос.
Фильмы
- Каталог TMDB
- Обогащение
- Текст фильма
- Вектор
- pgvector
Запрос
- Ваша фраза
- Нормализация
- Вектор той же моделью
- Ближайшие фильмы
Каталог
Основа — каталог 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 уходит только эмбеддинг самого запроса — и то если его нет в кеше.