運用で確認すること
同じポストでも、読む人の関心やフォロー関係によって扱いが変わります。表示回数だけでは、どの段階で選ばれなかったかは分かりません。
おすすめの仕組み / 詳しく読む
ポストの集め方、評価の条件、根拠のコードを確認できます。
フォロー中とフォロー外から集める
いいねや返信などをするか見積もる
ブロックやミュートなどを確認する
主な処理の役割です。実際には、途中でも条件の確認や絞り込みがあります。
選ばれる仕組み
フォロー中の相手や、まだフォローしていない相手からポストを集めます。読む人がどう反応しそうかを比べ、表示してよい内容かも確かめます。
ここでいう候補は「おすすめに載せるか検討するポスト」です。フォロー中の一覧や検索結果まで同じ扱いではありません。
For You はリクエストごとに組み立てます。Thunder はフォロー中の最近のポストを出し、Phoenix retrieval、SimClusters、popular_posts_source はフォロー外や人気の最近のポストを出します。
候補は共通のモデルで一緒に順位付けされ、その後に visibility-filtering が表示可否を別判定します。
いいえ。候補に入ることと、実際に表示されることは別です。先に反応しそうかを見積もり、後でブロックやミュート、ラベルなどの条件で表示可否を確認します。
はい。フォロー中の最近のポストに加えて、フォロー外のポストも複数の方法で探します。おすすめは、フォロー関係だけで決まる仕組みではありません。
2. **Out-of-Network** — [`phoenix/`](phoenix/) retrieval and [`simclusters/`](simclusters/) find posts from accounts the viewer does not follow. [`popular_posts_source`](home-mixer/sources/popular_posts_source.rs) adds a shared list of popular recent posts.
Both are ranked together by the same model. **Phoenix** reads the viewer's recent engagement history and predicts, for each post, how likely the viewer is to take each action on it. Those predictions are combined into one score using weights held in the code — see [Scoring and Ranking](#scoring-and-ranking).Two pipelines do the work. The **Post Pipeline** finds, ranks and filters posts. The **Blending Pipeline** wraps it and adds what the model does not rank: ads, Who to Follow recommendations, prompts.
Ranking sets the order. Whether a post can be shown at all is decided separately, by [`visibility-filtering/`](visibility-filtering/), from the viewer's own actions such as blocks and mutes and from labels that other systems here attach to posts and accounts.
---
## System Architecture
### Request Path
<pre>
┌──────────────────────────────────────────────────────────────────────────────────────────┐Stages can be switched on and off individually, with defaults in [`home-mixer/params/param.rs`](home-mixer/params/param.rs) — see [Experiments and Configuration](#experiments-and-configuration) for how those defaults relate to what runs in production.
### Labeling Path
<pre>
┌─────── 1. CONTENT UNDERSTANDING — happens continuously, not on the request path ───────┐
│ │
│ POSTS AND MEDIA ACCOUNTS │┌─────────────────── 4. VISIBILITY FILTERING <a href="visibility-filtering/">visibility-filtering/</a> ────────────────────┐
│ │
│ for each post and viewer, one of four answers: │
│ │
│ ALLOW show the post normally │
│ NOTICE show the post with a notice attached to it │いいね・返信・共有
Xは「この人は、このポストにいいねや返信をしそうか」を見積もります。その値に行動ごとの係数を掛け、ポストを比べる点数にします。
重みは実件数ではありません。報告1件でいいね何件分、という読み方はできません。
Phoenix は favorite、reply、repost、quote、share、click、dwell など多くの行動をポストごとに予測します。
RankingScorer は `Final Score = Σ(weight_i × P(action_i))` で合計します。
重みは `home-mixer/params/param.rs` にあり、負の重みは not interested、mute、block、report などに付いています。
重みは予測確率や連続値に掛け、実件数には掛けません。
| 予測する行動 | 公開設定の例 | 補足 |
|---|---|---|
| いいね | 0.66 | 予測値に掛ける係数 |
| 返信 | 5.0 | bidirectional補正の対象 |
| リポスト | 1.0 | 予測値に掛ける係数 |
| 共有をコピーリンクで送る | 20.0 | 件数換算ではない |
| 報告 | -234.0 | マイナス側の寄与 |
言えません。重みは実際の件数ではなく、その人がその行動をしそうかという予測値に掛けます。報告やミュートはマイナス側に入りますが、件数換算の表ではありません。
使いません。公開コードに書かれた設定のままでは、新しい計算は動かず、別の処理でおすすめを作ります。X側が使う設定に切り替え、必要な情報と計算用のAIがそろったときだけ使われます。
### Scoring and Ranking
Phoenix predicts a probability for each action:
```
Engagement favorite · reply · repost · quote · share · share via DM · share via copy link
Clicks post · profile · link · photo expand · video open · quoted post
Attention video quality view · dwell · dwell time · click dwell time · video continuation seconds · profile visit seconds
Author follow author
Negative not interested · mute author · block author · report · not dwelled
```
`RankingScorer` combines them:
```
Final Score = Σ (weight_i × P(action_i))
```Positive actions carry positive weights, negative actions negative ones. The weights are in [`home-mixer/params/param.rs`](home-mixer/params/param.rs); the arithmetic is in [`xai-value-model/scoring.rs`](xai-value-model/scoring.rs).There is a common misconception to be aware of about the weights: they scale the predicted probabilities (or predicted continuous values, e.g. dwell time) — they do *not* scale the raw engagement counts, so e.g. it'd be incorrect to see that a report has 468 times higher weight than a like and conclude that e.g. "1 report cancels out 468 likes". The weights are a multiple on your own predicted probability of Liking, Reporting, etc, which is substantially driven by your own behavior.
Three adjustments follow:
- **Author Diversity**: each post after an author's first is multiplied by a decaying factor, down to a floor.use crate::inputs::{CandidateScoringInputs, QueryScoringContext};
use crate::weights::{NEGATIVE_SCORES_OFFSET, ValueModelWeights};
use rustc_hash::FxHashMap;
use std::cmp::Ordering;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ValueScores {
pub weighted: Vec<f64>,
pub scores: Vec<f64>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PostFusionMultiplier {
pub author_diversity: f64,
pub oon: f64,
}
impl PostFusionMultiplier {
pub fn combined(&self) -> f64 {
self.author_diversity * self.oon
}
pub fn apply(&self, score: f64) -> f64 {読む時間と動画
Xは、本文を読む時間、開いた後の時間、動画を見る時間などを別々に見積もります。項目によって係数が違い、0ならその項目は点数に加わりません。
秒数や完全視聴の扱いは、確認できた条件だけで読みます。視聴イベントの成立が未確認なら、秒数を推測しません。
`xai-value-model` では `dwell_time`、`click_dwell_time`、`video_continuation`、`user_video_continuation`、`profile_visit_secs` を別項目で合計します。
`vm-ranker/params.rs` では `ContDwellTimeWeight=0.004`、`ContClickDwellTimeWeight=0.4`、`VideoContinuationWeight=0.0`、`UserVideoContinuationWeight=0.0`、`ProfileVisitSecsWeight=0.0` です。
`home-mixer/params/param.rs` では `ContDwellTimeWeight=0.006` です。
| 項目 | vm-ranker初期値 | 意味 |
|---|---|---|
| 読む時間 | 0.004 | 継続読了の寄与 |
| 開いた後の時間 | 0.4 | クリック後の滞在寄与 |
| 動画継続 | 0.0 | 初期値では加点なし |
| プロフィール滞在 | 0.0 | 初期値では加点なし |
なりません。動画の長さに関する項目はありますが、完全視聴の成立条件が確認できない場合は秒数を推測しません。さらに、関連重みが0の項目もあり、長い動画だから自動で有利とは言えません。
入らない項目があります。係数が0なら、その項目は点数に反映されません。公開コードに書かれたままでは、動画継続やプロフィール滞在の加点は使われない設定です。
param!(ClickWeight, f64, "rust_home_mixer_click_weight", 0.3);
param!(OpenLinkWeight, f64, "rust_home_mixer_open_link_weight", 0.2);
param!(
ProfileClickWeight,
f64,
"rust_home_mixer_profile_click_weight",
0.0
);
param!(VqvWeight, f64, "rust_home_mixer_vqv_weight", 0.0);
param!(ShareWeight, f64, "rust_home_mixer_share_weight", 2.0);
param!(
ShareViaDmWeight,
f64,
"rust_home_mixer_share_via_dm_weight",
5.0
);
param!(
ShareViaCopyLinkWeight,
f64,
"rust_home_mixer_share_via_copy_link_weight",
20.0
);
param!(DwellWeight, f64, "rust_home_mixer_dwell_weight", 0.05);param!(QuoteWeight, f64, "rust_home_mixer_quote_weight", 5.0);
param!(
QuotedClickWeight,
f64,
"rust_home_mixer_quoted_click_weight",
0.05
);
param!(
QuotedVqvWeight,
f64,
"rust_home_mixer_quoted_vqv_weight",
0.0
);
param!(
FollowAuthorWeight,
f64,
"rust_home_mixer_follow_author_weight",
4.0
);
param!(
PostUnexploredWeight,
f64,
"rust_home_mixer_post_unexplored_weight",
0.015
);
param!(
PostVideoWeight,
f64,
"rust_home_mixer_post_video_weight",
0.0
);
param!(
ContDwellTimeWeight,
f64,
"rust_home_mixer_cont_dwell_time_weight",
0.006
);param!(
ContClickDwellTimeWeight,
f64,
"rust_home_mixer_cont_click_dwell_time_weight",
0.4
);
param!(
NotInterestedWeight,
f64,
"rust_home_mixer_not_interested_weight",
-52.272
);
param!(
BlockAuthorWeight,
f64,
"rust_home_mixer_block_author_weight",
-31.2
);
param!(
MuteAuthorWeight,
f64,
"rust_home_mixer_mute_author_weight",
-58.8
);
param!(ReportWeight, f64, "rust_home_mixer_report_weight", -234.0);
param!(
NotDwelledWeight,
f64,
"rust_home_mixer_not_dwelled_weight",
-0.02
);
param!(
EnableFollowingRepliedUsersFacepile,
bool, 10_000
);
param!(
EnableQuotedVqvDurationCheck,
bool,
"rust_home_mixer_enable_quoted_vqv_duration_check",
false
);
param!(
EnableContextFeatures,
bool,
"rust_home_mixer_enable_context_features",
true
);
param!(
EnableIpFeature,
bool,
"rust_home_mixer_enable_geo_features",
true
);
param!(
EnableExplicitEngagementSignals,
bool,
"rust_home_mixer_enable_explicit_engagement_signals",
true
);
param!(
EnableImplicitEngagementSignals,
bool,
"rust_home_mixer_enable_implicit_engagement_signals",
true
);
param!(
EngagementSignalsMaxPerType,
usize,ミュート・報告
Xは、いいねや返信に加えて「興味がない」、ミュート、ブロック、報告をしそうかも見積もります。これらの見積もりは点数を下げる側に使います。
報告やブロックの重みは、実件数の換算表ではありません。多く報告されたから全員の表示が同じだけ減る、とは言えません。
`compute_weighted_score` は `not_interested`、`block_author`、`mute_author`、`report`、`not_dwelled` を負の重みで合計します。
`vm-ranker/params.rs` では `NotInterestedWeight=-47.52`、`BlockAuthorWeight=-31.2`、`MuteAuthorWeight=-58.8`、`ReportWeight=-234.0`、`NotDwelledWeight=-0.02` です。
重みは予測値に掛けるもので、実件数ではありません。
あります。返信は好意だけでなく、反論や不満でも増えます。報告やミュートの予測も同時に入るので、返信数だけで評価の上下は判断できません。
影響の中心は予測値です。実際の件数をそのまま足すのではなく、その読者が報告しそうかを見積もって重みを掛けます。表示の可否は別の処理でも確認されます。
### Scoring and Ranking
Phoenix predicts a probability for each action:
```
Engagement favorite · reply · repost · quote · share · share via DM · share via copy link
Clicks post · profile · link · photo expand · video open · quoted post
Attention video quality view · dwell · dwell time · click dwell time · video continuation seconds · profile visit seconds
Author follow author
Negative not interested · mute author · block author · report · not dwelled
```
`RankingScorer` combines them:
```
Final Score = Σ (weight_i × P(action_i))
```Positive actions carry positive weights, negative actions negative ones. The weights are in [`home-mixer/params/param.rs`](home-mixer/params/param.rs); the arithmetic is in [`xai-value-model/scoring.rs`](xai-value-model/scoring.rs). PageRlExplorationTopK,
usize,
"rust_home_mixer_page_rl_exploration_top_k",
64
);
// These weights reflect a combination of how much an action is
// valued in ranking and typical propensities of these actions
// across the X network (e.g. negative feedback is overall rare).
// Each weight multiplies the *predicted* probability of that
// action (P(favorite), P(repost), …) or a continuous value e.g.
// watch time -- the weights do not multiply raw engagement counts.
// One common misinterpretation is that you can read these weight
// ratios as count equivalences, e.g. the incorrect statement thatparam!(
ContClickDwellTimeWeight,
f64,
"rust_home_mixer_cont_click_dwell_time_weight",
0.4
);
param!(
NotInterestedWeight,
f64,
"rust_home_mixer_not_interested_weight",
-52.272
);
param!(
BlockAuthorWeight,
f64,
"rust_home_mixer_block_author_weight",
-31.2
);
param!(
MuteAuthorWeight,
f64,
"rust_home_mixer_mute_author_weight",
-58.8
);
param!(ReportWeight, f64, "rust_home_mixer_report_weight", -234.0);
param!(
NotDwelledWeight,
f64,
"rust_home_mixer_not_dwelled_weight",
-0.02
);
param!(
EnableFollowingRepliedUsersFacepile,
bool,投稿者の偏り
ある読者向けの候補に同じ投稿者のポストが複数ある場合、2本目以降の点数に倍率を掛けます。1日の投稿順を数える処理ではありません。
この倍率は表示回数の減少率ではありません。候補内の点数を調整するだけです。
`diversity_multiplier(decay_factor, floor, exponent)` は `(1-floor)×decay_factor^exponent + floor` で計算します。
`vm-ranker/params.rs` の初期値は `EnableAuthorDiversity=true`、`AuthorDiversityDecay=0.5`、`AuthorDiversityFloor=0.25` です。
`author_pool_counts` は候補を点数順に並べ、同じ作者の何番目かを数えます。
| 同じ投稿者の候補順 | 調整係数 |
|---|---|
| 1番目 | 1.0 |
| 2番目 | 0.625 |
| 3番目 | 0.4375 |
違います。候補の並びに対する倍率です。同じ読者に見せる候補の中で、同じ投稿者が続くと後ろの候補ほど点数を下げます。投稿本数そのものを制限する処理ではありません。
下がりません。最初の候補は1.0倍です。2件目以降に倍率が掛かるので、同じ投稿者の連続登場を抑える調整だと考えてください。
weighted_score / NEGATIVE_SCORES_OFFSET * total_sum - w.negative_sum()
} else {
weighted_score - NEGATIVE_SCORES_OFFSET
}
}
pub fn diversity_multiplier(decay_factor: f64, floor: f64, exponent: f64) -> f64 {
(1.0 - floor) * decay_factor.powf(exponent) + floor
}
pub fn author_pool_counts(author_ids: &[u64], ordering_scores: &[f64]) -> Vec<u32> {
let mut indexed: Vec<(usize, f64)> = ordering_scores
.iter()
.enumerate()
.map(|(i, &s)| (i, s))
.collect();
indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(Ordering::Equal)); let mut counts = vec![0u32; author_ids.len()];
let mut author_counts: FxHashMap<u64, u32> = FxHashMap::default();
for (idx, _) in indexed {
let author_id = author_ids[idx];
let k = author_counts.get(&author_id).copied().unwrap_or(0);
counts[idx] = k;
author_counts.insert(author_id, k + 1);
}
counts
}
fn oon_applies(weights: &ValueModelWeights, candidate: &CandidateScoringInputs) -> bool {
match candidate.in_network {
Some(false) => true,
Some(true) => {
weights.oon_rescore_in_network_replies_retweets 0.0
);
param!(
WeightPerturbationSigma,
f64,
"rust_home_mixer_weight_perturbation_sigma",
0.0
);
param!(
WeightPerturbationSalt,
String,
"rust_home_mixer_weight_perturbation_salt",
""
);
param!(
EnableAuthorDiversity,
bool,
"rust_home_mixer_enable_author_diversity",
true
);
param!(
AuthorDiversityDecay,
f64,
"rust_home_mixer_author_diversity_decay",
0.5
);
param!(
AuthorDiversityFloor,
f64,
"rust_home_mixer_author_diversity_floor",
0.25
);
param!(
EnableOonRescoreForInNetworkRepliesRetweets,
bool,表示が少ない投稿
Xには、投稿からの時間や表示数などの条件に合うポストを選び、点数を引き上げる処理があります。すべてのポストに同じ表示数を保証するものではありません。
条件に合う候補があることと、実際に表示が増えることは別です。
`AuthorColdStart` は、返信やリポストを除き、`ColdStartFollowerCap=50000`、`ColdStartMaxPostAgeSecs=7200`、`ColdStartImpressionThreshold=200`、`LowImpressionsMaxPositionRatio=0.97` などの条件で候補を選びます。
`EnableViewerColdStart=true`、`EnableColdStartThompsonSampling=true` が既定です。
必要なら `pick_by_score` または Thompson sampling で1件を選び、その点数を目標値まで引き上げます。
| 条件 | 標準値 |
|---|---|
| 投稿の種類 | 返信・リポスト除外 |
| フォロワー数 | 50000以下 |
| 投稿後の時間 | 7200秒以内 |
| ホーム表示 | 200未満 |
必ずではありません。候補を選ぶための条件であって、表示回数の保証ではありません。対象条件を通った候補から1件を選び、さらに点数の引き上げを行います。
違います。確認しているのはフォロワー数、経過時間、ホーム表示数などです。アカウント年齢で一律に決める仕組みではありません。
use crate::models::candidate::PostCandidate;
use crate::models::query::ScoredPostsQuery;
use crate::params::{
AuthorIsControl, AuthorIsTreatment, ColdStartBetaAlpha0, ColdStartBetaBeta0,
ColdStartFollowerCap, ColdStartImpressionScale, ColdStartImpressionThreshold,
ColdStartMaxPostAgeSecs, ColdStartSlotMax, ColdStartSlotMin, ColdStartTrackedIds,
ColdStartTsTopK, EnableColdStartThompsonSampling, EnableViewerColdStart,
LowImpressionsMaxPositionRatio, PhoenixMoeCodivertViewerIsControl,
PhoenixMoeCodivertViewerIsTreatment,
};
use rand::Rng;
use rand_distr::{Beta, Distribution}; follower_cap: query.params.get(ColdStartFollowerCap),
impression_threshold: query.params.get(ColdStartImpressionThreshold) as u64,
max_post_age: Duration::from_secs(query.params.get(ColdStartMaxPostAgeSecs)),
max_position_ratio: query.params.get(LowImpressionsMaxPositionRatio),
slot_min: query.params.get(ColdStartSlotMin) as usize,
slot_max: query.params.get(ColdStartSlotMax) as usize,
use_thompson_sampling: query.params.get(EnableColdStartThompsonSampling),
beta_alpha0: query.params.get(ColdStartBetaAlpha0), let max_cold_start_slot = (params.max_position_ratio * nonzero as f64) as usize;
let eligible: Vec<usize> = candidates
.iter()
.enumerate()
.filter(|(i, c)| {
cold_start_base_eligible(c, params.follower_cap)
&& cold_start_corpus_eligible(arm, corpus[*i])
&& duration_since_creation_opt(c.tweet_id)
.is_some_and(|age| age <= params.max_post_age)
&& positions[*i] < max_cold_start_slot
&& c.view_count_on_home
.is_some_and(|imp| imp < params.impression_threshold)
}) .map(|(i, _)| i)
.collect();
let best_idx = if params.use_thompson_sampling {
pick_thompson(
&eligible,
candidates,
scores,
params.beta_alpha0,
params.beta_beta0,
params.impression_scale,
params.ts_top_k,
&mut rand::rng(),
)
} else {
pick_by_score(&eligible, scores)
};
let Some(best_idx) = best_idx else {
return (scores.to_vec(), None);
};
let mut effective = scores.to_vec();
effective[best_idx] = effective[best_idx].max(target);返信の届き方
読む人が投稿者をフォローしていない場合、その人の返信やリポストを候補から外す処理があります。一部の返信を残す例外は、公開コードの設定では使いません。
対象はホームのおすすめ候補です。返信欄やプロフィールの見え方とは別です。
`OONRetweetReplyFilter` は `in_network == false` の候補について、リポストと一部の返信を除外します。
`EnablePhoenixOonReplies=false`、`PhoenixOonRepliesRootOnly=false` が既定です。
`PhoenixReplyAncestorsHydrator` は `EnablePhoenixOonReplies` が有効なときに、TES から親や会話ルートを読み、`ancestors` が空の返信は残しません。
使われません。`EnablePhoenixOonReplies` は初期値 false なので、公開コードのままではこの例外経路は動きません。
X側が設定を切り替えたときだけ、指定された返信を候補に残せます。
`ancestors` が空の返信は外します。返信先をたどる情報がないと、会話の関係を確認できないためです。
use crate::clients::tweet_entity_service_client::TESClient;
use crate::models::candidate::PostCandidate;
use crate::models::query::ScoredPostsQuery;
use crate::params::EnablePhoenixOonReplies;
use std::collections::HashSet;
use std::sync::Arc;
use tonic::async_trait;
use xai_candidate_pipeline::hydrator::Hydrator;
use xai_home_mixer_proto as pb;
pub fn is_phoenix_home_retrieval(candidate: &PostCandidate) -> bool {
matches!(
candidate.served_type,
Some(pb::ServedType::ForYouPhoenixRetrieval | pb::ServedType::ForYouPhoenixRetrievalCold)
)
}
fn needs_ancestors(candidate: &PostCandidate) -> bool { is_phoenix_home_retrieval(candidate) && candidate.ancestors.is_empty()
}
pub struct PhoenixReplyAncestorsHydrator {
pub tes_client: Arc<dyn TESClient + Send + Sync>,
}
#[async_trait]
impl Hydrator<ScoredPostsQuery, PostCandidate> for PhoenixReplyAncestorsHydrator {
fn enable(&self, query: &ScoredPostsQuery) -> bool {
!query.has_cached_posts && query.params.get(EnablePhoenixOonReplies)
}
async fn hydrate(
&self,
_query: &ScoredPostsQuery,
candidates: &[PostCandidate],
) -> Vec<Result<PostCandidate, String>> {
let ids: Vec<u64> = candidates
.iter() .filter(|c| needs_ancestors(c))
.map(|c| c.tweet_id)
.collect::<HashSet<u64>>()
.into_iter()
.collect();
if ids.is_empty() {
return candidates
.iter()
.map(|_| Ok(PostCandidate::default()))
.collect();
}
let core_datas = self.tes_client.get_tweet_core_datas(ids).await;
let root_ids: Vec<u64> = core_datas
.values()
.filter_map(|r| match r {
Ok(Some(core)) => {
let parent = core.in_reply_to_tweet_id?;use crate::candidate_hydrators::phoenix_reply_ancestors_hydrator::is_phoenix_home_retrieval;
use crate::models::candidate::PostCandidate;
use crate::models::query::ScoredPostsQuery;
use crate::params::{EnablePhoenixOonReplies, PhoenixOonRepliesRootOnly};
use xai_candidate_pipeline::filter::{Filter, FilterResult};
pub struct OONRetweetReplyFilter;
impl Filter<ScoredPostsQuery, PostCandidate> for OONRetweetReplyFilter {
fn filter(
&self,
query: &ScoredPostsQuery,
candidates: Vec<PostCandidate>,
) -> FilterResult<PostCandidate> {フォロー外の探し方
読む人が反応したポストを手がかりに、別のポストを探すSIDという方法があります。ポストに付けた数値コードを使いますが、公開コードの設定ではこの方法は使いません。
SIDは本文検索ではありません。キーワードの一致と同じ意味で扱わないでください。
`SidSource` は `EnableSidSource=false` が既定で、`is_topic_request`、`in_network_only`、`has_cached_posts` などでは無効になります。
seed は `post_signal_ids` から作り、`SidSourceMaxSeeds=50`、`SidSourceMaxResults=800`、`SidSourceMaxPerSeed=100`、`SidSourceMinPrefixDepth=3` で検索します。
`xai-recsys-sid-retrieval` は 6 層の SID を使い、`shared_prefix_depth` を記録します。
| 検索条件 | 標準値 |
|---|---|
| 最大seed数 | 50 |
| 最大結果数 | 800 |
| seedごとの上限 | 100 |
| 最小一致深さ | 3 |
読者が反応したポストのIDです。反応情報を新しい順にまとめ、重複を外して seed にします。そこからSIDの共通部分がある別のポストを探します。
使いません。`EnableSidSource` は初期値 false なので、公開コードのままではこの経路は動きません。X側が設定を切り替えたときだけ使われます。
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 X.AI Corp.
use std::collections::HashSet;
pub const SID_LEVELS: usize = 6;
const BITS_PER_LEVEL: usize = 8;
const MAX_CODE: i64 = 255;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IndexedPost {
pub key: u64,
pub post_id: i64,
pub author_id: i64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SeedKey {
pub key: u64,
pub valid_depth: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Candidate {
pub post_id: i64,
pub author_id: i64,
pub seed_post_id: i64,
pub shared_prefix_depth: u32,
}#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RetrieveParams {
pub max_results: usize,
pub max_per_seed: usize,
pub min_prefix_depth: usize,
pub max_prefix_depth: usize,
}
pub struct SidIndex {
by_key: Vec<IndexedPost>,
by_post: Vec<(i64, u64)>,
snapshot_timestamp_secs: i64,
skipped_posts: usize,
}
fn is_valid_code(code: i64) -> bool {
(0..=MAX_CODE).contains(&code)
}
pub fn pack_full_key(codes: &[i64]) -> Option<u64> {
if codes.len() != SID_LEVELS || !codes.iter().all(|&c| is_valid_code(c)) {
return None;
}
Some(
codes
.iter() let max_depth = params.max_prefix_depth.min(seed.valid_depth);
let mut picked = Vec::new();
if params.min_prefix_depth == 0 || max_depth < params.min_prefix_depth {
return picked;
}
let mut consumed: Option<(usize, usize)> = None;
for depth in (params.min_prefix_depth..=max_depth).rev() {
let remaining = params.max_per_seed.saturating_sub(picked.len());
if remaining == 0 {
break;
}
let (lo, hi) = prefix_range(seed.key, depth);
let (start, end) = self.key_range(lo, hi); EnableSidSource, SidSourceMaxPerSeed, SidSourceMaxResults, SidSourceMaxSeeds,
SidSourceMinPrefixDepth,
};
use crate::sources::simclusters_source::{has_post_signals, post_signal_ids};
const METRIC_NAME: &str = "SidSource.retrieved";
static CALLER: LazyLock<String> = LazyLock::new(|| {
format!(
"home-mixer-{}",
std::env::var("APP_ENV").unwrap_or_else(|_| "prod".to_string())
)
});
pub struct SidSource {
client: Arc<dyn SidRetrievalClient>,
core_data_hydrator: CoreDataCandidateHydrator,
}
impl SidSource {
pub fn new(
client: Arc<dyn SidRetrievalClient>,人気ポストの一覧
Xは、最近の人気ポストをまとめた一覧からも、おすすめ用のポストを探します。読む人がすでに見たものを外し、その後も点数や表示の条件で選びます。
500件は読み出し上限です。表示件数や投稿上限に置き換えないでください。
`PopularPostsSource` は `!query.has_cached_posts && !query.in_network_only && EnablePopularPostsSource` のときだけ動きます。
`PopularPostsMaxResults=500` まで読み、`seen_ids` を外します。人気一覧は `popular_posts_source` が参照する共有リストで、候補は `ForYouPopularPosts` として扱われます。
この方法で読み出す候補の上限です。表示件数でも、1日の投稿上限でもありません。読み出した後に、読者がすでに見たものを除外します。
はい。`EnablePopularPostsSource` は true です。ただし、`has_cached_posts` や `in_network_only` のときは使いません。
true
);
param!(
PhoenixRequestCacheSideEffectTtlSeconds,
u64,
"rust_home_mixer_phoenix_request_cache_side_effect_ttl_seconds",
10800
);
param!(
EnableUrtMigrationComponents,
bool,
"rust_home_mixer_enable_urt_migration_components",
true
);
param!(
EnableAuthorServedMetricsExperimentBucket,
bool,
"rust_home_mixer_enable_author_served_metrics_experiment_bucket",
false
);
param!(
AuthorServedMetricsAuthorIds,
Vec<u64>,
"rust_home_mixer_author_served_metrics_author_ids",
vec![]
);
param!(
EnablePopularPostsSource,
bool, "rust_home_mixer_enable_popular_posts_source",
true
);
param!(
PopularPostsMaxResults,
u32,
"rust_home_mixer_popular_posts_max_results",
500
);
param!(
EnablePublishSeenIdsToKafka,
bool,
"rust_home_mixer_enable_publish_seen_ids_kafka",
true
);
param!(
EnableWhoToFollowModule,
bool,
"rust_home_mixer_enable_who_to_follow_module",
true
);
param!(
WhoToFollowFatigueHours,
u32,
"rust_home_mixer_who_to_follow_fatigue_hours",
30
);
param!(EnablePrompts, bool, "rust_home_mixer_enable_prompts", true);
param!(
ExcludeServedTweetIdsDuration,
u32,use crate::models::candidate::{PostCandidate, RetrievalSource};
use crate::models::query::ScoredPostsQuery;
use crate::params::{EnablePopularPostsSource, PopularPostsMaxResults};
use crate::util::popular_authors::now_ms;
use crate::util::popular_posts::PopularPostsCache;
use std::collections::HashSet;
use std::sync::Arc;
use tonic::async_trait;
use xai_candidate_pipeline::source::Source;
use xai_home_mixer_proto as pb;
const METRIC: &str = "PopularPostsSource";
const SERVED_TYPE: pb::ServedType = pb::ServedType::ForYouPopularPosts;
pub struct PopularPostsSource {
pub popular_posts: Arc<PopularPostsCache>,
} let limit = query.params.get(PopularPostsMaxResults) as usize;
let (generated_at_ms, posts) = self.popular_posts.top_posts(limit);
record("requests", 1);
if posts.is_empty() {
record("empty", 1);
return Ok(Vec::new());
}
if let Some(receiver) = xai_stats_receiver::global_stats_receiver() {
receiver.gauge(
"PopularPostsSource.ListAgeSecs",
&[],
(now - generated_at_ms) as f64 / 1000.0,
);
}
let seen: HashSet<u64> = query.seen_ids.iter().copied().collect();表示できる条件
ポストの順番を決める計算とは別に、ブロックやミュート、内容の分類、国や地域の条件などから、表示してよいかを確認します。
ここでの判定はおすすめ候補の表示可否です。返信欄や検索結果の表示とは同じではありません。
`visibility-filtering/` は、読む人とポストごとに `ALLOW`、`NOTICE`、`INTERSTITIAL`、`DROP` を返します。
ブロック、ミュート、フォロー関係、保護・停止状態、サブスク状態、国、設定を参照します。`VFCandidateHydrator` が判定を取り、`VFFilter` が drop を除きます。
いいえ。ランキングで上位でも、表示判定で落ちることがあります。ブロックやミュート、アカウントやポストのラベル、国や年齢の条件が別にあります。
必ずではありません。フォロー関係は条件の一つですが、ラベルや国、年齢、保護設定なども見ます。フォロワーでも表示されない場合があります。
┌─────────────────── 4. VISIBILITY FILTERING <a href="visibility-filtering/">visibility-filtering/</a> ────────────────────┐
│ │
│ for each post and viewer, one of four answers: │
│ │
│ ALLOW show the post normally │
│ NOTICE show the post with a notice attached to it ││ INTERSTITIAL show it behind an interstitial the viewer can tap │
│ through, e.g. for adult or graphic media │
│ DROP do not show it │
│ │
│ the rules read the labels above, plus whether the viewer blocks, mutes │
│ or follows the author, whether that account is protected, suspended or ││ deactivated, subscriber-only status, and the viewer's settings and │
│ country. Some rules drop a post only when it is a recommendation from │
│ an account the viewer does not follow — spam caught at high recall, for │
│ instance. The same post is allowed to a follower. │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────┘
▼┌─────────────── 5. <a href="home-mixer/filters/">POST-SELECTION FILTERS</a> <a href="home-mixer/filters/vf_filter.rs">VFFilter</a>, <a href="home-mixer/filters/ancillary_vf_filter.rs">AncillaryVFFilter</a> ────────────────┐
│ │
│ drop ──► the post is removed after ranking, and so is any post whose │
│ ancestor in the thread, quoted post or reposted post was │
│ itself dropped │