Building Content-Based Recommendation Engines: A Practical Guide

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1All 1564 services
Building Content-Based Recommendation Engines: A Practical Guide
Medium
~1-2 weeks
Frequently Asked Questions

AI Development Areas

AI Solution Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1347
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1247
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    948
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1183
  • image_logo-advance_0.webp
    B2B Advance company logo design
    642
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    921

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.

Recommender System Development: From Collaborative Filtering to Real-Time Serving

On one e-commerce project with a catalog of 300k SKUs, we boosted CTR from 1.8% to 4.4% — a 2.4x increase. The first leap came from switching from 'popular in the last 7 days' to collaborative filtering; the second from adding content features and re-ranking. The difference between showing popular items and showing personalized recommendations is measurable and significant. Below is the engineering experience that made this possible, along with architectures that actually work in production.

Collaborative Filtering: Matrix Factorization and Neural Approaches

Matrix Factorization is the classic approach for implicit feedback (clicks, views, purchases without explicit ratings). ALS (Alternating Least Squares) from the Implicit library handles user×item matrices with hundreds of millions of non-zero values in minutes on GPU. Latent factors 64–256, regularization λ=0.01–0.1 are starting parameters. Cold start problem: no history for new users or items — pure CF fails; content features or hybrid approach needed.

Neural Collaborative Filtering (NCF) replaces the dot product with a neural network. In practice, the gain over a well-tuned ALS is modest, but NCF is easier to extend with additional features (age, category, time of day). Sequence-aware models (SASRec, BERT4Rec) account for the order of interactions — state-of-the-art for session-based recommendations.

How to Choose Recommender System Architecture?

The answer depends on data, load, and cold start requirements. Below are three main approaches with selection criteria.

Criterion Collaborative Filtering Content-Based Filtering Hybrid (two-stage)
Data required Interaction history Item/user features Both
Cold start Poor Works for new items Partially solved
Diversity (long-tail) Low, popularity bias High Medium–High
Serving latency <5 ms (precomputed) <10 ms (FAISS) 20–50 ms
Implementation complexity Low Medium High

Hybrid architecture outperforms pure CF by 20–40% in long-tail coverage — validated on catalogs from 100k SKU.

Content-Based Filtering: When Interaction History is Scarce

Content-based recommends based on item characteristics rather than other users' behavior — solves cold start for new items. Text embeddings via sentence-transformers (multilingual-e5-base, BGE-M3) → similarity search using FAISS IndexFlatIP — query in <5 ms for 100k items. Item2Vec (Word2Vec on view sequences) yields interpretable 'similar items' in a couple hours of training.

Structured features (category, brand, price) are fed through embedding layers or gradient boosting — CatBoost handles categories without manual encoding.

Why Hybrid Models Work Better?

Production systems are almost always two-level. Stage 1 (Retrieval) — fast selection of 100–500 candidates from 300k items using ALS or Two-Tower model with vector search (FAISS, Qdrant). Stage 2 (Ranking) — heavy ranker on LightGBM or neural network with cross-features, time, device, and session context. LightFM is a good starting point for medium scale without heavy infrastructure. Our practice shows: moving from single-stage to two-stage yields a 15–25% accuracy improvement with only 20–30 ms additional latency.

Real-Time Serving: Architecture Under Load

Latency SLA — 50–100 ms at thousands of requests per second. Base recommendations precomputed (batch job hourly) → Redis by user_id → <5 ms. Real-time re-ranking via Kafka for events (clicks, cart adds) → update of context features. Feature serving — Redis with TTL (views in 24 hours, last clicked item). At 10k req/s, we deploy Redis Cluster with replication.

A/B testing is the only reliable way to measure improvements. Offline metrics do not always correlate with online. Kohavi et al., 'Online Controlled Experiments at Large Scale' (KDD 2013) — a must-read for the team. Test on 5–10% of traffic, monitor CTR, conversion, revenue per session. One of our client systems after hybridization increased revenue by 18% over a month of A/B.

Recommender System Development Timeline

The stages and typical time frames are in the table below. Costs are calculated individually based on catalog scale and latency requirements.

Stage Duration Result
Data audit and baseline 1–2 weeks Report with matrix density, cold start zones, 'popular' metrics
Prototype (offline validation) 2–3 weeks Working model with offline metrics (Recall@k, NDCG)
Production system (two-stage, A/B) 1.5–2.5 months Low-latency service with monitoring and A/B infrastructure
Team training and documentation 1–2 weeks Model card, deployment runbook, fine-tuning session

What's Included in Turnkey Development

  1. Data audit — user×item matrix density (typically <0.1%), activity distribution, temporal patterns, cold start statistics.
  2. Baseline — 'popular' as a simple threshold that is often hard to beat.
  3. Iterative improvement — ALS → content features → two-stage → sequence-aware. Each step with A/B.
  4. Serving infrastructure — batch precomputation, Redis, real-time re-ranking, Grafana monitoring.
  5. Documentation — model card with metrics, deployment instructions, feature descriptions.
  6. Team training — session on interpreting results and model fine-tuning.
  7. Support — 1 month post-launch (incident fixes, pipeline tuning).

We are a team with 7+ years of experience in recommender systems, having delivered over 30 projects for e-commerce and media. We guarantee transparent A/B testing and documented metric improvements.

Want to assess the growth potential of your catalog? Contact us for a free data audit. Order recommender system development — first prototype within two weeks.

Example ALS config for implicit feedback
from implicit.als import AlternatingLeastSquares

model = AlternatingLeastSquares(
    factors=64,
    regularization=0.05,
    iterations=15,
    use_gpu=True
)
model.fit(user_item_matrix)

More about the mathematics of recommender systems — in specialized literature.