Skip to content
Machine Learning
Recommender Systems 10 min read

Content-Based Filtering

Recommend "more like this" by turning movie descriptions into TF-IDF vectors and ranking them with cosine similarity — a full search-style recommender in the browser.

Download notebook Open Google ColabIn Colab: File → Upload notebook → pick the downloaded file.

Popularity charts treat everyone the same. Content-based filtering takes the first step toward personalization: if you just watched Toy Story, recommend movies that are like Toy Story. To do that we need two ingredients — a way to represent each item as numbers, and a way to measure how close two items are. This lesson builds both, then wires them into a working get_recommendations() function.

From "more like this" to vectors

Content-based filtering compares item attributes: genres, keywords, cast, director, plot synopsis. For movies, the most flexible trick is to mash all of that text into a single string per movie — often called a metadata soup:

"animation family comedy toys jealousy a cowboy doll is threatened
 when a new spaceman figure becomes the favorite toy"

Once every movie is a bag of words, the problem becomes a document search problem: encode each soup as a vector, then find the vectors nearest to the one the user just watched. It's the same machinery behind a search engine — except the "query" is a movie instead of typed keywords.

Counting words — and why raw counts mislead

The simplest encoding is a word count: one dimension per vocabulary word, and each movie's vector holds how often each word appears (scikit-learn's CountVectorizer). The flaw: common words dominate. If half your catalog's synopses contain "world" or "life", those words contribute big counts to many pairs of movies — inflating similarity without carrying any real signal.

TF-IDF (term frequency × inverse document frequency) fixes this by down-weighting words that appear in many documents and up-weighting rare, distinctive ones. A word like "dinosaur" that appears in only two synopses becomes a strong link between exactly those two movies; a word like "story" that appears everywhere gets weight near zero. That's TfidfVectorizer.

Cosine similarity: direction, not length

How do we compare two vectors? Euclidean distance is tempting, but it punishes length — a long synopsis would look far from a short one even if they use identical vocabulary. Cosine similarity measures only the angle between vectors: 1 means "pointing the same way", 0 means "nothing in common". See it in two dimensions first:

Python — runs in your browser

Cosine gets it right: the two sci-fi synopses are a perfect 1.0 match because they point in the same direction, while Euclidean distance claims the romance synopsis is closer to the short sci-fi one — purely because the long synopsis has bigger numbers. For text, always think angles.

Building the recommender

Now the real thing: 15 movies, each with a genre + keyword + overview soup. Pipeline: TfidfVectorizer → similarity matrix → look up a title → return the five nearest neighbors (skipping the movie itself, which is always its own best match).

Python — runs in your browser

The clusters fall out beautifully: Toy Story pulls the other animated family films, The Dark Knight finds Batman Begins through shared words like "gotham", "batman", and "superhero", and Titanic lands in the romance corner. Nobody told the model about genres as a concept — the vocabulary overlap alone encodes it.

CountVectorizer or TfidfVectorizer?

For free-text like synopses, TF-IDF is almost always better — it silences filler words automatically. But for curated metadata (genre tags, cast names, director), plain CountVectorizer is often the right call: every token was chosen deliberately, and you may not want "Steven Spielberg" down-weighted just because he directed many films. A common design: count-vectorize the structured tags, TF-IDF the synopsis, and combine. You can also repeat important tokens in the soup to weight them manually.

Strengths and the filter bubble

What content-based filtering gets right:

  • No other users needed. It works from day one with a catalog of one user — similarity comes from item attributes, not crowd behavior. New items are no problem either: as soon as a movie has a synopsis, it can be recommended.
  • It explains itself. "Recommended because you watched Toy Story — both are animated family films" is a legible, trust-building reason.

Its central weakness is over-specialization. The system can only recommend things similar to what you've already consumed. Watch three space movies and your homepage becomes an airlock — it will never discover that you'd also love jazz documentaries, because no vocabulary connects them. Users get trapped in a filter bubble of their own history. Breaking out requires information the item descriptions don't contain: what other people with tastes like yours enjoyed. That's collaborative filtering, next lesson.

Check your understanding

4 questions · free
  1. Q1.Why does TF-IDF usually beat raw word counts for comparing synopses?

  2. Q2.Two synopses use the same words in the same proportions, but one is three times longer. What does cosine similarity report?

  3. Q3.In get_recommendations, why do we drop the queried title before taking the top k?

  4. Q4.Which problem can content-based filtering NOT solve, even in principle?

Exercise: More like A Quiet Place

Extend the 15-movie corpus with two movies of your own — one horror film described with words overlapping A Quiet Place (monsters, silence, survival…) and one family film. Rebuild the TF-IDF matrix and check get_recommendations("A Quiet Place"): does your horror film appear in the top 5? Which shared words do you think made the match?

Next up: collaborative filtering — dropping item descriptions entirely and learning taste from the ratings matrix itself.