Building a Netflix-Style Recommendation Engine with Vector Embeddings
Recommendation engines in modern streaming platforms must balance semantic nuance, latency, and operational cost. While modern Large Language Models (LLMs) possess vast knowledge, routing catalog recommendation queries directly through LLMs introduces high latency, prohibitive token costs, and catalog hallucinations. By decoupling semantic representation from text generation using dense vector embeddings and geometric similarity metrics, engineers can construct low-latency, deterministic, and cost-effective recommendation systems.
1. Problem Space: Catalog Search vs. Generative Recommendations
When building a recommendation engine for an enterprise streaming catalog (such as Netflix or Amazon Prime), the primary objective is to surface the most relevant items from a fixed, proprietary inventory based on either a free-form natural language query or item-to-item association.
The Naive LLM Generation Approach
A naive implementation sends the user query directly to an LLM with a system prompt:
System: You are an expert movie recommender.
User: Recommend a sci-fi space related movie.
LLM Output: "I recommend Interstellar, 2001: A Space Odyssey, and Solaris."
This naive approach introduces critical failure modes in production:
- Catalog Leakage & Hallucination: The LLM recommends titles based on its general pretraining corpus rather than what the platform currently licenses. Recommending a movie not available on the platform causes immediate user drop-off.
- Context Window Exhaustion & Cost: Injecting the entire platform catalog (tens or hundreds of thousands of titles) into the prompt context on every search request scales input tokens linearly ( catalog size), resulting in astronomical API costs and high latency.
- Generative Overkill: Recommendation engines require structured identifiers (Movie IDs, metadata, relevance scores) to populate a user interface, not verbose, conversational paragraphs.
The Vector Retrieval Alternative
Instead of continuous LLM generation, the catalog items are mapped into a high-dimensional vector space once (precomputation). Natural language queries or item interactions are then resolved using lightweight, mathematical vector operations (such as Cosine Similarity) without generative overhead.
2. Embedding Models vs. Generative LLMs: Architecture and Divergence
Embedding models and autoregressive LLMs share architectural foundations in Transformer neural networks, but diverge fundamentally in their objective functions, output layers, and runtime resource profiles.
Key Architectural Differences
Conceptually: An embedding model compresses semantic meaning, style, and domain concepts into geometric coordinates. If two texts share semantic characteristics, their coordinate vectors point in nearly identical directions within the -dimensional space.
3. Semantic Vector Representation: Descriptions vs. Titles
A vector embedding is only as informative as the underlying text used to generate it. When transforming a movie catalog into vectors, developers must choose which metadata fields to embed:
Why Titles Fail
- Titles are often abstract, allegorical, or homonymic (e.g., Gravity, Fargo, Her).
- An embedding model cannot deduce that Gravity involves an astronaut stranded in low Earth orbit from the single word "Gravity"; it requires contextual narrative metadata.
Why Descriptions Succeed
- Detailed synopsis data provides dense semantic grounding.
- Sub-genres, plot pacing, themes, and emotional tones are explicitly encoded in the text, allowing the embedding model to place the title adjacent to conceptually related queries (e.g., mapping "sad romance" to Titanic based on plot summaries rather than genre labels alone).
4. Mathematical Foundation: Cosine Similarity vs. Distance Metrics
Once text is converted into vectors, similarity determination becomes a geometric calculation. The two primary metrics evaluated for dense text embeddings are Euclidean Distance ( Distance) and Cosine Similarity.
The Cosine Similarity Equation
Cosine similarity evaluates the cosine of the angle between two non-zero vectors and :
- Range: Measures alignment from (diametrically opposed) to (identical direction). In normalized text embeddings, values typically fall between and .
- Magnitude Invariance: By dividing the dot product by the product of vector magnitudes , cosine similarity ignores differences in text length (verbosity) and isolates thematic alignment.
Step-by-Step Manual Calculation Example
Consider two arbitrary 3-dimensional vectors:
Step 1: Compute Dot Product ()
Step 2: Compute Norm of ()
Step 3: Compute Norm of ()
Step 4: Compute Cosine Similarity
The resulting similarity score of indicates near-perfect directional alignment.
5. End-to-End System Architecture & Data Flow
The recommendation engine operates using two discrete operational pipelines: Offline Catalog Precomputation and Online Request Evaluation.
Pipeline 1: Offline Ingestion & Embedding Generation (Runs once on startup/catalog update)
movies.json (Static Catalog File)
|
| (1) Read title & description records
v
Application Startup (Spring Boot @PostConstruct)
|
| (2) Send descriptions to OpenAI API (text-embedding-3-small)
v
Embedding Model
|
| (3) Return 1536-dimensional float arrays
v
In-Memory Catalog Store (List<Movie> containing precomputed vectors)
Pipeline 2: Runtime Semantic Search (GET /movies/search?query=...)
Pipeline 3: Item-to-Item Similarity ("More Like This") (GET /movies/{title}/similar)
6. Data Modeling and Application Startup Ingestion
The domain model consists of three core abstractions: the raw catalog structure (MovieData), the vectorized operational record (Movie), and the final ranked projection (MovieMatch).
Domain Models
package com.fde.moviematch.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
// Ingest model representing raw JSON input
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MovieData {
private String title;
private String description;
}
package com.fde.moviematch.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
// Internal representation containing precalculated vector embeddings
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Movie {
private String title;
private String description;
private List<Float> embedding; // Dimensions: 1536 for text-embedding-3-small
}
package com.fde.moviematch.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
// DTO returned to client containing calculated similarity score
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MovieMatch {
private String title;
private String description;
private double match; // Calculated Cosine Similarity
}
Catalog Initialization Service
The ingestion engine reads the catalog from a structured JSON file at boot time and generates embeddings for each movie description.
package com.fde.moviematch.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fde.moviematch.model.Movie;
import com.fde.moviematch.model.MovieData;
import jakarta.annotation.PostConstruct;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@Service
public class MovieService {
private final EmbeddingModel embeddingModel;
private final List<Movie> movieEmbeddings = new ArrayList<>();
public MovieService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
@PostConstruct
public void initializeMovies() {
try {
ObjectMapper mapper = new ObjectMapper();
InputStream inputStream = getClass().getResourceAsStream("/movies.json");
List<MovieData> movieDataList = mapper.readValue(
inputStream,
new TypeReference<List<MovieData>>() {}
);
for (MovieData data : movieDataList) {
// Generate vector embedding strictly on the movie description
List<Float> vector = embeddingModel.embed(data.getDescription());
movieEmbeddings.add(new Movie(
data.getTitle(),
data.getDescription(),
vector
));
}
System.out.println(movieEmbeddings.size() + " movies loaded with embeddings.");
} catch (Exception e) {
throw new RuntimeException("Failed to initialize movie catalog vectors", e);
}
}
}
7. Implementing Natural Language Semantic Search
Runtime search transforms unstructured user inputs into vectors and ranks the catalog using programmatic cosine similarity.
// Core vector math and semantic search methods inside MovieService.java
public List<MovieMatch> search(String query) {
// Step 1: Embed the incoming user query using the same embedding model
List<Float> queryVector = embeddingModel.embed(query);
List<MovieMatch> matches = new ArrayList<>();
// Step 2: Linear scan across precomputed catalog vectors
for (Movie movie : movieEmbeddings) {
double similarity = cosineSimilarity(queryVector, movie.getEmbedding());
matches.add(new MovieMatch(
movie.getTitle(),
movie.getDescription(),
similarity
));
}
// Step 3: Sort by similarity in descending order
sortBySimilarity(matches);
// Step 4: Extract top-K items (e.g., K = 3)
return getTopMatches(matches, 3);
}
public double cosineSimilarity(List<Float> vectorA, List<Float> vectorB) {
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < vectorA.size(); i++) {
float a = vectorA.get(i);
float b = vectorB.get(i);
dotProduct += a * b;
normA += a * a;
normB += b * b;
}
// Edge case: handle zero-vector scenarios to avoid division by zero
if (normA == 0.0 || normB == 0.0) {
return 0.0;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
private void sortBySimilarity(List<MovieMatch> matches) {
matches.sort((m1, m2) -> Double.compare(m2.getMatch(), m1.getMatch()));
}
private List<MovieMatch> getTopMatches(List<MovieMatch> matches, int k) {
return matches.subList(0, Math.min(k, matches.size()));
}
8. Implementing Item-to-Item ("More Like This") Recommendations
When a user clicks on an existing movie, the system bypasses the external embedding API completely. It queries the pre-existing vector already held in memory, eliminating external network latency and API costs.
// Item-to-item recommendation logic inside MovieService.java
public List<MovieMatch> getSimilarMovies(String title) {
// Step 1: Locate the target movie in the precomputed catalog
Movie targetMovie = movieEmbeddings.stream()
.filter(m -> m.getTitle().equalsIgnoreCase(title))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Movie not found: " + title));
List<MovieMatch> matches = new ArrayList<>();
// Step 2: Compare target movie vector against the catalog
for (Movie movie : movieEmbeddings) {
// Critical Edge Case: Prevent comparing the movie to itself
if (movie.getTitle().equalsIgnoreCase(targetMovie.getTitle())) {
continue;
}
double similarity = cosineSimilarity(targetMovie.getEmbedding(), movie.getEmbedding());
matches.add(new MovieMatch(
movie.getTitle(),
movie.getDescription(),
similarity
));
}
// Step 3: Sort and return Top-K matches
sortBySimilarity(matches);
return getTopMatches(matches, 3);
}
Controller Layer Implementation
The REST controller exposes these capabilities through clear, decoupled HTTP endpoints:
package com.fde.moviematch.controller;
import com.fde.moviematch.model.MovieMatch;
import com.fde.moviematch.service.MovieService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/movies")
public class MovieController {
private final MovieService movieService;
public MovieController(MovieService movieService) {
this.movieService = movieService;
}
@GetMapping("/search")
public List<MovieMatch> search(@RequestParam("query") String query) {
return movieService.search(query);
}
@GetMapping("/{title}/similar")
public List<MovieMatch> getSimilarMovies(@PathVariable("title") String title) {
return movieService.getSimilarMovies(title);
}
}
9. Engineering Realities: Brute-Force vs. Vector Databases & Multi-Modal Signals
While an in-memory linear scan is sufficient for small prototypes, deploying this system at production scale introduces significant algorithmic bottlenecks and requires multi-factorial ranking adjustments.
1. Algorithmic Complexity: Exact Scan vs. ANN
- Current Implementation (): Every query linearly compares the query vector against catalog vectors across dimensions ().
- For , one query requires floating-point multiplications, which saturates CPU resources and degrades response times.
- Production Solution (ANN via Vector DBs): Dedicated Vector Databases (e.g., Pinecone, Milvus, Qdrant,
pgvector) utilize Approximate Nearest Neighbor (ANN) indexing structures such as HNSW (Hierarchical Navigable Small World) or IVF (Inverted File Index), reducing search complexity from to .
2. Multi-Signal Hybrid Recommendation Scoring
Production platforms like Netflix do not rank recommendations strictly on raw text similarity. Real-world relevance scoring combines vector semantic similarity with user context and business heuristics:
Summary and Key Takeaways
- Generative LLMs are inefficient for catalog search: Using generative LLMs directly to retrieve catalog recommendations causes catalog hallucinations, context-window overflow, and high token costs.
- Embeddings decouple intelligence from generation: Text embedding models transform unstructured narrative metadata into fixed-size dense coordinate vectors (), capturing semantics while remaining computationally lightweight.
- Precompute the catalog, embed only the query: Catalog vectors are calculated once during ingestion and stored. Online searches only embed the single incoming user query string, dramatically reducing operational costs.
- Item-to-item similarity requires zero model inference: The "More Like This" feature operates exclusively on pre-existing catalog vectors, executing entirely in memory via vector arithmetic with zero external API calls.
- Embed descriptions, not titles: Movie titles contain minimal semantic information. Comprehensive plot synopses provide the model with the necessary thematic, emotional, and structural context for accurate spatial positioning.
- Cosine similarity normalizes text length: By evaluating the angular difference between vectors rather than their spatial distance, cosine similarity prevents varying description lengths from distorting relevance scores.
- Production requires Approximate Nearest Neighbors (ANN): While an in-memory scan works for small catalogs, enterprise deployments rely on Vector Databases (using HNSW or IVF indexes) to achieve search latency over millions of vectors.