Under the hood
Under the hood: how the search is built
The path from the TMDB catalogue to the list of films for your query, step by step: which data is used, what exactly becomes a vector, where it is stored and how a query is compared with it.
Films
- TMDB catalogue
- Enrichment
- Film text
- Vector
- pgvector
Query
- Your phrase
- Normalisation
- Vector from the same model
- Nearest films
The catalogue
The foundation is the TMDB catalogue: title, year, plot summary, tagline, genres, keywords, collection, directors and cast. Ratings and vote counts come from the IMDb non-commercial datasets. Every film is stored in two languages; when TMDB lacks a Russian title, summary or keywords, a language model translates them, and those texts are labelled on the site.
Not every film in the catalogue takes part in meaning-based search — only those that pass a TMDB popularity threshold and have ratings: a truly obscure film has too little data, and its vectors would be noise. Keyword search still reaches the whole catalogue.
Enrichment
A TMDB summary retells the plot, while people usually search by feeling. So for every film a language model (OpenAI gpt-4o-mini) writes a separate paragraph — not about what happens, but about what it is like to watch: mood, atmosphere, themes, visual style, pacing, place and era, the situations in which you would want to put this film on.
The model receives only the title and the summary. It is told not to retell the plot or spoil anything; it must write one fluent paragraph of 450–800 characters rather than a list of keywords. The English and Russian versions are written in a single request but independently — two texts that are natural in their own language, not a translation of one another.
This paragraph is shown on the film page in the “Mood and atmosphere” block, labelled as written by a model.
The text that gets embedded
All of this data is assembled into a single text per film — from a template, separately for each language. Lines without data are skipped, English and Russian fields are never mixed, and people’s names stay as they are. Here is the English text for Lost in Translation:
Lost in Translation (2003)
Genres: Comedy, Drama, Romance
Keywords: loneliness, jet lag, karaoke, culture clash, midlife crisis, homesickness, tokyo, japan, …
Directed by Sofia Coppola. Starring: Bill Murray, Scarlett Johansson, Giovanni Ribisi, Anna Faris, …
Everyone wants to be found.
Description: Two lost souls visiting Tokyo -- the young, neglected wife of a photographer and a washed-up movie star shooting a TV commercial -- find an odd solace and pensive freedom to be real in each other's company, away from their lives in America.
Film about: Immerse yourself in the serene yet melancholic atmosphere of Tokyo, where the neon lights dance against the backdrop of rainy streets, creating a cozy, reflective ambiance. This film captures the essence of loneliness intertwined with hope, as two lost souls navigate their lives, finding solace in fleeting moments of connection. The beautiful cinematography evokes nostalgia, reminiscent of a bygone era, while the haunting soundtrack lingers in the air, echoing the complexities of identity and dreams. Perfect for those quiet evenings when you seek comfort in emotional depth and a sense of freedom, this experience unfolds like a poetic exploration of friendship and self-discovery amid the vibrant chaos of a bustling city.The year is the only number in the text, because it carries the sense of an era. Ratings, popularity and budgets never reach the vector — filtering and sorting use them separately. If a language has neither a title nor a summary, no text is built at all and the film is absent from search in that language: better nothing than a vector made of nothing.
The template is versioned and every text keeps a SHA-256 hash: a vector is recomputed only when its text has actually changed — after enrichment or a new translation, for example.
The vector
The text is turned into a vector by OpenAI text-embedding-3-large: 3072 numbers that act as coordinates of meaning. Texts with similar meaning get similar vectors, even when they use different words or different languages.
- Model
- OpenAI text-embedding-3-large, 3072 dimensions. Vectors are normalised to unit length.
- Storage
- PostgreSQL with the pgvector extension, halfvec type: 16-bit numbers halve the space with practically no loss of precision.
- Index
- HNSW over cosine distance (m = 16, ef_construction = 64) — a nearest-neighbour graph that spares the query from being compared with every film in turn.
- Spaces
- One per “model × language” pair: English and Russian vectors never mix, and vectors for a new model can be built alongside without touching the live ones.
The query
The phrase you type takes the same path, only shorter. First it is normalised: Unicode NFC, lower case, “ё” → “е”, extra spaces collapsed — so that “Space Survival” and “space survival” are one and the same query.
Then the query is turned into a vector by the same model that embedded the films, and compared with films in the space of the same language. This matters: every model has its own coordinate system, and comparing vectors from different models is meaningless.
Query vectors are cached — in server memory and in a Postgres table — so a repeated query never goes to OpenAI: it is faster and costs nothing.
The results
The database finds up to 240 films whose vectors lie closest to the query vector by cosine distance; relevance is one minus that distance. Filters — genre, tag, actor, years, IMDb score — are applied inside the same SQL query, so only matching films make it into that pool. The results can then stay in order of relevance or be re-sorted by rating, popularity or year.
There are three search modes, and meaning is only one of them:
- By meaning
- Only the vector branch described above.
- By words
- Postgres full-text search: websearch_to_tsquery and ts_rank with a language configuration.
- Hybrid
- Both branches, 240 candidates each, merged with Reciprocal Rank Fusion: a film scores 1 / (60 + rank by words) + 1 / (60 + rank by meaning). Films ranked high in both lists rise to the top. If the embedding provider is unavailable, hybrid falls back to search by words.
The stack
The backend and the data pipeline are written in Go, the database is PostgreSQL with pgvector, and the frontend is React with server-side rendering. Translation, enrichment and film embeddings are computed ahead of time by separate pipeline commands, not on the fly: at search time only the embedding of the query itself goes to OpenAI — and only when it is not cached.