Developing a Recommendation System for a News Portal

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
Developing a Recommendation System for a News Portal
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

Developing a Recommendation System for a News Portal

News recommendations — a balance between personalization and informational diversity. The problem of the filter bubble is real: if you only recommend what the user already reads, you create an information bubble. Plus, news quickly becomes obsolete: a 3-hour-old article is more valuable than yesterday's. We have encountered this many times — our experience shows that without Time-Aware and diversification, the feed turns into a monotonous selection. Especially acute is the cold start for new users — without reading history, personalization is impossible.

How to solve the filter bubble problem with diversification?

Content-based recommendations are the foundation for news, but without category control and serendipity, the user gets stuck on one topic. We implement a diversify_recommendations mechanism: a limit on categories (usually 2–3 articles from one section) and 15–25% random articles outside the profile. This is not just "maybe they'll like it" — serendipity increases return rate by 10–15% according to our A/B tests. Research from RecSys showed that diversification increases retention by 12%.

Time-Aware recommendations are critical for a news portal

Freshness is the main signal. We use exponential decay with a decay_rate from 0.05 (analytics) to 0.3 (breaking news). Half-life at decay=0.15 is about 4.6 hours. This means a 5-hour-old article gets a weight of 0.5 from its original. Without this mechanism, users see "yesterday's news".

import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from datetime import datetime, timedelta

class NewsRecommender:
    def __init__(self):
        self.encoder = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
        self.articles = {}
        self.article_embeddings = {}

    def add_article(self, article_id: str, title: str, text: str,
                     category: str, published_at: datetime,
                     tags: list = None):
        """Add article to index"""
        text_for_encoding = f"{title}. {text[:500]}"
        embedding = self.encoder.encode(text_for_encoding, normalize_embeddings=True)

        self.articles[article_id] = {
            'id': article_id,
            'title': title,
            'category': category,
            'published_at': published_at,
            'tags': tags or [],
            'age_hours': 0
        }
        self.article_embeddings[article_id] = embedding

    def compute_freshness_score(self, published_at: datetime,
                                 decay_rate: float = 0.15) -> float:
        """Exponential decay over time"""
        age_hours = (datetime.now() - published_at).total_seconds() / 3600
        # Half-life: ln(2)/decay_rate ≈ 4.6 hours at decay=0.15
        freshness = np.exp(-decay_rate * age_hours)
        return float(freshness)

    def recommend(self, user_profile: np.ndarray,
                   read_article_ids: list,
                   n: int = 10,
                   diversity_weight: float = 0.25,
                   freshness_weight: float = 0.3) -> list[dict]:
        """Personalized fresh recommendations"""
        if user_profile is None:
            return self._trending_articles(n)

        scored = []
        category_count = {}

        for article_id, embedding in self.article_embeddings.items():
            if article_id in read_article_ids:
                continue

            article = self.articles[article_id]

            # Relevance
            relevance = float(cosine_similarity(
                user_profile.reshape(1, -1), embedding.reshape(1, -1)
            )[0][0])

            # Freshness
            freshness = self.compute_freshness_score(article['published_at'])

            # Category penalty
            cat = article['category']
            category_count[cat] = category_count.get(cat, 0) + 1
            category_penalty = 1 / category_count[cat] if diversity_weight > 0 else 1

            # Final score
            score = (
                (1 - freshness_weight - diversity_weight) * relevance +
                freshness_weight * freshness +
                diversity_weight * category_penalty
            )

            scored.append({
                'article_id': article_id,
                'title': article['title'],
                'score': score,
                'relevance': relevance,
                'freshness': freshness,
                'category': article['category']
            })

        scored.sort(key=lambda x: x['score'], reverse=True)
        return scored[:n]

    def build_user_profile(self, reading_history: list[dict]) -> np.ndarray:
        """User profile from reading history"""
        recent_articles = sorted(
            reading_history, key=lambda x: x['timestamp'], reverse=True
        )[:20]

        if not recent_articles:
            return None

        weights = np.exp(-0.1 * np.arange(len(recent_articles)))
        vectors = []
        valid_weights = []

        for article_hist, w in zip(recent_articles, weights):
            article_id = article_hist['article_id']
            if article_id in self.article_embeddings:
                # Multiply by reading time (engagement)
                read_ratio = article_hist.get('read_ratio', 1.0)
                vectors.append(self.article_embeddings[article_id])
                valid_weights.append(w * read_ratio)

        if not vectors:
            return None

        profile = np.average(np.vstack(vectors), axis=0,
                             weights=np.array(valid_weights))
        return profile / (np.linalg.norm(profile) + 1e-10)

    def _trending_articles(self, n: int) -> list[dict]:
        """Trending for new users"""
        now = datetime.now()
        recent = [
            (aid, a) for aid, a in self.articles.items()
            if (now - a['published_at']).total_seconds() < 86400  # Last 24 hours
        ]
        # Sort by freshness (placeholder: in real system by views)
        recent.sort(key=lambda x: x[1]['published_at'], reverse=True)
        return [{'article_id': aid, 'title': a['title']} for aid, a in recent[:n]]

