Skip to main content

Engagement Service

Overview

The Engagement Service provides a sophisticated popularity scoring and sorting system for items based on real user engagement metrics. It uses logarithmic scaling to handle platforms with millions of interactions gracefully, ensuring that items continue to differentiate meaningfully at any scale. The service enriches item data with engagement metrics from the database and computes composite popularity scores.

Architecture

The Engagement Service operates as a pure computation layer on top of item data and engagement metrics. It fetches aggregated engagement data from the database via the engagement queries module, attaches metrics to items, and provides scoring/sorting functions that can be used by any listing or feed component.

Item Listing / Feed Component
|
engagement.service.ts (scoring + sorting)
|
engagement.queries.ts (aggregated metrics)
|
Database (views, votes, ratings, favorites, comments tables)

API Reference

Types

ItemWithEngagement

Extends ItemData with an optional engagement property containing aggregated metrics.

interface ItemWithEngagement extends ItemData {
engagement?: ItemEngagementMetrics;
}

Where ItemEngagementMetrics includes: views, votes, avgRating, favorites, comments.

Functions

calculatePopularityScore(item: ItemWithEngagement): number

Computes a composite popularity score for an item using weighted logarithmic scaling across multiple engagement signals, plus recency decay.

Parameters:

ParameterTypeDescription
itemItemWithEngagementAn item with optional engagement metrics

Returns: number -- A composite popularity score (higher = more popular).

Scoring Breakdown:

SignalScaleWeightExample: 1M interactions
FeaturedFixed+10,00010,000
Viewslog101,000x~6,000
Votes (net positive)log101,200x~7,200
Average RatingLinear (0-5)500x0-2,500
Favoriteslog101,100x~6,600
Commentslog101,000x~6,000
Recency (< 30 days)Linear decaymax 1,0000-1,000
Recency (30-90 days)Linear decaymax 5000-500
Recency (90-180 days)Linear decaymax 2500-250

sortByPopularity(a: ItemWithEngagement, b: ItemWithEngagement): number

Comparator function for sorting items by descending popularity score. Falls back to alphabetical name comparison for equal scores.

Parameters:

ParameterTypeDescription
aItemWithEngagementFirst item
bItemWithEngagementSecond item

Returns: number -- Negative if a is more popular, positive if b is more popular, 0 for equal.


enrichItemsWithEngagement(items: ItemData[]): Promise<ItemWithEngagement[]>

Fetches engagement metrics for a batch of items from the database and attaches them. Uses a single batch query for efficiency (no N+1 problem).

Parameters:

ParameterTypeDescription
itemsItemData[]Array of items to enrich

Returns: Promise<ItemWithEngagement[]> -- Items with engagement property populated.


logScale(value: number, weight: number): number (private)

Applies log10 scaling with a weight multiplier. Returns 0 for non-positive values. The formula is: log10(value + 1) * weight.

Scale examples (weight=1000):

  • 1 => 0, 10 => 1,000, 100 => 2,000, 1,000 => 3,000, 1M => 6,000

Implementation Details

  • Logarithmic scaling: The core design choice. Linear scoring breaks down at scale because the difference between 1M and 1.1M views is not meaningful to users, but the difference between 10 and 100 views is. Log10 compresses high values while preserving separation at lower ranges.
  • Recency decay: A three-tier time decay system gives progressively less weight to older items:
    • 0-30 days: up to 1,000 bonus points (linear decay)
    • 30-90 days: up to 500 bonus points
    • 90-180 days: up to 250 bonus points
    • 180+ days: no recency bonus
  • Featured boost: Featured items receive a flat 10,000-point boost, which places them significantly above non-featured items but does not completely override high organic engagement.
  • Batch enrichment: enrichItemsWithEngagement extracts all slugs upfront and performs a single batched database query, making it efficient for large item lists.
  • Date handling: The recency calculation handles both Date objects and date strings for the updatedAt field.

Database Interactions

OperationQuery FunctionTable(s)
Get metrics per itemgetEngagementMetricsPerItem(slugs)Aggregates from views, votes, ratings, favorites, comments

The getEngagementMetricsPerItem function returns a Map<string, ItemEngagementMetrics> keyed by item slug.

Error Handling

  • enrichItemsWithEngagement returns an empty array immediately if the input array is empty (short-circuit optimization).
  • Items without matching engagement data in the database will have engagement: undefined, and the scoring function handles this gracefully by treating all metrics as zero.
  • Database errors from the engagement queries module propagate up to the caller.

Usage Examples

import {
enrichItemsWithEngagement,
sortByPopularity,
calculatePopularityScore,
} from '@/lib/services/engagement.service';

// Enrich items and sort by popularity
const items = await getItems(); // ItemData[]
const enriched = await enrichItemsWithEngagement(items);
const sorted = enriched.sort(sortByPopularity);

// Get popularity score for a single item
const score = calculatePopularityScore(enriched[0]);
console.log(`Item "${enriched[0].name}" score: ${score}`);

// Use as a sort comparator directly
items.sort(sortByPopularity);

Configuration

This service has no environment variable dependencies. All scoring weights are defined as constants within the module and can be adjusted by modifying the source code.

Tunable constants:

  • Featured boost: 10000
  • View weight: 1000
  • Vote weight: 1200
  • Favorite weight: 1100
  • Comment weight: 1000
  • Rating multiplier: 500
  • Recency decay periods: 30/90/180 days