Content-Based Filtering
Content-Based Filtering (CBF) is a recommendation paradigm in which the recommender models a user’s preferences as a vector in the item-feature space and scores candidate items by the geometric similarity of each item’s feature vector to the user’s preference vector. The system never consults other users’ interaction history — it relies entirely on (a) the features of items the target user has interacted with in the past, and (b) the features of candidate items not yet seen. Because the model needs only one user’s data and the catalog’s content features, it has the structural advantage of working without other users’ behaviour, which makes it the natural answer to the new-item case of the Cold Start Problem and to use cases where user data must remain private.
1. Why It Exists — The Problem It Was Designed to Solve
Two limits of pure Collaborative Filtering motivate the existence of content-based methods.
Limit 1: Collaborative filtering cannot recommend items it has never seen. Suppose a streaming service uploads a brand-new film at noon. By 12:01pm there have been zero ratings, zero clicks, zero watch-events. The user-item interaction matrix R has a column for this film that is entirely empty. Every collaborative algorithm — neighborhood-based or matrix-factorization-based — fails on this column because the algorithm consumes only R, and R contains no signal about the new film. The film is invisible to the recommender until interactions accumulate, which is a chicken-and-egg problem because impressions are themselves a downstream function of the recommender’s output.
Content-based filtering does not have this limit. The new film comes with metadata: a title, a synopsis, a genre, a director, a cast list, a runtime, possibly a poster image and a trailer. From these the system can extract a feature vector — a TF-IDF (Term Frequency–Inverse Document Frequency, defined formally in §3) representation of the synopsis, a one-hot encoding of the genre, an embedding of the poster from a pretrained image model. With a feature vector in hand, the system can immediately score the film against any user’s preferences. The new film is no longer invisible.
Limit 2: Collaborative recommendations are hard to explain. A factor-model collaborative recommender that learns dense latent vectors cannot easily say why it recommended a particular item. The latent dimensions are learned from data; they may correspond loosely to recognizable axes (a “kid-friendly vs adult” axis is commonly reported in MovieLens factor models with k=20), but they are not directly interpretable in general.
Content-based recommenders, by contrast, can produce concrete textual explanations: “Recommended because you liked Arrival and Blade Runner 2049, which share the director Denis Villeneuve and the science-fiction genre.” The explanation is grounded in features that exist in the data and that the user can verify. This explainability is valuable in regulated domains (where recommendation systems must justify their outputs), in trust-sensitive applications (medical, financial), and in any setting where users want to understand and correct the model’s view of them.
2. The Three-Step Procedure
Every content-based recommender, regardless of feature representation or similarity metric, performs the same three steps. Understanding this skeleton is more important than memorizing any particular implementation.
flowchart LR Items --> Feat[Step 1: Featurize] Feat --> ItemVecs[Item vectors v_i] Hist[User's past liked items + ratings] --> Agg[Step 2: Aggregate into profile] ItemVecs --> Agg Agg --> UProf[User profile vector p_u] UProf --> Sim[Step 3: Score by similarity] ItemVecs --> Sim Sim --> Top[Top-N ranked items]
What this diagram shows. The pipeline runs left to right. Step 1 (“Featurize”) takes raw items and produces a numerical vector representation v_i ∈ ℝ^d for each item; the dimensionality d is the number of features (or the dimension of the embedding model’s output). Step 2 (“Aggregate into profile”) combines a user’s interaction history (the items they have liked, optionally weighted by their preference strength) with the items’ feature vectors to produce a single profile vector p_u ∈ ℝ^d that summarizes what kinds of items the user prefers. Step 3 (“Score by similarity”) computes a similarity score between p_u and every candidate item vector v_i, ranks the items by that score, and returns the top N. The key insight from this diagram is that the user profile lives in the same space as the item vectors — the model is essentially placing the user as a point in feature space and recommending items that are nearby.
2.1 Step 1 — Featurize the catalog
Each item is converted to a vector v_i ∈ ℝ^d. The choice of feature representation is the most consequential design decision in any content-based system; the rest of the pipeline is largely standardized.
For text-rich items (news articles, movie synopses, product descriptions), the classical representation is TF-IDF, which is defined precisely in §3 below. The TF-IDF vector is sparse and high-dimensional (typically tens of thousands of terms, mostly zero), and it captures topical content via term-occurrence statistics.
The modern alternative is a dense embedding from a pretrained model. Sentence-transformers (Reimers & Gurevych 2019, “Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks”) produces 384- or 768-dimensional vectors that capture semantic similarity rather than just lexical overlap. Two synopses that describe the same plot using different words will have similar sentence-transformer embeddings even when their TF-IDF vectors share no terms. For images, OpenAI’s CLIP (Contrastive Language-Image Pretraining) produces a unified text/image embedding space that supports cross-modal comparison.
For tabular metadata (genre, director, cast), one-hot or multi-hot encodings are common, sometimes augmented with embedding lookups for high-cardinality categorical features (e.g., learned embeddings for individual actors).
For audio (music recommendation), pretrained audio embeddings from CLAP (Contrastive Language-Audio Pretraining), VGGish, or wav2vec are standard.
For code (e.g., a code-snippet recommender), abstract syntax tree (AST) features from tree-sitter or learned code embeddings (CodeBERT, StarCoder embeddings) are options.
2.2 Step 2 — Build the user profile
The user’s profile vector p_u is some aggregation of the feature vectors of items the user has previously preferred. The simplest aggregation is the unweighted mean:
p_u = (1 / |L_u|) · Σ_{i ∈ L_u} v_i
where:
L_uis the set of items the user has previously liked. The definition of “liked” depends on the data: rated ≥ 4 stars in explicit-feedback systems, watched to completion in implicit-feedback systems, purchased in e-commerce.|L_u|is the size of the set, used to normalize so heavy users do not get profile vectors with larger magnitudes (which can distort similarity).v_iis the feature vector for itemi.
The unweighted mean treats every liked item as equally informative about the user’s taste. A more nuanced version weights by the preference strength:
p_u = (1 / Σ_{i ∈ L_u} w_{u,i}) · Σ_{i ∈ L_u} w_{u,i} · v_i
where w_{u,i} is the user’s preference for item i (the rating, the watch-time, or any other strength signal). This emphasizes items the user liked more.
When ratings are available, they should be mean-centered per user before being used as weights:
w_{u,i} = r_{u,i} − r̄_u
where r̄_u is the mean of user u’s ratings. The reason is that some users rate everything 4–5 stars while others use the full 1–5 range; without centering, the always-high rater’s profile is contaminated by every item they have touched, even ones they merely tolerated. Centering subtracts the user’s personal baseline and isolates their relative preferences.
For implicit feedback, time-decay is a common refinement: recent interactions weight more than old ones. The decay is usually exponential:
w_{u,i} = e^{−λ · age(i)}
where age(i) is the time since the interaction (in days, weeks) and λ is a decay rate. With λ = ln(2) / 30, an interaction from 30 days ago has half the weight of one from today.
2.3 Step 3 — Score candidates
For every candidate item i ∉ L_u (every item the user has not seen), compute the similarity between p_u and v_i. Rank items by similarity and return the top N. The dominant similarity measure is cosine similarity, defined precisely in §3.
3. The Mathematical Tools — TF-IDF and Cosine Similarity
The two pieces of standard machinery used in nearly every text-based content recommender deserve a careful treatment because they are easy to misuse.
3.1 TF-IDF (Term Frequency–Inverse Document Frequency)
TF-IDF is a numerical statistic that quantifies how important a term is to a particular document within a collection of documents. It is the product of two quantities:
Term Frequency (TF) measures how frequently a term t occurs in a particular document d. The simplest definition is the raw count, tf(t, d) = count(t, d). More commonly, one normalizes by document length (count / total_terms) or applies a logarithmic transformation (1 + log(count)) to dampen the influence of very frequent terms. The intuition: terms that appear many times in a document are likely important to the document’s topic.
Inverse Document Frequency (IDF) measures how rare a term is across the entire collection of documents. Its definition is:
idf(t, D) = log( |D| / |{d ∈ D : t ∈ d}| )
where:
Dis the collection of all documents (the entire item catalog in a recommender context).|D|is the total number of documents.|{d ∈ D : t ∈ d}|is the number of documents in which the termtappears at least once. This is the document frequency oft.- The logarithm dampens the score so that a term that appears in 1 out of 1 million documents does not get an astronomically larger weight than a term that appears in 100 out of 1 million.
The intuition: terms that appear in many documents are common and uninformative (“the”, “and”, “of”); terms that appear in few documents are rare and discriminative (“Villeneuve”, “Tarkovsky”, “panoptic”). IDF up-weights the rare terms and down-weights the common ones.
The TF-IDF score for term t in document d within collection D is:
tfidf(t, d, D) = tf(t, d) · idf(t, D)
A document is then represented as a vector indexed by all terms in the vocabulary, where the entry for term t is tfidf(t, d, D) if the term appears in the document and 0 otherwise. The vectors are very high-dimensional (tens of thousands of terms) and very sparse (most entries are zero for any given document).
The Wikipedia article on TF-IDF covers several variants in detail; the definition above is the standard form used in scikit-learn’s TfidfVectorizer and most production systems.
3.2 Cosine similarity
Cosine similarity measures the angle between two vectors, ignoring their magnitudes. The formula is:
cos(θ) = (A · B) / (‖A‖ · ‖B‖)
where:
AandBare vectors in ℝ^d.A · B = Σ_{i=1}^{d} A_i · B_iis the dot product (sum of element-wise products).‖A‖ = √(A · A) = √(Σ A_i²)is the L2 norm (Euclidean length) ofA.- The result is in
[−1, 1]for general vectors, and in[0, 1]when both vectors are non-negative (which is the case for TF-IDF vectors). - A value of 1 means the vectors point in exactly the same direction (most similar); 0 means orthogonal (no shared topic); −1 means opposite (only meaningful for centered or signed vectors).
The reason cosine is preferred over Euclidean distance for text vectors: Euclidean distance penalizes magnitude differences, but a 1000-word article and a 100-word summary about the same topic have very different magnitudes (the long article has more total term-counts) even though their topic distributions are nearly identical. By dividing by the norms, cosine measures only the direction, which is the useful signal. The Wikipedia article on Cosine similarity discusses the geometric interpretation and the connection to Pearson correlation in detail.
4. A Worked Implementation
A minimal but working content-based recommender in Python using scikit-learn:
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# items is a list of dicts with keys: 'item_id', 'description'
# ratings is a list of dicts with keys: 'user_id', 'item_id', 'rating'
# --- Step 1: featurize the catalog ---
descriptions = [item["description"] for item in items]
vectorizer = TfidfVectorizer(
stop_words="english", # remove "the", "a", "and", etc.
max_features=20_000, # cap the vocabulary
ngram_range=(1, 2), # include unigrams and bigrams
)
item_matrix = vectorizer.fit_transform(descriptions) # shape: n_items × n_terms (sparse)
# Index lookup so we can map item_id to row index
item_id_to_idx = {item["item_id"]: i for i, item in enumerate(items)}
def recommend_for_user(user_id, top_n=10):
# --- Step 2: build the user profile ---
user_ratings = [r for r in ratings if r["user_id"] == user_id and r["rating"] >= 4]
if not user_ratings:
return None # cold user — fall back to popularity (out of scope here)
liked_indices = [item_id_to_idx[r["item_id"]] for r in user_ratings]
user_vector = item_matrix[liked_indices].mean(axis=0) # 1 × n_terms
# --- Step 3: score every item against the profile ---
scores = cosine_similarity(np.asarray(user_vector), item_matrix).flatten()
# Mask out items the user has already liked so they aren't re-recommended
seen_mask = np.zeros(len(items), dtype=bool)
seen_mask[liked_indices] = True
scores[seen_mask] = -np.inf
top_indices = np.argsort(scores)[::-1][:top_n]
return [items[i] for i in top_indices]Walking through what this does:
TfidfVectorizer(stop_words="english")— this is a pre-built scikit-learn class that tokenizes each description into terms, removes English stop words (common closed-class words like “the”, “a”, “and” that carry no topical content), and constructs the TF-IDF matrix. Themax_features=20_000caps the vocabulary at the 20,000 most common terms across the catalog, which controls memory at modest cost in coverage. Thengram_range=(1, 2)instructs the vectorizer to include both unigrams (single terms like “science”) and bigrams (two-term sequences like “science fiction”), which captures simple compositional meaning that single terms miss.item_matrixis a sparse matrix where rowiis the TF-IDF vector for itemi, and columnjcorresponds to termjin the learned vocabulary. The matrix is sparse because most items use only a small fraction of the vocabulary.- The user vector is the mean of the rows for liked items. This is the unweighted mean; a production system would weight by ratings or watch-time and time-decay older interactions.
cosine_similarity(user_vector, item_matrix)computes the cosine between the 1-row user vector and every row of the item matrix, returning a 1-D array of similarity scores.- The
seen_maskstep is essential — without it, the recommender would return items the user has already liked at the top of the list (because they are in the profile). Masking liked items gives the user something genuinely new. argsortreturns indices that would sort the scores;[::-1][:top_n]reverses (so highest scores come first) and slices to N.
The whole thing is roughly twenty lines and is a complete content-based recommender. Most of the engineering effort in production goes into the featurization step (Step 1) — picking the right embeddings, handling missing descriptions, normalizing ratings — not the similarity computation.
5. Choosing the Feature Representation
The design decision that most affects quality is the feature representation. A short tour of what works for different item types in mid-2020s practice:
Text-heavy items (articles, blogs, reviews, product pages, social posts). Sentence-transformer embeddings (specifically all-MiniLM-L6-v2 for speed or all-mpnet-base-v2 for quality) are the modern default. They produce 384- or 768-dimensional dense vectors that capture semantic similarity. TF-IDF remains a competitive baseline and is much cheaper to compute.
Movies and TV. A composite representation of metadata (genre as multi-hot, cast as bag-of-actors with embedding lookup, director as embedding) plus a sentence-transformer embedding of the synopsis. The composite is concatenated or learned as a multi-task model.
E-commerce products. Title and description embedding from a sentence-transformer, plus structured attributes (category, price bucket, brand). For visually-driven categories (apparel, home goods) a CLIP image embedding of the primary product image is added.
Music. Pretrained audio embeddings from CLAP (Wu, Chen, Zhang, Berg, Le, Kong, Berg-Kirkpatrick 2023) or VGGish (Hershey et al. 2017), plus tags and artist embeddings learned over user behaviour.
News. Often dominated by recency and topical match. Sentence-transformer embeddings of the lead paragraphs work well; a topic model (LDA, Latent Dirichlet Allocation; or BERTopic) layered on top supports faceted recommendation.
Code (e.g., snippet recommendation, library suggestion). Tree-sitter AST features for syntactic structure, plus learned code embeddings (CodeBERT, UniXcoder).
The general trend across all item types is that hand-engineered features have been displaced by pretrained embeddings from large foundation models. A 768-dimensional sentence-transformer vector, computed with one forward pass, captures more semantic structure than any reasonable hand-crafted feature set. The cost is interpretability — a sentence-transformer dimension does not correspond to any human-readable concept — but for raw recommendation accuracy, this trade-off is almost always worth it.
6. Strengths
No new-item cold start. This is the foundational selling point. A new item is featurizable from day one and is therefore scoreable from day one. See Cold Start Problem §4.1.
Single-user operation. The model needs only one user’s history. There is no dependency on other users, which makes the approach viable for low-density platforms (the system has very few users yet) and for privacy-sensitive applications where exposing other users’ behaviour is unacceptable.
Explainability. Recommendations can be justified in terms of concrete features. “Recommended because you liked Arrival and Blade Runner 2049, which share science-fiction themes and the director Denis Villeneuve” is an explanation a user can verify. Compare to a collaborative-filtering recommendation justified only as “users who liked X also liked Y”, which is less concrete.
Privacy compatibility. Because the model operates on one user’s data plus a public catalog of features, it can in principle be deployed entirely on-device. This is the architecture used by Apple’s News recommender and by some of the privacy-preserving recommendation prototypes in the federated-learning literature.
Robust to sparsity in user history. Even a user with only one or two rated items can get a meaningful (if narrow) recommendation, because the user vector is just a mean of feature vectors and is well-defined for any non-empty L_u.
7. Weaknesses
Filter bubble and over-specialization. Because the model only knows what the user has already liked, every recommendation is a small neighbourhood around the user’s past behaviour. A user who has watched three thrillers will get only thrillers — not the documentary they would also love, not the comedy that complements their taste, not the cross-genre item that an aware human curator would suggest. This is the over-specialization failure mode, and it is the main reason content-based methods are paired with collaborative methods in production. See Beyond-Accuracy Objectives for the diversity techniques used to mitigate this.
Limited by feature quality. If your features do not capture why users like things, neither will the recommender. A movie’s vibe, humour, pacing, and cinematography are all hard to encode as features; two films that share genre and cast can have very different user appeal because of factors not visible in the metadata. Content-based recommenders on weak features produce weak recommendations regardless of the algorithmic sophistication.
No user cold start mitigation. The model needs at least one prior interaction to define p_u. A brand-new user with no history has no profile vector and no recommendations beyond what an onboarding questionnaire or popularity baseline provides.
Misses cross-domain affinities. Patterns like “people who like obscure post-rock also like Russian sci-fi novels” are visible only in co-occurrence across users; they cannot be derived from features. Content-based recommenders are blind to such affinities.
Featurization is engineering-intensive. Picking the right featurization, handling missing descriptions, dealing with multilingual content, normalizing across feature types — these are non-trivial engineering problems that have to be solved upfront and maintained.
8. Pitfalls and Misconceptions
“Content-based filtering doesn’t need ML.” Wrong. The featurization step is itself a major ML problem — it determines whether the model captures the relevant aspects of the content. Modern systems use pretrained transformer embeddings (sentence-transformers, CLIP) as the featurizer, which are very much ML models with hundreds of millions of parameters.
Stop-word leakage in TF-IDF. Forgetting to remove stop words means that “the”, “a”, and “and” dominate the similarity computation simply because they appear in every document. Always pass stop_words="english" or a domain-specific stop-list. Test by inspecting the top-weighted terms in a few sample document vectors.
Profile staleness. A profile built from 3-year-old likes does not reflect current taste. Production systems either time-decay older interactions (exponential decay with a half-life of weeks to months, as discussed in §2.2) or use only the most recent N interactions.
Treating ratings as weights without normalization. A user who only ever rates 5s (because they only rate things they liked) will have a profile that uses every item they have ever touched as a positive. Mean-center the ratings per user before using them as weights. This separates relative preferences from absolute ones.
Mismatched feature scales. Concatenating a multi-hot genre vector (mostly zeros and ones) with a TF-IDF vector (real-valued, often small) and a CLIP image embedding (dense, unit-norm) and treating them as a single feature space distorts the similarity computation because the magnitudes are so different. Either standardize each feature group separately, learn separate towers, or use a model that handles mixed inputs natively.
Cosine similarity on un-normalized integer counts. Cosine implicitly normalizes vectors to unit length. If the input vectors are TF-IDF, the normalization is appropriate. If the input vectors are raw integer counts, the cosine result is dominated by the longer documents in subtle ways, and TF or TF-IDF is preferable.
Assuming explainability is automatic. A content-based recommender can produce explanations, but only if the engineering team builds the explanation surface. The model outputs a similarity score; turning that score into “because you liked X and Y, which share genre Z” requires extracting the contributing terms or features and rendering them in a UI. This is not free.
9. Open Questions
- Foundation-model embeddings as the universal featurizer. Can pretrained CLIP / sentence-transformer / audio embeddings replace 90%+ of feature engineering across all item types? The trend says yes; the remaining cases are usually domain-specific (very long documents, multi-modal items) where the foundation model needs adaptation.
- Counteracting over-specialization without sacrificing relevance. Re-ranking for diversity is the standard answer (see Beyond-Accuracy Objectives), but the right trade-off between relevance and diversity is application-specific and not well-quantified.
- On-device content-based recommenders for privacy. With foundation embeddings becoming small enough to run on a phone, fully on-device content recommenders are increasingly feasible. The blocker is usually engineering complexity (managing the embedding update pipeline) rather than accuracy.
10. See Also
- Recommender Systems — the umbrella where content-based is the first paradigm introduced
- Collaborative Filtering — the complementary paradigm content-based is usually paired with
- Hybrid Recommender Systems — formal taxonomy for combining content and collaborative methods
- Cold Start Problem — content-based filtering is the canonical fix for the new-item case
- Building a Simple Recommender — the staged code progression includes a content-based step
- Two-Tower Retrieval Model — modern industrial content-based-style retrieval at scale
- Recommender Systems MOC