Skip to content

候选选择与成型

选帖过程:TopK 选 50 条 → 选后水合与过滤 → 截断到 35 条 → 外层 BlenderSelector 组装成信息流

Updated

这一页回答什么

本页回答:候选打完分之后,系统怎样从几百到几千条带分候选里选出最终展示的几十条。本页说明流水线的选择阶段与之后的成型步骤。打分见 打分与排序。

核心结论

  1. 选择 = 排序 + 截断。Selector trait 的默认 select() 按分降序排序,再截断到 size()(candidate-pipeline/selector.rs:52-66)。
  2. 内层先选 50 条,最后留 35 条。TopKScoreSelector 取 TOP_K_CANDIDATES_TO_SELECT = 50 条(home-mixer/params/config.rs:17)。选后水合与过滤完成后,框架截断到 RESULT_SIZE = 35 条(config.rs:18)。两个值都是开源默认值。
  3. 视频轮播从落选候选里再补选。视频轮播开启时,TopKScoreSelector 从未入选的候选中按轮播分数补选最多 30 条视频(home-mixer/selectors/top_k_score_selector.rs:18-25)。这些视频只进轮播模块,不进主信息流。
  4. 外层 BlenderSelector 不按分排序,只做组装:帖子与广告混排后,依次插入 Prompt、Who to Follow、视频轮播、PushToHome、Frame、FeedSurvey(home-mixer/selectors/blender_selector.rs:40-106)。
  5. 框架不丢弃落选候选:落选候选进入 non_selected,与截断掉的候选一起交给 side effect(candidate-pipeline/candidate_pipeline.rs:141-170)。

Selector:排序 + 截断的骨架

Selector trait(candidate-pipeline/selector.rs:24-107)把选择定义成可复用的骨架:

// candidate-pipeline/selector.rs:52-66
fn select(&self, _query: &Q, candidates: Vec<C>) -> SelectResult<C> {
    let mut sorted = self.sort(candidates);
    if let Some(limit) = self.size() {
        let non_selected = sorted.split_off(limit.min(sorted.len()));
        SelectResult { selected: sorted, non_selected }
    } else {
        SelectResult { selected: sorted, non_selected: vec![] }
    }
}
  • sort()(:70-78):按 score() 降序排序。partial_cmp 失败(NaN)时,两条候选视为相等。
  • score()(:68):业务必须实现,返回用于排序的分数。
  • size()(:80-82):默认 None,不截断。
  • enable()(:29-31):默认 true。返回 false 时框架跳过选择,全部候选进入 selected(candidate_pipeline.rs:416-431)。
  • run()(:40-50)在调用 select() 前后记录组件统计与 selected_count、non_selected_count。

业务代码至少要实现 score()。TopKScoreSelector 与 BlenderSelector 都重写了 select()。

TopKScoreSelector:内层选择器

// home-mixer/selectors/top_k_score_selector.rs:10-38(节选)
fn select(&self, query: &ScoredPostsQuery, candidates: Vec<PostCandidate>) -> SelectResult<PostCandidate> {
    let mut selected = self.sort(candidates);
    let mut non_selected =
        selected.split_off(params::TOP_K_CANDIDATES_TO_SELECT.min(selected.len()));
    if video_carousel::is_enabled(query) {
        selected.extend(video_carousel::take_videos_by_carousel_score(
            &mut non_selected,
            query.params.get(VideoCarouselExtraCandidates),
            &CarouselScoring::from_query(query),
            &query.seen_carousel_video_ids,
        ));
    }
    SelectResult { selected, non_selected }
}

fn score(&self, candidate: &PostCandidate) -> f64 {
    candidate.score.unwrap_or(f64::NEG_INFINITY)
}
  • 排序依据是 candidate.score。在 2e2295b 的装配里,打分链最后写入 score:VMRanker 先写入加权分与冷启动调整后的分数,再用 vm-ranker 服务返回的分数覆盖(home-mixer/scorers/vm_ranker.rs:93-136)。两个页级重排打分器默认关闭。细节见 打分与排序 与 VM Ranker。
  • 没有分数的候选取负无穷,排到最后。
  • 视频轮播补选(util/video_carousel.rs:189-206):从 non_selected 中挑出有视频、不是回复、有 video_open_score、轮播里没看过的候选,按轮播分数取最多 VideoCarouselExtraCandidates 条(默认 30,params/param.rs:1343-1348),并标记 video_carousel_extra = true。轮播开关 EnableVideoCarousel 默认 false(params/param.rs:1217-1222)。

选完之后:水合 → 过滤 → 截断 → finalize

选择之后,框架执行四步(candidate-pipeline/candidate_pipeline.rs:146-160):

// candidate-pipeline/candidate_pipeline.rs:146-160
let post_selection_hydrated_candidates = self
    .hydrate_post_selection(&hydrated_query, selected_candidates)
    .await;
let (mut final_candidates, post_selection_filtered_candidates) =
    self.filter_post_selection(&hydrated_query, post_selection_hydrated_candidates);
