Skip to main content
Scorers are responsible for assigning numerical scores to candidates in the Home Mixer pipeline. These scores determine the final ranking order of posts shown to users. Scorers implement the Scorer trait with both score and update methods.

Overview

Scorers generate new score values and return updated candidate objects. The update method merges these scores back into the original candidates:
#[async_trait]
trait Scorer<Q, C> {
    async fn score(&self, query: &Q, candidates: &[C]) -> Result<Vec<C>, String>;
    fn update(&self, candidate: &mut C, scored: C);
}

Available Scorers

PhoenixScorer

Uses the Phoenix ML prediction service to generate engagement probability scores for each candidate.
phoenix_client
Arc<dyn PhoenixPredictionClient>
required
Client for communicating with the Phoenix prediction service
Purpose: Predicts user engagement likelihood across multiple action types

Predicted Actions

Phoenix generates probability scores for the following actions:
favorite_score
Option<f64>
Probability the user will like/favorite the tweet
reply_score
Option<f64>
Probability the user will reply to the tweet
retweet_score
Option<f64>
Probability the user will retweet
photo_expand_score
Option<f64>
Probability the user will expand photos
click_score
Option<f64>
Probability the user will click the tweet
profile_click_score
Option<f64>
Probability the user will click the author’s profile
vqv_score
Option<f64>
Probability of video quality view (watching video to completion)
share_score
Option<f64>
Probability the user will share the tweet
share_via_dm_score
Option<f64>
Probability of sharing via direct message
Probability of sharing via copy link
dwell_score
Option<f64>
Probability the user will dwell on the tweet
quote_score
Option<f64>
Probability the user will quote tweet
quoted_click_score
Option<f64>
Probability of clicking a quoted tweet
follow_author_score
Option<f64>
Probability the user will follow the author
not_interested_score
Option<f64>
Probability of negative feedback (“not interested”)
block_author_score
Option<f64>
Probability the user will block the author
mute_author_score
Option<f64>
Probability the user will mute the author
report_score
Option<f64>
Probability the user will report the tweet

Continuous Actions

dwell_time
Option<f64>
Predicted dwell time in seconds (continuous value, not a probability)

Additional Metadata

prediction_request_id
Option<u64>
Unique identifier for the prediction request, useful for debugging and logging
last_scored_at_ms
Option<u64>
Timestamp (milliseconds since epoch) when the candidate was scored

Implementation Details

Code Example:
home-mixer/scorers/phoenix_scorer.rs
let tweet_infos: Vec<xai_recsys_proto::TweetInfo> = candidates
    .iter()
    .map(|c| {
        let tweet_id = c.retweeted_tweet_id.unwrap_or(c.tweet_id as u64);
        let author_id = c.retweeted_user_id.unwrap_or(c.author_id);
        xai_recsys_proto::TweetInfo {
            tweet_id,
            author_id,
            ..Default::default()
        }
    })
    .collect();

let result = self
    .phoenix_client
    .predict(user_id, sequence.clone(), tweet_infos)
    .await;
Retweet Handling: For retweets, Phoenix looks up predictions using the original tweet ID, ensuring consistent scoring:
home-mixer/scorers/phoenix_scorer.rs
let lookup_tweet_id = c.retweeted_tweet_id.unwrap_or(c.tweet_id as u64);

let phoenix_scores = predictions_map
    .get(&lookup_tweet_id)
    .map(|preds| self.extract_phoenix_scores(preds))
    .unwrap_or_default();

WeightedScorer

Combines Phoenix prediction scores into a single weighted score using configurable weights. Purpose: Translates multiple engagement signals into a single ranking score

Score Calculation

