惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google DeepMind News
Google DeepMind News
博客园_首页
云风的 BLOG
云风的 BLOG
月光博客
月光博客
I
InfoQ
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
美团技术团队
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
D
Docker
F
Fortinet All Blogs
N
Netflix TechBlog - Medium
博客园 - 叶小钗
Martin Fowler
Martin Fowler
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Claprec: Machine Learning in Practice (4/6)
Kenan Sejmen · 2026-05-18 · via DEV Community

Series Roadmap


Building the Recommendation Engine: A Hybrid Approach to Personalization in Claprec

When building Claprec, the primary challenge wasn't just storing reviews; it was surfacing them. The landing page is the gateway to the platform, and a generic "latest posts" feed simply doesn't cut it when you're trying to build user retention. A system was needed that could handle the cold start problem, respect geographical relevance, and leverage user behavior - specifically, how much time a user spends consuming content.

In this post, I'm going to break down the architecture of the recommendation algorithm that powers the main feed. It's a hybrid system combining collaborative filtering via Matrix Factorization, geo-spatial heuristics, and other factors.


The Architecture: Strategies for Different Contexts

One of the first decisions I made was to avoid a "one size fits all" approach. The backend logic needs to pivot based on the client's state. Specifically, are we dealing with an authenticated user with or without location data, or an anonymous user?

The entry point handles these three distinct scenarios:

  1. Anonymous & No Location: The system defaults to a popularity contest, using recency as a fallback metric.
  2. Authenticated User: The architecture deploys a personalized ML-driven feed that prioritizes interests, followed by popularity and recency.
  3. Authenticated User & Location: The logic applies a personalized ML-driven feed that prioritizes geospatial factors, followed by interests, popularity, and recency.
// HigherCrudServiceReview.cs

public async Task<PaginatedResponse<DtoRelatedReview>> Recommended(BaseSearchDto search, UriBuilder uriBuilder, double? lat, double? lng, User? user)
{
  // ...

  return await PaginatedResultWithAttachedProperties(...);
}

Enter fullscreen mode Exit fullscreen mode

Handling Geo-Spatial Complexity

Before we even get to the Machine Learning, we have to solve the location problem. In Claprec, a review isn't just a point on a map. It can be attached to a specific business address, but it can also be attached to a product that is sold across multiple locations.

This required a robust resolution strategy. We don't just check a single column; we traverse a hierarchy. We aggregate coordinates from the specific BusinessAddress, but if the review targets a product, we fetch all business addresses stocking that product. Furthermore, we resolve user locations not just from GPS pings (lat/lng), but also from Zipcode and City-level data provided in their profile.

// HigherCrudServiceReview.cs

// ...

var reviewsLatitudesLongitudes = await PairReviewsWithTheirUniqueLatsAndLngs(reviews);

var latitudesAndLongitudesOfUser = await PairLatitudesAndLongitudesOfUser(user!.Id);

// ...

Enter fullscreen mode Exit fullscreen mode

// HigherCrudServiceReview.cs

private double? CalculateClosestProximity(double lat, double lng, HashSet<(double, double)> latitudesLongitudes)
{
    if (latitudesLongitudes.Count == 0)
    {
        return null;
    }

    var absLat = Math.Abs(lat);
    var absLng = Math.Abs(lng);

    var proximity = double.MaxValue;

    foreach (var latitudeLongitude in latitudesLongitudes)
    {
        var latDiffAbs = Math.Abs(absLat - Math.Abs(latitudeLongitude.Item1));
        var lngDiffAbs = Math.Abs(absLng - Math.Abs(latitudeLongitude.Item2));

        var proximityOfTuple = Math.Abs(latDiffAbs - lngDiffAbs);

        if (proximityOfTuple < proximity)
        {
            proximity = proximityOfTuple;
        }
    }

    return proximity;
}

Enter fullscreen mode Exit fullscreen mode

This logic ensures that when we calculate proximity, we are matching a user's "area of influence" against a review's "area of availability".


The ML Core: Matrix Factorization on Implicit Feedback

For authenticated users, simple sorting isn't enough. We need personalization. I opted for a collaborative filtering approach using Matrix Factorization, but with a twist: we don't have explicit ratings (1-5 stars) for the act of viewing a review. Instead, we rely on Implicit Feedback - specifically, "Time Spent".

When a user stares at a review for 10, 30, or 60 seconds, that is a signal. We treat this duration as the label for the interaction.

Model Training Pipeline

The model is built using Microsoft.ML. The pipeline is relatively standard but effective. We map the UserId and ReviewId to keys and feed the Label (time spent in seconds) into a MatrixFactorizationTrainer.

// ReviewModel.cs

var estimator = mlContext.Transforms.Conversion.MapValueToKey("UserIdEncoded", "UserId")
    .Append(mlContext.Transforms.Conversion.MapValueToKey("ReviewIdEncoded", "ReviewId"));

var options = new MatrixFactorizationTrainer.Options
{
    MatrixColumnIndexColumnName = "UserIdEncoded",
    MatrixRowIndexColumnName = "ReviewIdEncoded",
    LabelColumnName = "Label",
    NumberOfIterations = 20,
    ApproximationRank = 100
};

Enter fullscreen mode Exit fullscreen mode

We set the ApproximationRank to 100 and run for 20 iterations. This balances latency and the ability to capture complex user-item interactions. Although implicit feedback data can be noisy, we train the model to predict how long a user would spend on a review they haven't seen yet, relying on the algorithm to find signal amidst the noise.