filtered_candidates.extend(post_selection_filtered_candidates);
let truncated_candidates = final_candidates.split_off(
    self.result_size_for(&hydrated_query).min(final_candidates.len()),
);
non_selected_candidates.extend(truncated_candidates);
self.finalize(&hydrated_query, &mut final_candidates);
步骤 内层内容 为什么放在选择之后
选后水合 10 个水合器:可见性、品牌安全、帖子类型指标、关注者回复头像、互关 Jaccard、话题反馈上下文、AI 趋势反馈上下文、视频媒体、视频嵌入、祖先视频(phoenix_candidate_pipeline.rs:485-509) 可见性与品牌安全查询成本高,只对会展示的候选执行
选后过滤 5 个过滤器:VFFilter、AncillaryVFFilter、AncestorVideoFilter、DedupConversationFilter、VideoCarouselFilter(:511-517) 这些过滤器依赖选后水合的数据。会话去重要在接近最终的集合上执行。详见 过滤流水线
截断 result_size_for(query),默认等于 result_size() = 35 选后过滤删掉一部分候选后,再截断到最终条数
finalize 默认空实现 留给业务代码做最后一次调整

result_size_for 是新增钩子(candidate_pipeline.rs:100-102),允许流水线按请求决定结果条数。2e2295b 中没有流水线重写它。

为什么有两个数量上限

flowchart LR
    A[打分后的全部候选] -->|"TopKScoreSelector 取 50"| B[入选候选]
    A -.->|"轮播开启时补选至多 30 条视频"| B
    B --> C[选后水合]
    C --> D["选后过滤<br/>删不可见、去会话重复、移出轮播视频"]
    D -->|"截断到 35"| E[内层输出]

如果一开始只选 35 条,选后过滤删掉不可见或重复的帖子后,条数会不够。因此选择器先多选 15 条作余量。

BlenderSelector:外层选择器与最终成型

外层 ForYouCandidatePipeline 的选择器是 BlenderSelector。它的候选是 FeedItem,score() 恒为 0.0(blender_selector.rs:108-110)。帖子顺序在内层已经确定。select() 的步骤(:40-106):

  1. partition_feed_items 按类型分桶:帖子、广告、Who to Follow、Prompt、PushToHome、Frame、FeedSurvey、视频轮播(:225-257)。
  2. 存在 Who to Follow 模块时丢弃视频轮播(:55、:157-162)。
  3. 按 AdsBlenderType 选广告混排器,混排帖子与广告。被丢弃的广告写入 query.ad_drops(:61-82)。开源默认值是 multi_risk(params/param.rs:957-962),见 广告混排。
  4. 依次插入其他内容:
内容 位置 源码
Prompt 列表最前,position 记为 PROMPTS_POSITION = 0 :113-123、params/config.rs:31
Who to Follow 第 6 位(下标 5) :125-137、config.rs:29
视频轮播 VideoCarouselPosition,默认第 6 位 :139-155、param.rs:1229-1234
PushToHome 钉在下标 0 :164-175
Frame 由 frames::plan 规划的槽位 :177-198
FeedSurvey 第 12 位 :200-212、config.rs:32
  1. 统计被混排丢掉的帖子数与未投放的广告,构造 non_selected(:95-100、selectors/mod.rs:14-40)。

外层框架截断到 FOR_YOU_MAX_RESULT_SIZE = 48(config.rs:21-22)之后,还执行一道选后过滤 AdAdjacentServedFilter。该过滤器开源默认关闭(param.rs:1021-1026)。

完整选择链

flowchart TB
    S[打分后的候选] --> T["TopKScoreSelector<br/>按 score 降序取 50"]
    T --> SEL[入选候选]
    T --> NS[落选 → non_selected]
    NS -.->|"轮播开启时补选视频"| SEL
    SEL --> PH["选后水合 ×10"]
    PH --> PF["选后过滤 ×5"]
    PF --> TR["截断到 35"]
    TR --> POSTS[内层输出:帖子 + 可选视频轮播]
    POSTS --> BL["外层 BlenderSelector<br/>帖子与广告混排,再插入模块"]
    BL --> FEED([For You 信息流,至多 48 项])

设计决策

决策 理由
Selector trait 提供默认 select() 排序与截断是通用逻辑,业务通常只填 score()
内层按最终分截断 帖子顺序只由打分链决定,行为可预期
先选 50、最后留 35 给选后过滤留余量
轮播视频从落选候选补选 轮播不挤占主信息流的 50 个名额
昂贵水合放在选择之后 只对会展示的候选查询可见性与品牌安全
外层只组装、不排序 相关性排序与广告、模块编排是两件事

FAQ

Q:落选的候选去哪了? A:落选候选进入 SelectResult.non_selected,截断掉的候选也并入 non_selected。它们随 SideEffectInput 交给 side effect。例如,Redis 候选缓存把选中与落选候选一起写入,供下次翻页复用(side_effects/redis_post_candidate_cache_side_effect.rs:49-60)。

Q:Who to Follow 与视频轮播会同时出现吗? A:不会。BlenderSelector 在 Who to Follow 模块存在时丢弃视频轮播(blender_selector.rs:157-162)。

源码锚点

  • candidate-pipeline/selector.rs:24-107 —— Selector trait 与默认 select()
  • candidate-pipeline/candidate_pipeline.rs:141-160 —— 选择、选后水合与过滤、截断
  • home-mixer/selectors/top_k_score_selector.rs:9-39 —— TopKScoreSelector
  • home-mixer/util/video_carousel.rs:189-206 —— 轮播补选
  • home-mixer/selectors/blender_selector.rs:39-212 —— BlenderSelector
  • home-mixer/params/config.rs:17-32 —— 选择数量与模块位置常量

相关页面

Type to search…

↑↓ navigate↵ selectEsc close