这一页回答什么
本页回答:候选打完分之后,系统怎样从几百到几千条带分候选里选出最终展示的几十条。本页说明流水线的选择阶段与之后的成型步骤。打分见 打分与排序。
核心结论
- 选择 = 排序 + 截断。
Selectortrait 的默认select()按分降序排序,再截断到size()(candidate-pipeline/selector.rs:52-66)。 - 内层先选 50 条,最后留 35 条。
TopKScoreSelector取TOP_K_CANDIDATES_TO_SELECT= 50 条(home-mixer/params/config.rs:17)。选后水合与过滤完成后,框架截断到RESULT_SIZE= 35 条(config.rs:18)。两个值都是开源默认值。 - 视频轮播从落选候选里再补选。视频轮播开启时,
TopKScoreSelector从未入选的候选中按轮播分数补选最多 30 条视频(home-mixer/selectors/top_k_score_selector.rs:18-25)。这些视频只进轮播模块,不进主信息流。 - 外层
BlenderSelector不按分排序,只做组装:帖子与广告混排后,依次插入 Prompt、Who to Follow、视频轮播、PushToHome、Frame、FeedSurvey(home-mixer/selectors/blender_selector.rs:40-106)。 - 框架不丢弃落选候选:落选候选进入
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):
partition_feed_items按类型分桶:帖子、广告、Who to Follow、Prompt、PushToHome、Frame、FeedSurvey、视频轮播(:225-257)。- 存在 Who to Follow 模块时丢弃视频轮播(
:55、:157-162)。 - 按
AdsBlenderType选广告混排器,混排帖子与广告。被丢弃的广告写入query.ad_drops(:61-82)。开源默认值是multi_risk(params/param.rs:957-962),见 广告混排。 - 依次插入其他内容:
| 内容 | 位置 | 源码 |
|---|---|---|
| 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 |
- 统计被混排丢掉的帖子数与未投放的广告,构造
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——Selectortrait 与默认select()candidate-pipeline/candidate_pipeline.rs:141-160—— 选择、选后水合与过滤、截断home-mixer/selectors/top_k_score_selector.rs:9-39——TopKScoreSelectorhome-mixer/util/video_carousel.rs:189-206—— 轮播补选home-mixer/selectors/blender_selector.rs:39-212——BlenderSelectorhome-mixer/params/config.rs:17-32—— 选择数量与模块位置常量
相关页面
- 打分与排序 —— 候选的最终分怎么来
- VM Ranker —— 写入最终
score的重排服务 - 过滤流水线 —— 选后过滤器细节
- 广告混排 ——
BlenderSelector的广告混排 - CandidatePipeline trait ——
execute()的阶段编排 - candidate-pipeline 框架 ——
Selectortrait 在框架中的位置 - home-mixer 编排层 —— 内外两层流水线
- 帖子是怎么被选中的 —— 本页的白话版