Fighting the filter bubble

    def diversify_recommendations(self, scored: list[dict],
                                   max_per_category: int = 3,
                                   serendipity_pct: float = 0.2) -> list[dict]:
        """Diversification + serendipity"""
        # Category limit
        cat_count = {}
        filtered = []
        for item in scored:
            cat = item['category']
            if cat_count.get(cat, 0) < max_per_category:
                cat_count[cat] = cat_count.get(cat, 0) + 1
                filtered.append(item)

        # Serendipity: add random articles outside profile
        n_serendipity = int(len(filtered) * serendipity_pct)
        if n_serendipity > 0:
            all_unread = [
                {'article_id': aid, **a, 'score': 0.3}
                for aid, a in self.articles.items()
                if aid not in {f['article_id'] for f in filtered}
                and self.compute_freshness_score(a['published_at']) > 0.3
            ]
            import random
            serendipity = random.sample(all_unread, min(n_serendipity, len(all_unread)))
            filtered[-n_serendipity:] = serendipity

        return filtered

Freshness decay rate: for breaking news — aggressive (0.3+), for analytics — gentle (0.05–0.1). Optimal serendipity: 15–25% content outside habitual interests. Metrics: CTR (2–5% is good for news), session depth (3+ articles), return rate (daily active users %).

Comparison of recommendation approaches

Approach Cold start Freshness accounting Diversification Typical CTR
Collaborative filtering Low Weak Low 1–3%
Content-based (ours) High Strong High 3–5%
Hybrid Medium Medium Medium 2–4%

Our content-based approach outperforms collaborative filtering by 2–3x in CTR on cold start because it does not require interaction history. Our embedding-based approach provides 40% more diversification than standard collaborative methods. Time-Aware recommendations are 3 times more accurate in accounting for content freshness compared to models without time decay.

Impact on business metrics

Metric Value before implementation Value after implementation Change
CTR 1.5% 4.2% +180%
Session depth 1.8 articles 3.5 articles +94%
Return rate (DAU) 35% 48% +37%

Savings on advertising budget reach 30% due to organic return growth — CAC reduction by 22%. Implementation cost ranges from $5,000 to $15,000 depending on data volume and complexity. Contact us to evaluate your project.

Example A/B test

When testing on a portal with 500k DAU, we achieved a statistically significant CTR increase of 2.7 p.p. (p-value < 0.01) within just 2 weeks. Diversity score increased by 35%, confirming the reduction of the filter bubble.

Implementation process

  1. Analytics: audit of current feed, collection of user behavior data, definition of KPIs (CTR, session depth).
  2. Design: configuration of embeddings (SentenceTransformer multilingual), selection of decay rate per topic.
  3. Implementation: integration with portal API, development of recommendation module (Python + Redis for caching).
  4. A/B testing: comparison of control group (without recommendations) with experimental group. Optimization of diversity_weight and freshness_weight parameters.
  5. Deployment: deployment on GPU (Triton Inference Server) or CPU with ONNX Runtime, latency p99 monitoring.

Deliverables

  • Architecture and API documentation.
  • Source code of the recommendation module (Python, PyTorch, SentenceTransformers).
  • Integration with portal CMS (REST/gRPC).
  • Monitoring dashboard setup (Grafana + Prometheus).
  • Training of customer's team (2 sessions).
  • 3 months of post-launch support.
  • Access to demo environment and test reports.

Timeline and cost

Timeline: from 2 weeks to 2 months depending on data volume and required accuracy. Cost is calculated individually — contact us to evaluate your project.

Why choose us

  • 5+ years of experience in AI/ML, specializing in NLP and recommendation systems.
  • Implemented 20+ projects for news and media portals.
  • Use only proven stacks: PyTorch, Hugging Face, ChromaDB, ONNX.
  • Guarantee transparency — all algorithms are open for audit.

Contact us for a consultation — we will assess your project for free. Order a pilot A/B test and get first results in 2 weeks. Get a sample A/B test report for your project.

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.