Skip to content

Ranking

recsys/ranking/ scores candidates and fuses them into a diverse, explainable list. Pure functions throughout. The hard contract: every scorer returns [0,1], so the weighted sum is valid without rescaling (the fused score itself may go negative via the aversion penalty, only the per-scorer outputs are bounded).

scorers.py

Each scorer maps a candidate to [0,1]. They are combined by weight in fusion.

Function Range What it measures
cosine(a, b) [-1,1] standard cosine; 0 if either side missing/zero
score_semantic(signals, vec) [0,1] closeness to the whole-history taste centroid: (cosine(taste_vec, vec)+1)/2
score_tag(signals, content) [0,1] affinity-weighted overlap of tag_affinity with the candidate's tags: Σ aff[l]·w[l] / Σ aff[l]
score_recency(signals, vec) [0,1] closeness to the most-recent view (sequence awareness, "more like what you just read")
score_aversion(signals, content) [0,1] overlap with disliked themes (tag_aversion); fused with a negative weight so it pushes such items down
score_geo(content, ref, scale_m) [0,1] proximity to the request location: exp(-haversine/scale); 0 when either side lacks coordinates

score_geo is the only scorer that takes a per-request input (the visitor's live GPS), independent of the tag filter; the others read from the stored user model.

fusion.py

Weighted fusion, explainable

weighted_fuse(per_scorer: dict[str, float], weights: FusionWeights)
    -> tuple[float, dict[str, float]]
fused = Σ_s fusion_weight[s] · score_s

Returns the fused score and a {scorer: weight·score} breakdown that rides along in each ScoredCandidate, so every recommendation can answer "why this item?". Default weights:

scorer tag semantic recency aversion geo
weight 0.45 0.20 0.05 -0.30 0.20

(geo only contributes when a request location is supplied.) All weights are runtime-tunable (settings page / RECSYS_W_* env) and become the prior of the learned bandit, see Bandit / online learning.

MMR rerank, diversity

mmr_rerank(candidates, vectors, *, lambda_, limit) -> list[ScoredCandidate]
select  argmax_i [ λ · fused_i − (1−λ) · max_{j∈selected} cosine(vec_i, vec_j) ]

λ = mmr_lambda (default 0.7) trades relevance vs diversity. Avoids returning ten near-duplicate stories on the same theme.

The distractor (exploration / serendipity)

After MMR, one deliberately off-profile item is injected, the WP5-FR-09 "encourage discovery" requirement.

Param Default Meaning
distractor_enabled true inject at all
distractor_probability 1.0 1.0 = every request; 0.5 = every other
distractor_slots [3, 4] 1-based position, picked at random -> rank 3 or 4
distractor_strategy max_dissimilar max_dissimilar | unexplored_theme | random
  • max_dissimilar searches the opposite of the taste vector (most off-profile story, may be outside the candidate pool).
  • In a filter/geo request it becomes within_constraint: the lowest-relevance item of the same restricted set (serendipity inside the location, never leaks out).
  • It is ranked (1-based position) and flagged internally kind="distractor" (surfaced as role: "distractor" in the API response); never an already-seen item.
  • It is never silent: if no off-profile item exists (tiny/exhausted set), diagnostics report distractor.placed = false with a reason.

Learned ranking (bandit)

ranking/bandit.py is an optional linear contextual bandit (LinUCB) that learns the fusion weights from engagement. Its prior is the static weights, so ranking_mode="bandit" starts identical to weighted fusion and adapts. Full detail: Bandit / online learning.

Putting it together

flowchart TD
    cand["Candidates (semantic ∪ tag, or filter/geo)"] --> sc["score_*: semantic, tag, recency, aversion, geo"]
    sc --> rank{"ranking_mode"}
    rank -->|static| wf["weighted_fuse -> fused + breakdown"]
    rank -->|bandit| th["θ·x (+ UCB bonus)"]
    wf & th --> mmr["mmr_rerank(λ, limit)"]
    mmr --> dis["inject distractor (slot 3/4)"]
    dis --> rec["Recommendation.items"]

This is the body of Recommender.recommend_for_signals, see Orchestration.


Full auto-generated reference

Code reference -> Recsys package.