You install a new OTT app for the first time — say JioHotstar — and watch exactly one film: War, an action thriller. Before any other human on the platform has anything to do with your account, before the app knows a single thing about "people like you," the very next screen already recommends Pathaan. How? No other user's data was consulted. The app looked at the one data point it had — the movie you watched — read off its own properties (action, spy thriller, high-octane), and searched its catalog for other items with matching properties. That is the entire idea behind content-based filtering: describe items by their own features, describe you by the features of what you liked, and match the two descriptions directly. No crowd required.
This is different from the more famous "people who liked X also liked Y" approach (collaborative filtering, covered separately), which needs thousands of other users' behavior before it can say anything useful. Content-based filtering needs exactly one thing: a way to turn an item into numbers.
Turning a Movie Into a Vector
Pick a small set of genre tags — say Action, Comedy, Romance, and Sci-Fi — and represent each movie as a 4-dimensional vector where each coordinate is 1 if that genre applies (a simplified, primary-genre tagging, ignoring subplots) and 0 if it doesn't. This is called one-hot encoding of a categorical attribute, and it is the most basic form of feature engineering in a content-based system.
Movie [Action, Comedy, Romance, Sci-Fi]
War [ 1, 0, 0, 0 ]
Pathaan [ 1, 0, 0, 0 ]
Golmaal [ 1, 1, 0, 0 ]
DDLJ [ 0, 0, 1, 0 ]
Koi... Mil Gaya [ 0, 0, 0, 1 ]
PK [ 0, 1, 0, 1 ]
Every movie in the catalog now lives as a point (equivalently, an arrow from the origin) in a 4-dimensional "genre space." Two movies that share genres point in similar directions. Two movies with nothing in common point in very different, even perpendicular, directions. This geometric picture — direction, not just presence of overlap — is the whole trick, and we'll make it precise in a moment.
Building the User's Taste Vector
Suppose you've watched and liked two films so far: War and Golmaal. The standard content-based recipe builds your user profile vector as the average of the feature vectors of everything you liked:
War = [1, 0, 0, 0]
Golmaal = [1, 1, 0, 0]
--------------------------------
User U = average = [1, 0.5, 0, 0]
Read this vector as a taste summary: "fully into Action, halfway into Comedy, no signal yet on Romance or Sci-Fi." Every new item you rate updates this average, so the profile drifts as your history grows — this is why the recommendations you see today are different from the ones you'd have seen after your very first watch.
Scoring Candidates: Why Raw Dot Product Fails
Now the catalog needs to be ranked against U. The most natural first guess is the dot product: multiply matching coordinates and add them up, A · B = A₁B₁ + A₂B₂ + A₃B₃ + A₄B₄. A higher dot product should mean "more overlap," right?
Check it against Pathaan = [1, 0, 0, 0]:
U · Pathaan = (1)(1) + (0.5)(0) + (0)(0) + (0)(0) = 1.0
Now imagine a hypothetical movie D tagged with every genre at full strength: D = [1, 1, 1, 1].
U · D = (1)(1) + (0.5)(1) + (0)(1) + (0)(1) = 1.5
D scores higher than Pathaan by raw dot product, even though D is only weakly, generically related to your taste while Pathaan is an almost exact match on the one genre you actually care about. The dot product got fooled by D's sheer size — a vector with more nonzero, large coordinates racks up a bigger sum almost regardless of direction. Raw dot product rewards "big vectors," not "vectors that point the same way." That's the bug we need to fix.
Deriving Cosine Similarity From the Law of Cosines
The fix is to strip out magnitude and keep only direction. For two vectors A and B separated by angle θ, consider the triangle formed by A, B, and the connecting side A − B. The law of cosines gives:
|A − B|² = |A|² + |B|² − 2|A||B|cos θ
The same quantity, expanded algebraically using the dot product's distributive property, gives:
|A − B|² = (A − B)·(A − B) = A·A − 2A·B + B·B = |A|² + |B|² − 2(A·B)
Both expressions equal |A − B|², so set them equal to each other and cancel |A|² + |B|² from both sides:
−2(A·B) = −2|A||B|cos θ ⟹ cos θ = (A·B) / (|A||B|)
This is cosine similarity: the dot product divided by the product of the two magnitudes. Dividing by |A| and |B| is exactly what neutralizes the "bigger vector wins" problem — every vector, no matter how many genres it lists, gets rescaled to length 1 before comparison. Only direction survives. The value always lies between −1 and 1 (between 0 and 1 for our non-negative genre vectors): 1 means identical direction, 0 means perpendicular — no shared taste signal at all.
In full generality, for n-dimensional vectors:
cos θ = (Σ AᵢBᵢ) / (√ΣAᵢ² · √ΣBᵢ²)
Recompute the same two candidates with cosine similarity instead of raw dot product. First, |U| = √(1² + 0.5²) = √1.25 ≈ 1.118.
Pathaan: cos θ = 1.0 / (1.118 × 1) = 0.894
D: cos θ = 1.5 / (1.118 × 2) = 0.671 (|D| = √4 = 2)
Pathaan now correctly outranks D — 0.894 versus 0.671 — because it points almost exactly where your taste vector points, while D's direction is diluted across genres you never showed interest in. This is the payoff of the whole derivation: normalizing by magnitude turns "how many features match" into "how well does the pattern of features match," which is the actually meaningful question.
Ranking the Full Candidate List
Score every remaining movie in the catalog against U = [1, 0.5, 0, 0], |U| ≈ 1.118:
Candidate Vector A·U |A| cos θ Rank
Pathaan [1,0,0,0] 1.000 1.000 0.894 1
PK [0,1,0,1] 0.500 1.414 0.316 2
Koi Mil Gaya [0,0,0,1] 0.000 1.000 0.000 tie 3
DDLJ [0,0,1,0] 0.000 1.000 0.000 tie 3
Verify PK: A·U = (0)(1) + (1)(0.5) + (0)(0) + (1)(0) = 0.5; |A| = √(0² + 1² + 0² + 1²) = √2 ≈ 1.414; cos θ = 0.5 / (1.414 × 1.118) ≈ 0.316. It ranks above the two zero-overlap films because it shares the Comedy coordinate with your profile, even though its Sci-Fi coordinate contributes nothing (your profile has 0 there). Koi... Mil Gaya and DDLJ score exactly zero — their nonzero coordinates (Sci-Fi, Romance) don't overlap with any coordinate where your profile is nonzero (Action, Comedy), so the vectors are perpendicular in this space and share no directional signal at all.
You can verify all of this with eight lines of Python:
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
mag_a = sum(x ** 2 for x in a) ** 0.5
mag_b = sum(y ** 2 for y in b) ** 0.5
return dot / (mag_a * mag_b)
user_profile = [1, 0.5, 0, 0]
pathaan = [1, 0, 0, 0]
pk = [0, 1, 0, 1]
print(round(cosine_similarity(user_profile, pathaan), 3))
print(round(cosine_similarity(user_profile, pk), 3))
Tracing it: dot(user_profile, pathaan) = 1*1 + 0.5*0 + 0*0 + 0*0 = 1.0, mag_a = 1.25**0.5 ≈ 1.118, mag_b = 1.0**0.5 = 1.0, so the function returns 1.0 / 1.118 ≈ 0.894. The second call gives dot = 0 + 0.5 + 0 + 0 = 0.5, mag_b = 2**0.5 ≈ 1.414, so it returns 0.5 / (1.118 × 1.414) ≈ 0.316. Output:
0.894
0.316
Both match the hand computation exactly, which is the whole point of tracing code by hand before trusting it.
Seeing the Angle
The picture below shows why "angle" is not just a metaphor. Restrict to two dimensions — Action and Comedy — so it can actually be drawn. Your profile U = (1, 0.5) sits at roughly 26.6° above the Action axis (since tan⁻¹(0.5/1) = 26.6°). Pathaan, at (1, 0), sits exactly on the axis, only 26.6° away from U — a small angle, hence cos 26.6° ≈ 0.894, matching the earlier calculation exactly. A hypothetical rom-com at (0.2, 1) sits at about 78.7° from the axis, which is 52.1° away from U — a much wider angle, hence a much smaller cosine, cos 52.1° ≈ 0.61. Smaller angle between arrows means more similar taste direction means higher rank.
A Common Confusion: Content-Based vs Collaborative Filtering
Common misconception: students often assume "content-based" recommendations somehow rely on what other users with similar taste watched — because that's what recommendation systems "feel like" in everyday use. This is wrong, and it matters for exams because the two techniques are graded on entirely different failure modes. Content-based filtering, as built above, uses only your own history and each item's own features — it never looks at any other account's data. A brand-new item with zero views can still be recommended the instant it's tagged with genres, because the algorithm never needed other users to rate it first (this is called immunity to the item cold-start problem). Collaborative filtering, by contrast, ignores item content entirely and instead mines patterns like "accounts that rated War highly also rated Pathaan highly" across the whole user base — it needs no genre tags at all, but it is helpless for a brand-new item nobody has rated yet, and helpless for a brand-new user with no rating history (the user cold-start problem, which is exactly the situation this chapter opened with, and exactly where content-based filtering has the advantage).
Beyond Genres: TF-IDF for Text Features
Genre tags are hand-labeled and coarse. Real systems often build feature vectors straight out of unstructured text — a plot synopsis, a product description, a news article — where the "genre space" is replaced by "one dimension per word in the vocabulary." The obvious first attempt, counting how often each word appears (term frequency, TF), fails for the same reason raw dot product failed: extremely common words like "the" or "movie" appear everywhere and drown out the words that actually distinguish one item from another.
The fix is TF-IDF (term frequency × inverse document frequency), which down-weights words in proportion to how many documents (items) they show up in. If a word appears in df out of N total item descriptions in the catalog:
IDF(word) = log(N / df)
Take a small catalog of N = 4 movie synopses. The word "the" appears in all four (df = 4): IDF = log(4/4) = log(1) = 0 — it gets zeroed out entirely, correctly, since a word every item shares carries no information for telling items apart. The word "war" appears in two synopses (df = 2): IDF = log(4/2) = log(2) ≈ 0.301 (base-10 logs here; any base works since it only rescales every weight by the same constant and never changes the ranking). The word "alien" appears in just one synopsis (df = 1): IDF = log(4/1) = log(4) ≈ 0.602 — the rarer and more distinctive the word, the larger its weight.
Multiply by term frequency to get the final feature value for that word-dimension in a specific item's vector. Say Koi... Mil Gaya's 50-word synopsis mentions "alien" 3 times: TF = 3/50 = 0.06, so its weight along the "alien" axis is TF × IDF = 0.06 × 0.602 ≈ 0.036. Every synopsis in the catalog becomes a long vector of these TF-IDF weights — mostly zeros, with nonzero entries only at the words that actually appear — and cosine similarity is computed on these vectors exactly the way it was computed on the 4-dimensional genre vectors above. The dimension count changes from 4 to "vocabulary size," but the geometry and the formula are identical.
Strengths, Limits, and Where This Meets the Syllabus
Content-based filtering's real advantages: it needs no other users' data (solving the new-item cold start), and every recommendation is explainable in one sentence — "recommended because it's tagged Action, like War, which you rated highly" — which collaborative filtering generally cannot say. Its real weakness is the mirror image of its strength: because it only ever searches for items that resemble what you already liked, it tends toward overspecialization (a filter bubble) — a user who only ever watches action films will never be shown anything outside that lane, even a film they would genuinely have loved. It's also entirely dependent on the quality of the features you engineer; badly chosen or missing tags produce badly ranked recommendations no matter how correct the cosine-similarity math is. Production systems (large e-commerce catalogs, streaming platforms, news aggregators) typically blend content-based scores with collaborative-filtering scores precisely to trade off explainability and cold-start robustness against the ability to surprise a user with something outside their usual pattern.
For exam purposes: the dot product, vector magnitude, and angle-between-vectors formula derived above via the law of cosines are directly the "Vector Algebra" chapter tested in CBSE Class 11–12 and in JEE Main/Advanced and BITSAT — cosine similarity is literally cos θ = A·B/|A||B| from that chapter, applied to a data-science problem instead of a geometry problem. The logarithm rules used in the IDF formula are Class 11 "Logarithms." At the undergraduate/GATE-foundation level, "content-based vs collaborative filtering" and "cosine similarity as a distance measure" are standard, frequently tested definitions in any introductory machine-learning or information-retrieval course, so getting the vector geometry solid now pays off directly later.
Check Your Understanding
- A user's profile vector (over genres [Action, Comedy, Romance, Sci-Fi]) is
U = [0.8, 0, 0.4, 0]. Compute the cosine similarity betweenUand a candidate movieC = [0.5, 0, 0.5, 0]by hand. (ComputeU·C,|U|,|C|separately before dividing.) - Explain, using the
D = [1,1,1,1]example from this chapter, why a recommender that ranks purely by raw dot product would systematically favor items with more tags — even irrelevant ones — over items with fewer, more precisely matching tags. - A brand-new user just signed up and rated one item. A brand-new item was just uploaded and has zero ratings from anyone. For each case, say whether content-based filtering or collaborative filtering can still produce a recommendation, and why.
- In the TF-IDF formula, why does a word that appears in every single document in the catalog end up with a weight of exactly zero, regardless of how many times it's repeated inside any one document?
- Two item vectors are perpendicular (cosine similarity = 0) in genre space. Does that mean the items have literally nothing in common as movies, or does it mean something narrower and more specific about the feature space chosen? Justify your answer using the DDLJ/Koi Mil Gaya example above.
Summary
- Content-based filtering represents each item as a feature vector (one-hot genre tags, or TF-IDF weights over text) and represents a user as the average of the vectors of items they liked.
- Raw dot product is a bad similarity score because it rewards vector magnitude, not direction — an item tagged with many genres can outscore a precisely matching item.
- Cosine similarity,
cos θ = (A·B)/(|A||B|), derived from the law of cosines, fixes this by normalizing out magnitude and comparing only direction; recommendations are ranked by this score, highest first. - TF-IDF extends the same idea to raw text by weighting each word by how rare and distinctive it is across the catalog (
IDF = log(N/df)), then feeds the exact same cosine-similarity formula. - Content-based filtering needs no other users' data, solving the new-item cold-start problem and giving explainable recommendations, but it overspecializes and cannot surprise a user outside their established pattern — the reason real systems hybridize it with collaborative filtering.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where content-based filtering: features tell the story is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting content-based filtering: features tell the story to at least 3 other topics you have studied.