这一页回答什么
本页回答四个问题:PhoenixCandidatePipeline 在哪两个阶段过滤候选;每个阶段有哪些过滤器;每个过滤器按什么条件删帖;哪些过滤器在开源默认值下不运行。可见性过滤服务本身的规则见 可见性过滤。本页只说明 home-mixer 怎样使用它的结果。
核心结论
- 两道过滤:打分前 21 个预打分过滤器(
home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:429-453),选择后 5 个选后过滤器(:511-517)。旧版是 14 个加 3 个。 - 过滤器顺序执行:框架把上一个过滤器的
kept交给下一个(candidate-pipeline/candidate_pipeline.rs:379-397)。被删的候选汇入filtered_candidates,供日志与统计使用。 - 7 个预打分过滤器在开源默认值下不运行:
ZeroFavCodivertFilter、NewUserSoftNsfwFilter、NewUserMinEngagementFilter、InventoryHoldoutFilter、FavHoldoutFilter的开关默认关闭;VideoFilter、TopicIdsFilter只在请求带对应参数时运行。 Brazil2026ElectionFilter已删除。它在 2026-08-14 加入,2026-10-28 的上游提交删除了它。删除前它位于AuthorSocialgraphFilter与VideoFilter之间。- 流水线在选后阶段才使用可见性结果。
VFCandidateHydrator只对入选候选查询可见性。VFFilter删掉 Drop、Tombstone、NotEvaluated 三类结果(home-mixer/filters/vf_filter.rs:22-27)。
两道过滤的位置
flowchart LR
SRC[9 个候选源] --> HYD[候选水合 ×15]
HYD --> PRE["预打分过滤 ×21"]
PRE --> SCORE[打分 ×5]
SCORE --> SEL[TopKScoreSelector]
SEL --> PH["选后水合 ×10<br/>含 VFCandidateHydrator"]
PH --> POST["选后过滤 ×5"]
POST --> OUT[截断到 35 条]
过滤器实现 Filter<ScoredPostsQuery, PostCandidate> trait。enable() 决定本次请求是否运行,filter() 返回 FilterResult { kept, removed }(candidate-pipeline/filter.rs:29-64)。多数过滤器用 FilterResult::partition 按谓词拆分候选,拆分保留输入顺序(filter.rs:14-27)。
预打分过滤器全表
下表按装配顺序排列(phoenix_candidate_pipeline.rs:430-452)。“默认”列表示该过滤器在开源默认值下是否运行。
| # | 过滤器 | 删掉什么 | enable() 条件 |
默认 |
|---|---|---|---|---|
| 1 | DropDuplicatesFilter |
同一 tweet_id 的后续重复;被删条目的召回来源合并到保留条目 |
总是 | 运行 |
| 2 | ZeroFavCodivertFilter |
只由 Phoenix ZeroFav 召回带来的候选,除非作者在实验组 | ViewerZeroFavCodivertIsTreatment |
不运行 |
| 3 | CoreDataHydrationFilter |
author_id == 0,即核心数据水合失败 |
总是 | 运行 |
| 4 | NewUserSoftNsfwFilter |
新用户看到的站外软色情标签帖 | For You 非话题请求、开关开启、账号小于 7 天、打分序列短于 100 | 不运行 |
| 5 | AgeFilter |
发布超过 48 小时的帖子 | 总是 | 运行 |
| 6 | SelfTweetFilter |
用户自己的帖子 | 总是 | 运行 |
| 7 | OONRetweetReplyFilter |
站外转发、站外回复、没有祖先信息的回复 | 总是 | 运行 |
| 8 | OONNsfwSimclustersFilter |
SimClusters 召回的站外帖,且作者带成人内容标记 | EnableSimclustersSource |
运行 |
| 9 | RetweetDeduplicationFilter |
指向同一原帖的重复转发或原帖 | 总是 | 运行 |
| 10 | IneligibleSubscriptionFilter |
用户未订阅作者的订阅专属帖 | 总是 | 运行 |
| 11 | PreviouslySeenPostsFilter |
seen_ids 或客户端 Bloom 过滤器命中的帖子 |
总是 | 运行 |
| 12 | PreviouslySeenPostsBackupFilter |
impressed_post_ids 命中的帖子 |
总是;列表为空时全部放行 | 运行但无效果 |
| 13 | PreviouslyServedPostsFilter |
served_ids 命中的帖子 |
EnableServedFilterAllRequests,或下拉加载且非 ForegroundTruncate |
运行 |
| 14 | ViewerMutedKeywordFilter |
正文命中静音词的帖子 | 总是;无静音词时全部放行 | 运行 |
| 15 | AuthorSocialgraphFilter |
屏蔽、静音、被作者屏蔽等社交图冲突 | 总是 | 运行 |
| 16 | VideoFilter |
自身或引用帖带视频的帖子 | 请求带 exclude_videos |
随请求 |
| 17 | TopicIdsFilter |
话题外的帖子、排除话题内的帖子 | 话题请求或有排除话题 | 随请求 |
| 18 | NewUserMinEngagementFilter |
新用户看到的低互动站外帖 | 开关开启,且账号或复活时间在 30 分钟内 | 不运行 |
| 19 | InventoryHoldoutFilter |
按帖子类型配置的百分比,确定性抽掉 | 开关开启且任一百分比大于 0 | 不运行 |
| 20 | FavHoldoutFilter |
按点赞数分档的百分比,确定性抽掉 | EnableFavHoldout |
不运行 |
| 21 | FairShareRankingFilter |
候选超过 2800 条时,按召回来源等比例截断 | 开关开启、无缓存、有打分序列、Phoenix 排序 kill switch 未开 | 运行 |
README 的预打分过滤表缺少第 2、4、21 行,并把第 14 行写作 MutedKeywordFilter。上表以源码为准。
去重类(1、9)
DropDuplicatesFilter 按 tweet_id 去重(filters/drop_duplicates_filter.rs:14-31)。RetweetDeduplicationFilter 按 retweeted_tweet_id.unwrap_or(tweet_id) 去重:原帖与它的转发只保留第一次出现的那条(filters/retweet_deduplication_filter.rs:19-30)。两者都把被删条目的 retrieval_sources 合并进保留条目。因此下游仍能知道一条帖子被几个源同时召回。
数据完整性与时效(3、5)
CoreDataHydrationFilter 保留 author_id != 0 的候选(filters/core_data_hydration_filter.rs:13)。AgeFilter 从雪花 ID 解析发布时间,删除超过 MAX_POST_AGE 的帖子(filters/age_filter.rs:16-30)。MAX_POST_AGE = 48 × 60 × 60 秒,开源默认值(params/config.rs:39)。ID 无法解析时,过滤器也删除该帖子(age_filter.rs:19)。
作者与关系(6、7、15)
SelfTweetFilter 删掉 author_id == user_id 的帖子(filters/self_tweet_filter.rs:13-14)。
OONRetweetReplyFilter 是新增过滤器(filters/oon_retweet_reply_filter.rs:10-29)。它删除三类候选:
- 站外(
in_network == Some(false))的转发。 - 站外的回复。例外是
EnablePhoenixOonReplies打开且候选来自 Phoenix 首页召回;PhoenixOonRepliesRootOnly打开时还要求回复直接挂在根帖下。两个开关的开源默认值都是 false(params/param.rs:67-78)。 - 任何
ancestors为空的回复,无论站内站外。
AuthorSocialgraphFilter 在候选命中任一条件时删除它(filters/author_socialgraph_filter.rs:24-48):作者被用户静音或屏蔽;作者屏蔽了用户;引用帖作者屏蔽了用户;用户屏蔽了引用帖作者;用户屏蔽了被转发的作者。
内容偏好(10、14、16、17)
IneligibleSubscriptionFilter:候选有 subscription_author_id 时,只有用户订阅了该作者才保留(filters/ineligible_subscription_filter.rs:21-26)。
ViewerMutedKeywordFilter 替代了旧的 MutedKeywordFilter。它用 TweetTokenizer 把静音词和正文分词,再用 MatchTweetGroup 匹配。匹配在 tokio::task::block_in_place 中执行,避免阻塞异步运行时(filters/viewer_muted_keyword_filter.rs:20-47)。
VideoFilter 只在 exclude_videos 时运行,删除 min_video_duration_ms 或 quoted_max_video_duration_ms 有值的帖子(filters/video_filter.rs:8-25)。旧版只检查帖子自身的视频。新版在引用帖带视频时也删除该帖子。
TopicIdsFilter 分两步(filters/topic_ids_filter.rs:15-105):
- 话题请求:把请求话题经
TopicIdExpansion展开成子话题后匹配候选的话题标签。批量话题请求(话题数大于 6)改为“排除补集”:只删掉全部话题都落在未请求话题里的候选(:28-41)。 - 排除话题:展开排除话题,删掉命中的候选。没有话题标签的候选在这一步也被删除(
:93-99的_ => false分支)。
TopicIdExpansion 内置话题分类(topic_ids_filter.rs:108 起)。例如, SPORTS 展开为足球、篮球、NFL、MLB 等子话题。旧版的 NewUserTopicIdsFilter 已删除。
已看与已服务(11、12、13)
三个过滤器都用 related_post_ids_iter() 检查帖子本身、被转发的原帖、被回复的父帖(home-mixer/util/candidates_util.rs:13-17)。因此父帖已看过的回复也会被删除。
| 过滤器 | 数据来源 | 说明 |
|---|---|---|
PreviouslySeenPostsFilter |
seen_ids 与客户端 Bloom 过滤器 |
Bloom 由 bloom_filter_entries 重建,may_contain 判定(filters/previously_seen_posts_filter.rs:16-31) |
PreviouslySeenPostsBackupFilter |
impressed_post_ids |
列表为空时直接放行(filters/previously_seen_posts_backup_filter.rs:15-20)。开源装配没有注册填充它的 ImpressedPostsQueryHydrator(phoenix_candidate_pipeline.rs:350-352) |
PreviouslyServedPostsFilter |
served_ids |
EnableServedFilterAllRequests 默认 true(params/param.rs:1027-1032),所以每次请求都运行(filters/previously_served_posts_filter.rs:14-19) |
ThunderSource 已在请求里排除 seen_ids。其他源不排除,这三个过滤器负责兜底。
新用户保护(4、18)
NewUserSoftNsfwFilter(filters/new_user_soft_nsfw_filter.rs:13-57)删除 in_network == Some(false) 且 soft_nsfw == Some(true) 的候选。运行条件:For You 非话题请求;EnableNewUserSoftNsfwFilter 打开;账号年龄小于 NewUserSoftNsfwFilterMaxAccountAgeSecs 604800 秒(7 天);打分序列长度小于 100,或该上限设为 0。开关默认 false(params/param.rs:881-898)。soft_nsfw 标签由 NewUserSoftNsfwLabelHydrator 在候选水合阶段写入。
NewUserMinEngagementFilter(filters/new_user_min_engagement_filter.rs:31-117)只作用于站外候选。可选指标是点赞、互动总数、浏览量,也可以除以浏览量取比率。过滤器删除低于阈值的候选。所有候选都缺指标时,过滤器放行整批。运行条件是开关打开,且账号年龄或账号复活后时长小于 1800 秒。开源默认值:关闭、指标 fav、阈值 0.0(params/param.rs:851-880、:899-904)。
实验抽样(2、19、20)
InventoryHoldoutFilter 用 (post_id, viewer_id) 的确定性哈希得到 0-99 的桶号,桶号小于配置百分比就删除(filters/inventory_holdout_filter.rs:31-48)。原创、回复、转发三类各有百分比,开源默认值都是 0(params/param.rs:1034-1057)。同一用户对同一帖子的抽样结果每次相同。
FavHoldoutFilter 复用同一个哈希,百分比由点赞数分档决定(filters/fav_holdout_filter.rs:10-47):点赞数 ≤ 1 不抽;大于 1 抽 2%;大于 3000 抽 15%;中间按表逐档上升。开关默认 false(params/param.rs:1059-1064)。
ZeroFavCodivertFilter 实现 ZeroFav 召回的作者分桶实验(filters/zero_fav_codivert_filter.rs:59-110)。它只在用户处于实验组时运行。它只删“仅由 ZeroFav 召回带来”的候选;作者处于 Treatment 桶时,候选保留。
来源配额(21)
FairShareRankingFilter(filters/fair_share_ranking_filter.rs:10-110)把候选按 served_type 分组。总数不超过预算时全部保留;超过时按各组数量等比例分配名额,余数按小数部分大小补齐,每组保留输入顺序中的前 N 条。预算是 PhoenixScoresResultSize = 2800,开关 EnableFairShareRanking 默认 true(params/param.rs:1357-1368)。它是最后一个预打分过滤器,用于控制送进 Phoenix 打分的候选数量。
选后过滤器
选后过滤器在 TopKScoreSelector 与 10 个选后水合器之后运行(phoenix_candidate_pipeline.rs:511-517)。单元测试保证 VideoCarouselFilter 是最后一个(:1184-1193)。
| 过滤器 | 删掉什么 | 源码 |
|---|---|---|
VFFilter |
可见性结果为 Drop、Tombstone、NotEvaluated 的帖子 | filters/vf_filter.rs:8-27 |
AncillaryVFFilter |
祖先、引用帖或被转发原帖被判 drop 的帖子 | filters/ancillary_vf_filter.rs:7-18 |
AncestorVideoFilter |
exclude_videos 时,祖先带视频的回复 |
filters/ancestor_video_filter.rs:8-22 |
DedupConversationFilter |
同一会话中分数较低的分支 | filters/dedup_conversation_filter.rs:8-49 |
VideoCarouselFilter |
选择阶段为视频轮播额外选入的候选;它们被移入轮播模块 | filters/video_carousel_filter.rs:14-53 |
VFFilter 与 AncillaryVFFilter
可见性结果由 VFCandidateHydrator 写入(candidate_hydrators/vf_candidate_hydrator.rs:53-130):
- 站内候选与被转发原帖用
TimelineHome安全等级查询;站外候选、祖先帖、引用帖用TimelineHomeRecommendations查询(:70-106)。同一帖子对关注者与非关注者可能得到不同结果。 EnableXaiVfClient默认 true,水合器调用 Rust 版可见性服务。旧的 Strato 版保留作回退(:60-65、params/param.rs:1350-1355)。drop_ancillary_posts在任一未墓碑化的祖先、引用帖或被转发原帖被判 drop 时置真(:140-170)。
VFFilter 的判定(vf_filter.rs:22-27):
match action {
Action::Allow | Action::Interstitial | Action::Avoid | Action::Downrank => false,
Action::Drop(_) | Action::Tombstone | Action::NotEvaluated => true,
}旧版按 visibility_reason 判定,删除任何非 SafetyResult 原因的帖子。新版按 visibility_action 判定,保留 Interstitial、Avoid、Downrank。README 说明:interstitial 与 notice 的帖子留在信息流里,本仓库不负责绘制遮罩。
源码推断:可见性查询失败时,水合器对该候选返回 Err(vf_candidate_hydrator.rs:119-121),框架不更新字段,visibility_action 保持 None,VFFilter 不删除它(vf_filter.rs:16)。线上是否另有兜底,源码未包含。
可见性服务的规则、四种动作与标签来源见 可见性过滤 与 标签规则。
DedupConversationFilter
会话 ID 取 ancestors 的最小值;没有祖先时取原帖 ID(dedup_conversation_filter.rs:42-49)。旧版的兜底是 tweet_id,新版是 get_original_tweet_id()。因此同一原帖的转发也算同一会话。同一会话出现更高分候选时,过滤器用它替换已保留的那条(:19-36)。
VideoCarouselFilter
只在 For You、EnableVideoCarousel 打开、未处于轮播疲劳期、非下拉加载时运行(home-mixer/util/video_carousel.rs:16-21);开关默认 false(params/param.rs:1217-1222)。它删掉带 video_carousel_extra 标记的候选,再从这些候选与排在第 35 名之后的候选里挑视频,写入 query.video_carousel(video_carousel_filter.rs:24-51)。它默认去掉 HighRisk 品牌安全等级的视频(params/param.rs:1277-1282)。
历史说明:Brazil2026ElectionFilter
| 时间 | 事件 |
|---|---|
| 2026-08-14 | 上游提交 c65aa17 加入 Brazil2026ElectionFilter,位于 AuthorSocialgraphFilter 与 VideoFilter 之间 |
| 2026-10-13 | 按巴西选举法院文书,名单缩减为 10 月 25 日第二轮的候选人(过滤器文件注释,commit 17c96bf) |
| 2026-10-26 | README Notable Updates 宣布删除 |
| 2026-10-28 | 上游提交 5ed7963 删除过滤器文件与装配 |
依据是巴西选举决议 23.610 第 28 条 § 1º-A:使用推荐系统的平台须从结果中排除向选举法院报告的频道与账号的内容(付费推广除外)。过滤器内置一份账号 ID 名单,删除作者、被转发作者、被引用作者或任一祖先作者在名单里、且用户没有关注该账号的候选(commit 17c96bf 的 home-mixer/filters/brazil_2026_election_filter.rs:80-98)。用户关注的账号不受影响。在 2e2295b 中,该文件与装配都已不存在(phoenix_candidate_pipeline.rs:429-453)。
设计决策
| 决策 | 理由 |
|---|---|
| 预打分与选后两道过滤 | 打分前删掉不合格候选,省下 Phoenix 打分开销;可见性查询成本高,只对入选候选执行 |
| 过滤器顺序执行 | 前序过滤缩小集合;每个过滤器的删除数独立记录,便于归因 |
| 去重时合并召回来源 | 保留“一条帖子被几个源同时召回”的信息,供统计与下游使用 |
| 已看检查覆盖父帖 | 避免把用户看过的帖子以回复的形式再推一次 |
| 抽样用确定性哈希 | 同一用户对同一帖子结果稳定,实验可复现 |
| FairShare 放在最后 | 先删掉所有不合格候选,再按来源分配打分名额,避免大源挤掉小源 |
| 可见性按动作而非原因判定 | Interstitial、Avoid、Downrank 只改变展示方式或排序,不应在这里删除 |
FAQ
Q:enable() 返回 false 的过滤器会怎样?
A:框架跳过该过滤器,只在阶段统计里记录总数与启用数(candidate-pipeline/candidate_pipeline.rs:386-388)。
Q:为什么关注的人的转发能出现,没关注的人的转发不能?
A:OONRetweetReplyFilter 只删 in_network == Some(false) 的转发(oon_retweet_reply_filter.rs:26)。站内转发不受影响。
源码锚点
home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:429-453—— 21 个预打分过滤器装配home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:511-517—— 5 个选后过滤器装配home-mixer/candidate_hydrators/vf_candidate_hydrator.rs:53-170—— 可见性查询与附属帖判定home-mixer/filters/vf_filter.rs:22-27—— 可见性动作到删除的映射home-mixer/filters/fair_share_ranking_filter.rs:68-110—— 等比例配额home-mixer/filters/topic_ids_filter.rs:15-105—— 话题过滤home-mixer/util/candidates_util.rs:13-17—— 已看检查的关联 ID
相关页面
- 可见性过滤 ——
VFFilter背后的规则与四种动作 - 标签规则 —— 可见性规则读取的标签从哪来
- candidate-pipeline 框架 ——
Filtertrait 与顺序执行 - home-mixer 编排层 —— 过滤器在内层流水线中的位置
- 打分与排序 —— 过滤之后的打分
- 候选选择与成型 —— 选后过滤所在的选择阶段
- SimClusters ——
OONNsfwSimclustersFilter针对的召回源 - 限流与 shadowban —— 从创作者视角解读这些过滤器