Scorers compute prediction scores and ranking signals for candidates. They run sequentially after filtering, allowing each scorer to see the results of previous scorers. Scorers update candidate fields with computed scores.
The Scorer trait defines the interface for scoring candidates:
candidate-pipeline/scorer.rs
#[async_trait]pub trait Scorer<Q, C>: Send + Syncwhere Q: Clone + Send + Sync + 'static, C: Clone + Send + Sync + 'static,{ /// Decide if this scorer should run for the given query fn enable(&self, _query: &Q) -> bool { true } /// Score candidates by performing async operations. /// Returns candidates with this scorer's fields populated. /// /// IMPORTANT: The returned vector must have the same candidates in the same order as the input. /// Dropping candidates in a scorer is not allowed - use a filter stage instead. async fn score(&self, query: &Q, candidates: &[C]) -> Result<Vec<C>, String>; /// Update a single candidate with the scored fields. /// Only the fields this scorer is responsible for should be copied. fn update(&self, candidate: &mut C, scored: C); /// Update all candidates with the scored fields from `scored`. /// Default implementation iterates and calls `update` for each pair. fn update_all(&self, candidates: &mut [C], scored: Vec<C>) { for (c, s) in candidates.iter_mut().zip(scored) { self.update(c, s); } } fn name(&self) -> &'static str { util::short_type_name(type_name_of_val(self)) }}
The score method must return candidates in the same order as the input. The returned vector must have the same length. Dropping or reordering candidates is not allowed - use a filter instead.
Combines multiple engagement scores into a single ranking score
Applies configurable weights to each engagement type
Conditionally applies VQV weight based on video duration
Includes negative signals (blocks, reports, not interested)
Normalizes scores for consistent ranking
The Weighted scorer is the final ranking signal used by the selector to sort candidates. It combines positive engagement signals (likes, retweets) with negative signals (blocks, reports) to produce a single score.
If a scorer fails or returns mismatched length, the error is logged and candidates remain unscored. This ensures partial failures don’t break the pipeline.
The scored vector must match the input order and length:
let scored_candidates = candidates .iter() .map(|c| { // Compute scores for each candidate in order PostCandidate { weighted_score: Some(compute_score(c)), ..Default::default() } }) .collect();
Partial Updates
Only update fields this scorer is responsible for:
If scoring fails, return candidates unchanged instead of erroring:
if let Ok(response) = prediction_result { // Score candidates with predictions return Ok(scored_candidates);}// Return candidates unchanged if prediction failedOk(candidates.to_vec())
Negative signals (blocks, reports) have negative weights, which can make the combined score negative. The offset ensures all final scores are positive for ranking.