Feed Ranking Algorithm: Balancing Recency, Affinity, and Priority Pins

TL;DR: Building a content feed ranking system with configurable weights for recency, user affinity, proximity, and time-limited priority pinning for admin content

Designing a feed ranking algorithm requires balancing multiple signals: recency, social affinity, proximity, and administrative priorities. Here’s a practical implementation approach.

Ranking Components

┌────────────────────────────────────────────────────────────────────┐
│                      FEED RANKING SYSTEM                           │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│   Content Pool                                                     │
│        │                                                           │
│        ▼                                                           │
│   ┌─────────────────────────────────────────────────────────────┐ │
│   │              STATIC RULES (Priority Pins)                   │ │
│   │                                                             │ │
│   │   Notice → Pin to top for 24 hours                   │ │
│   │   Poll   → Pin until user interacts                  │ │
│   │   Alert  → Pin with complex rules                    │ │
│   │                                                             │ │
│   └─────────────────────────────────────────────────────────────┘ │
│        │                                                           │
│        ▼                                                           │
│   ┌─────────────────────────────────────────────────────────────┐ │
│   │              SCORE-BASED RANKING                            │ │
│   │                                                             │ │
│   │   Score = w₁(affinity) + w₂(follower) + w₃(proximity)      │ │
│   │         + w₄(recency) + w₅(trend)                          │ │
│   │                                                             │ │
│   └─────────────────────────────────────────────────────────────┘ │
│        │                                                           │
│        ▼                                                           │
│   Ranked Feed                                                      │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Scoring Weights Configuration

{
  "affinity_weight": 0,
  "follower_weight": 0,
  "proximity_weight": 0,
  "recency_weight": 1,
  "trend_weight": 0
}

Weight Definitions

WeightDescriptionUse Case
recency_weightNewer content scores higherChronological-ish feed
affinity_weightContent from users you interact withPersonalization
follower_weightContent from accounts you followFollowing list priority
proximity_weightContent from nearby usersLocation-based communities
trend_weightContent with high engagementViral/popular content

Static Priority Rules

1. Announcement Prioritization

Announcements have complex pinning rules:

Lifecycle:
                                                    
T+0         T+24h       T+34h       T+120h
 │           │           │           │
 ▼           │           │           │
Created      │           │           │
(Pinned)     ▼           │           │
             │           │           │
         Still pinned    ▼           │
         if no new    Return to      │
          notice  pin if not    │
                      interacted     ▼
                                  Max duration
                                  (drops to recency)

Rules:

  1. Pin to top for first 24 hours from creation
  2. If new notice published within 24h, queue behind it
  3. If user hasn’t viewed/interacted by T+34h, re-pin
  4. Maximum pin duration: 120 hours (5 days)
  5. User interaction (view, read, CTA click) → drops to recency-based position

2. Notice Prioritization

admin_notice:
  pin_duration: 24h
  position: 1             # Always first
  user_dismissable: false
  cascade_on_new: true    # New notice pushes old down

3. Poll Prioritization

admin_poll:
  pin_duration: until_interaction
  position: 1
  show_until_voted: true

Score Calculation

Recency Score

def recency_score(created_at, half_life_hours=24):
    """
    Exponential decay based on content age.
    Half-life: Score drops to 0.5 after N hours.
    """
    age_hours = (now - created_at).total_seconds() / 3600
    return math.exp(-0.693 * age_hours / half_life_hours)
Recency Decay (24h half-life):
                                                    
Score
1.0  │█
     │██
0.5  │───██────────────────────────────────
     │     ████
0.25 │         ████████
     │                 ████████████████
0.0  └─────────────────────────────────────
     0    24   48   72   96  Hours

Affinity Score (Future Enhancement)

def affinity_score(user_id, content_creator_id):
    """
    Based on historical interactions between users.
    """
    interactions = get_interaction_count(user_id, content_creator_id)
    # Likes, comments, profile views, etc.
    return normalize(interactions, max_expected=100)

Trend Score

def trend_score(content_id, window_hours=4):
    """
    Recent engagement velocity.
    """
    recent_reactions = get_reactions_in_window(content_id, window_hours)
    recent_comments = get_comments_in_window(content_id, window_hours)
    
    # Weighted engagement
    engagement = recent_reactions * 1 + recent_comments * 3
    
    return normalize(engagement, max_expected=50)

Combined Ranking Formula

def calculate_final_score(content, user, weights):
    # Check static rules first
    if is_pinned(content, user):
        return float('inf')  # Always on top
    
    # Calculate weighted score
    score = (
        weights['recency_weight'] * recency_score(content.created_at) +
        weights['affinity_weight'] * affinity_score(user.id, content.creator_id) +
        weights['follower_weight'] * follower_score(user.id, content.creator_id) +
        weights['proximity_weight'] * proximity_score(user.location, content.location) +
        weights['trend_weight'] * trend_score(content.id)
    )
    
    # Apply creator type boost (if configured)
    if content.creator_type == 'ADMIN':
        score *= 1.2  # 20% boost for admin content
    
    return score

User-Generated Content Prioritization

For user posts, add creator-type weights with time decay:

creator_weights:
  ADMIN:
    boost: 1.5
    decay_hours: 168      # 1 week
  MODERATOR:
    boost: 1.2
    decay_hours: 72       # 3 days
  VERIFIED_USER:
    boost: 1.1
    decay_hours: 48       # 2 days
  REGULAR_USER:
    boost: 1.0
    decay_hours: 24       # 1 day

A/B Testing Configuration

Easily test different weight combinations:

experiment_control:
  recency_weight: 1
  affinity_weight: 0
  trend_weight: 0

experiment_variant_a:
  recency_weight: 0.7
  affinity_weight: 0.2
  trend_weight: 0.1

experiment_variant_b:
  recency_weight: 0.5
  affinity_weight: 0.3
  trend_weight: 0.2

Implementation Notes

  1. Static rules take precedence - Pins are checked before score calculation
  2. Scores are relative - Normalize each component to 0-1 range
  3. Weights sum to 1 - Easier to reason about relative importance
  4. Cache aggressively - Affinity scores don’t change frequently
  5. Log decisions - Track why content was ranked where for debugging

Starting with pure recency (recency_weight: 1) and gradually introducing affinity and trend weights allows careful tuning based on engagement metrics.

Acknowledgements
  • Anil — Case study of ranking algorithms