Building Content-Based Recommendation Engines: A Practical Guide

Building Content-Based Recommendation Engines

AI Development Areas

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1284
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1240
  • image_logo-advance_0.webp
    B2B Advance company logo design
    696
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_logo-aider_0.webp
    AIDER company logo development
    917
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1031

Building Content-Based Recommendation Engines

A new platform with video tutorials struggles with user retention: 80% churn in the first week. We solve this with a content-based recommendation system that doesn't require other users' data and delivers relevant recommendations after just 3–5 interactions. Our experience: over 50 deployed recommender systems for e-commerce, media, and EdTech. Our engineers are certified in MLOps and NLP, ensuring quality at every stage.

How a Content-Based Recommendation System Solves Cold Start

Content-based filtering builds a user profile solely from the attributes of items the user has interacted with. The method uses semantic text embeddings, TF-IDF, categorical and numerical features, combining them into a unified multimodal profile. Precision@10 on new users reaches 0.15–0.30, which is 10–30 times better than the popular popularity baseline (0.01–0.03).

Architecture of the Content-Based Recommendation System

Multimodal Content Profile: Step-by-Step Approach — Building the Recommendation Engine

  1. Data collection: item_id, title, description, categories, tags, price, rating, and other numeric features.
  2. Text vectorization: we use multilingual sentence-transformers (paraphrase-multilingual-mpnet-base-v2), obtaining 768-dimensional embeddings.
  3. TF-IDF features: additionally extract 5000 n-grams (1–2), compress to 50 components via SVD.
  4. Categorical features: binarize using MultiLabelBinarizer.
  5. Numerical features: StandardScaler for price, rating, review_count, release_year.
  6. Weighted concatenation: text (weight 2), TF-IDF (0.5), categories (1.0), numbers (0.3). Result is L2-normalized.
Click to expand code
import numpy as np import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.preprocessing import MultiLabelBinarizer, StandardScaler from sklearn.metrics.pairwise import cosine_similarity import torch from sentence_transformers import SentenceTransformer class ContentBasedRecommender: def __init__(self): self.text_model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2') self.tfidf = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) self.mlb = MultiLabelBinarizer() self.scaler = StandardScaler() self.item_vectors = None self.item_ids = [] def build_item_profiles(self, items_df: pd.DataFrame) -> np.ndarray: """ items_df: item_id, title, description, category (list), tags (list), price, rating, release_year, ... """ feature_parts = [] # 1. Semantic text embeddings (description + title) texts = ( items_df['title'].fillna('') + '. ' + items_df.get('description', pd.Series([''] * len(items_df))).fillna('') ).tolist() print(f"Encoding {len(texts)} items...") text_embeddings = self.text_model.encode( texts, batch_size=64, show_progress_bar=True, convert_to_numpy=True, normalize_embeddings=True ) feature_parts.append(text_embeddings) # 2. TF-IDF features from text tfidf_features = self.tfidf.fit_transform(texts).toarray() # PCA for compression from sklearn.decomposition import TruncatedSVD svd = TruncatedSVD(n_components=50) tfidf_compact = svd.fit_transform(tfidf_features) feature_parts.append(tfidf_compact) # 3. Categorical features if 'categories' in items_df.columns: cat_features = self.mlb.fit_transform( items_df['categories'].apply(lambda x: x if isinstance(x, list) else []) ) feature_parts.append(cat_features.astype(float)) # 4. Numerical features num_cols = ['price', 'rating', 'review_count', 'release_year'] available_num = [c for c in num_cols if c in items_df.columns] if available_num: num_features = self.scaler.fit_transform( items_df[available_num].fillna(items_df[available_num].median()) ) feature_parts.append(num_features) # Weighted concatenation weights = [2.0, 0.5, 1.0, 0.3][:len(feature_parts)] weighted_parts = [p * w for p, w in zip(feature_parts, weights)] combined = np.hstack(weighted_parts) # L2 normalization norms = np.linalg.norm(combined, axis=1, keepdims=True) self.item_vectors = combined / (norms + 1e-10) self.item_ids = items_df['item_id'].tolist() return self.item_vectors def build_user_profile(self, liked_items: list[str], weights: list[float] = None) -> np.ndarray: """User profile as weighted average of liked items""" item_indices = [ self.item_ids.index(item_id) for item_id in liked_items if item_id in self.item_ids ] if not item_indices: return None liked_vectors = self.item_vectors[item_indices] if weights is not None and len(weights) == len(item_indices): w = np.array(weights[:len(item_indices)]).reshape(-1, 1) user_vector = np.average(liked_vectors, axis=0, weights=w.flatten()) else: # Recent interactions have higher weight recency_weights = np.exp(np.linspace(-1, 0, len(item_indices))) user_vector = np.average(liked_vectors, axis=0, weights=recency_weights) norm = np.linalg.norm(user_vector) return user_vector / (norm + 1e-10) def recommend(self, user_profile: np.ndarray, exclude_items: list[str] = None, n: int = 10, diversity_penalty: float = 0.1) -> list[tuple]: """Recommendations with MMR diversity penalty""" if user_profile is None: return [] # Base scores scores = cosine_similarity( user_profile.reshape(1, -1), self.item_vectors )[0] # Exclude already seen items if exclude_items: for item_id in exclude_items: if item_id in self.item_ids: idx = self.item_ids.index(item_id) scores[idx] = -1 # MMR (Maximal Marginal Relevance) for diversity selected_indices = [] selected_embeddings = [] while len(selected_indices) < n: if not selected_embeddings: # First: most relevant best_idx = np.argmax(scores) else: # Subsequent: relevance minus similarity penalty to already chosen selected_matrix = np.vstack(selected_embeddings) similarity_to_selected = cosine_similarity( self.item_vectors, selected_matrix ).max(axis=1) adjusted_scores = scores - diversity_penalty * similarity_to_selected # Mask already selected for idx in selected_indices: adjusted_scores[idx] = -1 best_idx = np.argmax(adjusted_scores) if scores[best_idx] < 0: break selected_indices.append(best_idx) selected_embeddings.append(self.item_vectors[best_idx]) return [ (self.item_ids[idx], float(scores[idx])) for idx in selected_indices ] def item_to_item(self, item_id: str, n: int = 10) -> list[tuple]: """Similar items for product page""" if item_id not in self.item_ids: return [] item_idx = self.item_ids.index(item_id) item_vector = self.item_vectors[item_idx] scores = cosine_similarity( item_vector.reshape(1, -1), self.item_vectors )[0] scores[item_idx] = -1 # Exclude itself top_indices = np.argsort(scores)[-n:][::-1] return [(self.item_ids[idx], float(scores[idx])) for idx in top_indices] 