Operationalizing the Model: Retraining Strategy

A model is only as good as its data. Retraining a model on every single interaction is a resource drain. To solve this, I implemented a threshold-based retraining trigger.

We maintain a counter for specific activity types (staring at reviews). The model retrains only when the count of new activities hits a specific threshold (e.g., every x new interaction signals from a user). This ensures the model adapts to new trends without crippling the database with constant training jobs.

// HigherCrudServiceActivity.cs

public async Task<DtoPlainActivity> Insert(InsertDtoActivity insert, User user)
{
    // ...

    if (insert.ActivityTypeId is
        (short) EnumActivityType.STARING_AT_REVIEW_FOR_10_SECONDS or
        (short) EnumActivityType.STARING_AT_REVIEW_FOR_30_SECONDS or
        (short) EnumActivityType.STARING_AT_REVIEW_FOR_60_SECONDS
       )
    {
        await TrainReviewModelIfThresholdAchieved(user);
    }
}

private async Task TrainReviewModelIfThresholdAchieved(User user)
{
    var reviewActivitiesOfUserCount = await GetQueryable()
        .Where(a => a.UserId == user.Id &&
                    (a.ActivityTypeId == (short) EnumActivityType.STARING_AT_REVIEW_FOR_10_SECONDS ||
                    a.ActivityTypeId == (short) EnumActivityType.STARING_AT_REVIEW_FOR_30_SECONDS ||
                    a.ActivityTypeId == (short) EnumActivityType.STARING_AT_REVIEW_FOR_60_SECONDS)
              )
        .LongCountAsync();

    if (reviewActivitiesOfUserCount > 1 && reviewActivitiesOfUserCount % THRESHOLD_FOR_REVIEW_MODEL_TRAINING != 0)
    {
        return;
    }

    var reviewActivities = await GetQueryable()
        .Where(a => a.ActivityTypeId == (short) EnumActivityType.STARING_AT_REVIEW_FOR_10_SECONDS ||
                    a.ActivityTypeId == (short) EnumActivityType.STARING_AT_REVIEW_FOR_30_SECONDS ||
                    a.ActivityTypeId == (short) EnumActivityType.STARING_AT_REVIEW_FOR_60_SECONDS)
        .ToListAsync();

    ReviewModel.OrchestrateReviewRecommenderModel(reviewActivities, _webHostEnvironment, reviewActivities.LongCount());
}

Enter fullscreen mode Exit fullscreen mode


The "Buffer" Optimization & Popularity Dynamics

One of the more interesting engineering challenges was pagination. Standard pagination (Skip(x).Take(y)) breaks down when you have a volatile sorting order influenced by ML predictions and volatile proximity.

To solve this, I implemented a Buffer Strategy. Instead of querying the exact page size from the database, we fetch a larger "buffer" (e.g., 100 records) based on a stable initial sort (creation date). We then apply the heavy lifting - ML predictions, proximity calculations, and category matching - in-memory on this buffer.

// HigherCrudServiceReview.cs

// ...

var paginationCreator = new PaginationCreator<BaseSearchDto>(search, uriBuilder, totalRecords);

var bufferPage = paginationCreator.CalcSkippingRecordsCount() / REVIEW_BUFFER_SIZE;

query = query.Skip(bufferPage * REVIEW_BUFFER_SIZE).Take(REVIEW_BUFFER_SIZE);

var reviews = await query.ToListAsync();

// ...

Enter fullscreen mode Exit fullscreen mode

Crucially, this buffer approach mitigates a common pitfall of popularity-based ranking: the "rich get richer" feedback loop. If we ranked the entire database solely by total time spent, legacy reviews would dominate the feed indefinitely, burying new content.

By anchoring the buffer to CreatedAt, we enforce a sliding window of relevance. As new reviews are created, they enter the buffer, and older reviews are naturally pushed out. This ensures that "popularity" is only a deciding factor among recent content, guaranteeing freshness while still surfacing the best of what's new. It keeps the feed from stagnating and ensures that popularity serves as a quality signal for new content, rather than a permanent high ground for the old.


The Ranking Hierarchy

Finally, how do we merge all these signals? For a logged-in user, the sorting priority is strict:

  1. Prediction Score: A personalized prediction of how long the user will spend on the review.
  2. Proximity: How close is the user to the review's location?
  3. Category Matching: Does the review match the user's explicit interests?
  4. Global Popularity: Total time spent by all users on this review.
  5. Recency: Fallback to creation date.

Conclusion

Building the recommendation engine for Claprec was an exercise in balancing complexity with performance. By offloading geo-spatial resolution to helper methods, utilizing a buffer for pagination, and implementing an implicit-feedback Matrix Factorization model, I've created a system is created that feels instant yet deeply personalized.

It's a constant iteration - tuning the weights, adjusting the retraining threshold, and refining how we define "time spent". But for now, it provides a solid technical foundation for the user experience at claprec.dev.


What's Next?

While the recommendation engine serves as the "brain" of the platform, it relies on a robust data skeleton to function. In the next post, Claprec: Database Design - Modeling the Data (5/6), I'll peel back the layers on the storage architecture. I will share the full ER diagram for the project and detail the Entity Framework Core configurations that bring it to life. We'll look at how I modeled the relational database to empower the ML components - designing a schema that ensures data integrity and provides the flexibility needed to feed our algorithms effectively.


I'd love to hear your thoughts on this approach to recommendation systems or answer any questions you might have in the comments below.