The weighted score is computed as:
home-mixer/scorers/weighted_scorer.rs
let combined_score = Self::apply(s.favorite_score, p::FAVORITE_WEIGHT)
    + Self::apply(s.reply_score, p::REPLY_WEIGHT)
    + Self::apply(s.retweet_score, p::RETWEET_WEIGHT)
    + Self::apply(s.photo_expand_score, p::PHOTO_EXPAND_WEIGHT)
    + Self::apply(s.click_score, p::CLICK_WEIGHT)
    + Self::apply(s.profile_click_score, p::PROFILE_CLICK_WEIGHT)
    + Self::apply(s.vqv_score, vqv_weight)
    + Self::apply(s.share_score, p::SHARE_WEIGHT)
    + Self::apply(s.share_via_dm_score, p::SHARE_VIA_DM_WEIGHT)
    + Self::apply(s.share_via_copy_link_score, p::SHARE_VIA_COPY_LINK_WEIGHT)
    + Self::apply(s.dwell_score, p::DWELL_WEIGHT)
    + Self::apply(s.quote_score, p::QUOTE_WEIGHT)
    + Self::apply(s.quoted_click_score, p::QUOTED_CLICK_WEIGHT)
    + Self::apply(s.dwell_time, p::CONT_DWELL_TIME_WEIGHT)
    + Self::apply(s.follow_author_score, p::FOLLOW_AUTHOR_WEIGHT)
    + Self::apply(s.not_interested_score, p::NOT_INTERESTED_WEIGHT)
    + Self::apply(s.block_author_score, p::BLOCK_AUTHOR_WEIGHT)
    + Self::apply(s.mute_author_score, p::MUTE_AUTHOR_WEIGHT)
    + Self::apply(s.report_score, p::REPORT_WEIGHT);
Where apply multiplies each score by its weight:
fn apply(score: Option<f64>, weight: f64) -> f64 {
    score.unwrap_or(0.0) * weight
}

Configuration Weights

Weights are configured via constants (in the excluded params module):
  • Positive Engagement: FAVORITE_WEIGHT, REPLY_WEIGHT, RETWEET_WEIGHT, SHARE_WEIGHT, etc.
  • Negative Signals: NOT_INTERESTED_WEIGHT, BLOCK_AUTHOR_WEIGHT, MUTE_AUTHOR_WEIGHT, REPORT_WEIGHT (typically negative)
  • Continuous: CONT_DWELL_TIME_WEIGHT for dwell time prediction

Video Quality View (VQV) Eligibility

VQV weight is only applied to videos meeting minimum duration requirements:
home-mixer/scorers/weighted_scorer.rs
fn vqv_weight_eligibility(candidate: &PostCandidate) -> f64 {
    if candidate
        .video_duration_ms
        .is_some_and(|ms| ms > p::MIN_VIDEO_DURATION_MS)
    {
        p::VQV_WEIGHT
    } else {
        0.0
    }
}

Score Normalization

The weighted score is normalized and offset-adjusted:
home-mixer/scorers/weighted_scorer.rs
fn offset_score(combined_score: f64) -> f64 {
    if p::WEIGHTS_SUM == 0.0 {
        combined_score.max(0.0)
    } else if combined_score < 0.0 {
        (combined_score + p::NEGATIVE_WEIGHTS_SUM) / p::WEIGHTS_SUM * p::NEGATIVE_SCORES_OFFSET
    } else {
        combined_score + p::NEGATIVE_SCORES_OFFSET
    }
}
This ensures proper handling of negative signals while maintaining score scale.

Output

weighted_score
Option<f64>
The combined, weighted score used for initial ranking

AuthorDiversityScorer

Applies a decay multiplier to diversify authors within a feed response, preventing dominance by any single author.
decay_factor
f64
default:"AUTHOR_DIVERSITY_DECAY"
Exponential decay factor applied to subsequent posts from the same author
floor
f64
default:"AUTHOR_DIVERSITY_FLOOR"
Minimum multiplier value, ensuring even repeated authors aren’t completely suppressed
Purpose: Increases author variety in the feed

Diversity Multiplier Formula