Real-Time Profile Update

Click to expand code
class OnlineUserProfileUpdater: """Incremental profile update without full rebuild""" def __init__(self, recommender: ContentBasedRecommender): self.rec = recommender self.user_profiles = {} self.user_history = {} def update_on_interaction(self, user_id: str, item_id: str, interaction_type: str): """Update profile after interaction""" weights = { 'view': 1.0, 'click': 1.5, 'add_to_cart': 3.0, 'purchase': 5.0, 'dislike': -2.0, 'skip': -0.5 } weight = weights.get(interaction_type, 1.0) if user_id not in self.user_history: self.user_history[user_id] = [] self.user_history[user_id].append({ 'item_id': item_id, 'weight': weight, 'interaction_type': interaction_type }) # Recalculate profile (last 50 interactions) history = self.user_history[user_id][-50:] liked = [h['item_id'] for h in history if h['weight'] > 0] liked_weights = [h['weight'] for h in history if h['weight'] > 0] if liked: self.user_profiles[user_id] = self.rec.build_user_profile( liked, liked_weights ) def get_recommendations(self, user_id: str, n: int = 10) -> list[tuple]: profile = self.user_profiles.get(user_id) if profile is None: return [] exclude = [h['item_id'] for h in self.user_history.get(user_id, [])] return self.rec.recommend(profile, exclude_items=exclude, n=n) 

Content-based filtering with sentence-transformers achieves Precision@10 = 0.15–0.30 on new users (versus 0.01–0.03 for popular items). A profile can be built from as few as 3–5 interactions. Inference: SentenceTransformer encoding of 10K items takes 5–10 minutes on CPU, 30–60 seconds on GPU.

Performance comparison tables
Metric Content-Based (new users) Popular Baseline Improvement
Precision@10 0.15–0.30 0.01–0.03 10–30 times better
Recall@10 0.10–0.25 0.005–0.01 10–25 times better
Coverage 80%+ 1–5% 10–30 times better

Why Content-Based Filtering Beats Collaborative Filtering for New Projects

On platforms with few users or high content turnover, Collaborative Filtering suffers from sparse interaction matrices. According to Wikipedia, collaborative filtering requires large interaction matrices, while content-based filtering can operate with sparse data. Content-Based uses rich metadata and doesn't suffer from cold start—relevant results appear after the first click. For example, in a niche online cinema with 5000 movies and 200 active users, content-based filtering gives 80%+ coverage, which is up to 16 times better than collaborative filtering's 5–20% coverage. In terms of catalog coverage, content-based filtering (80%+) outperforms collaborative filtering (5-20%) by up to 16x. Average system payback period is 6–12 months due to reduced churn. For a platform with 10,000 users, reducing churn by 30% can save $1,000 monthly.

Characteristic Content-Based Collaborative Filtering
Dependency on other users No Yes
Cold-start requirement 3–5 actions Requires lots of data
Diversity (coverage) 80%+ 5–20%
Serendipity Low High
Computational complexity O(N * F) O(users * items)

What's Included in the Work

  • Documentation on architecture, input data, and API endpoints.
  • Source code for the vectorization pipeline and recommendation module.
  • Access to trained model weights and vector indices.
  • Training of your team for administration and model updates.
  • Support for the first month after deployment (incident resolution, optimization).

Estimated Timeline and Cost

Realization time depends on data volume and integration complexity. A typical project takes from 4 to 8 weeks and starts at $15,000. With an average item price of $50, a 10% increase in click-through rate can generate an additional $1,000 daily revenue per 10,000 users. For an accurate estimate, send us a description of your domain and catalog size—we will calculate cost and timeline within one working day. Contact us—we will analyze your data and find the optimal architecture.

Get a consultation: we'll show you how content-based filtering affects your funnel. The system significantly boosts engagement during onboarding when users have no history. With our implementation, you get quality assurance: we've already encountered all typical pitfalls—from embedding selection to diversity penalty calibration. Over 50 projects, 7+ years of experience, and certified engineers—that's why clients trust us.