home-mixer/scorers/author_diversity_scorer.rs
fn multiplier(&self, position: usize) -> f64 {
    (1.0 - self.floor) * self.decay_factor.powf(position as f64) + self.floor
}
Where position is the 0-indexed count of previous posts from the same author. Example: If decay_factor = 0.8 and floor = 0.5:
  • 1st post from author: multiplier = 1.0 (no penalty)
  • 2nd post from author: multiplier = 0.9 (0.5 * 0.8^1 + 0.5)
  • 3rd post from author: multiplier = 0.82 (0.5 * 0.8^2 + 0.5)
  • Converges to floor = 0.5

Algorithm

  1. Sort candidates by weighted_score (descending)
  2. Track author occurrence counts
  3. For each author’s nth post, multiply score by multiplier(n-1)
  4. Return updated scores in original order
Code Example:
home-mixer/scorers/author_diversity_scorer.rs
let mut author_counts: HashMap<u64, usize> = HashMap::new();

let mut ordered: Vec<(usize, &PostCandidate)> = candidates.iter().enumerate().collect();
ordered.sort_by(|(_, a), (_, b)| {
    let a_score = a.weighted_score.unwrap_or(f64::NEG_INFINITY);
    let b_score = b.weighted_score.unwrap_or(f64::NEG_INFINITY);
    b_score.partial_cmp(&a_score).unwrap_or(Ordering::Equal)
});

for (original_idx, candidate) in ordered {
    let entry = author_counts.entry(candidate.author_id).or_insert(0);
    let position = *entry;
    *entry += 1;

    let multiplier = self.multiplier(position);
    let adjusted_score = candidate.weighted_score.map(|score| score * multiplier);

    scored[original_idx] = PostCandidate {
        score: adjusted_score,
        ..Default::default()
    };
}

Output

score
Option<f64>
The diversity-adjusted score (weighted_score × diversity_multiplier)

OONScorer

Out-of-Network Scorer that applies a weight factor to out-of-network candidates, typically reducing their scores relative to in-network content.
oon_weight_factor
f64
Multiplier applied to out-of-network candidates (configured via OON_WEIGHT_FACTOR)
Purpose: Balances in-network and out-of-network content in the feed Logic:
  • If candidate.in_network is Some(false), multiply score by OON_WEIGHT_FACTOR
  • Otherwise, leave score unchanged
Implementation:
home-mixer/scorers/oon_scorer.rs
let scored = candidates
    .iter()
    .map(|c| {
        let updated_score = c.score.map(|base_score| match c.in_network {
            Some(false) => base_score * p::OON_WEIGHT_FACTOR,
            _ => base_score,
        });

        PostCandidate {
            score: updated_score,
            ..Default::default()
        }
    })
    .collect();
This allows the system to prioritize familiar content (in-network) while still surfacing discovery content (out-of-network) when it’s highly relevant.

Output

score
Option<f64>
The network-adjusted final score

Scorer Pipeline Order

Scorers are typically applied in this sequence:
  1. PhoenixScorer - Generate ML prediction scores
  2. WeightedScorer - Combine predictions into weighted_score
  3. AuthorDiversityScorer - Apply diversity adjustments to create score
  4. OONScorer - Apply final network-based adjustments to score
Each scorer reads from previous scores and writes to new fields, creating a progression:
phoenix_scores → weighted_score → score (with diversity) → score (final)

Score Field Progression

FieldTypeSet ByPurpose
phoenix_scoresPhoenixScoresPhoenixScorerRaw ML predictions
weighted_scoreOption<f64>WeightedScorerWeighted combination
scoreOption<f64>AuthorDiversityScorer, OONScorerFinal ranking score
The final score field is used by selectors to determine the order candidates are served to users.
  • Sources - Retrieve candidates
  • Filters - Filter candidates before scoring
  • Selectors - Select and order scored candidates

Build docs developers (and LLMs) love