読むための用語集

用語の意味を調べる

本文の点線が付いた用語を押すと、意味を確認できます。

用語一覧

検索では、ほかのページの用語も探せます。

おすすめの仕組み / 詳しく読む

公開コードから読む
おすすめの仕組み

ポストの集め方、評価の条件、根拠のコードを確認できます。

参照コード 公式コード 9106e9f ↗

最初に読むなら

01

選ばれる仕組み

この説明へ ↓
02

いいね・返信・共有

この説明へ ↓
03

ミュート・報告

この説明へ ↓

公開コードで見る三つの役割

  1. ポストを探す

    フォロー中とフォロー外から集める

  2. 反応を予測する

    いいねや返信などをするか見積もる

  3. 表示を確かめる

    ブロックやミュートなどを確認する

主な処理の役割です。実際には、途中でも条件の確認や絞り込みがあります。

01

選ばれる仕組み

おすすめは、読む人ごとにポストを選びます

フォロー中の相手や、まだフォローしていない相手からポストを集めます。読む人がどう反応しそうかを比べ、表示してよい内容かも確かめます。

運用で確認すること

同じポストでも、読む人の関心やフォロー関係によって扱いが変わります。表示回数だけでは、どの段階で選ばれなかったかは分かりません。

ここでいう候補は「おすすめに載せるか検討するポスト」です。フォロー中の一覧や検索結果まで同じ扱いではありません。

根拠・詳しい条件

公開コードで確認できること

For You はリクエストごとに組み立てます。Thunder はフォロー中の最近のポストを出し、Phoenix retrieval、SimClusters、popular_posts_source はフォロー外や人気の最近のポストを出します。

候補は共通のモデルで一緒に順位付けされ、その後に visibility-filtering が表示可否を別判定します。

よくある疑問

候補に入れば、そのまま表示されますか?

いいえ。候補に入ることと、実際に表示されることは別です。先に反応しそうかを見積もり、後でブロックやミュート、ラベルなどの条件で表示可否を確認します。

フォロー外のポストも選ばれますか?

はい。フォロー中の最近のポストに加えて、フォロー外のポストも複数の方法で探します。おすすめは、フォロー関係だけで決まる仕組みではありません。

根拠のコード

README.md ↗
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).
README.md ↗
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>
┌──────────────────────────────────────────────────────────────────────────────────────────┐
README.md ↗
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                                           │
README.md ↗
┌───────────────────  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                          │
02

いいね・返信・共有

いいねの件数ではなく、読む人が反応しそうかを計算

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.0bidirectional補正の対象
リポスト1.0予測値に掛ける係数
共有をコピーリンクで送る20.0件数換算ではない
報告-234.0マイナス側の寄与

よくある疑問

「1報告で何件のいいね相当」とは言えますか?

言えません。重みは実際の件数ではなく、その人がその行動をしそうかという予測値に掛けます。報告やミュートはマイナス側に入りますが、件数換算の表ではありません。

初期設定がオフの機能は、公開コードのまま使いますか?

使いません。公開コードに書かれた設定のままでは、新しい計算は動かず、別の処理でおすすめを作ります。X側が使う設定に切り替え、必要な情報と計算用のAIがそろったときだけ使われます。

根拠のコード

README.md ↗
### 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))
```
README.md ↗
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).
README.md ↗
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.
xai-value-model/scoring.rs ↗
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 {
03

読む時間と動画

本文を読む時間と、動画を見る時間は別の項目

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なら、計算に入りますか?

入らない項目があります。係数が0なら、その項目は点数に反映されません。公開コードに書かれたままでは、動画継続やプロフィール滞在の加点は使われない設定です。

根拠のコード

home-mixer/params/param.rs ↗
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);
home-mixer/params/param.rs ↗
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
);
home-mixer/params/param.rs ↗
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,
home-mixer/params/param.rs ↗
    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,
04

ミュート・報告

ミュートや報告をされそうかも、点数に使う

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` です。

重みは予測値に掛けるもので、実件数ではありません。

よくある疑問

返信が多いのに、評価が下がることはありますか?

あります。返信は好意だけでなく、反論や不満でも増えます。報告やミュートの予測も同時に入るので、返信数だけで評価の上下は判断できません。

実際の報告件数を見なくても、表示に影響しますか?

影響の中心は予測値です。実際の件数をそのまま足すのではなく、その読者が報告しそうかを見積もって重みを掛けます。表示の可否は別の処理でも確認されます。

根拠のコード

README.md ↗
### 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))
```
README.md ↗
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).
home-mixer/params/param.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 that
home-mixer/params/param.rs ↗
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,
05

投稿者の偏り

同じ人のポストばかり並ばないように点数を調整

ある読者向けの候補に同じ投稿者のポストが複数ある場合、2本目以降の点数に倍率を掛けます。1日の投稿順を数える処理ではありません。

運用で確認すること

この倍率から、1日の投稿本数や投稿間隔の正解は決められません。自分の2本目の投稿が必ず不利になる、という意味でもありません。

この倍率は表示回数の減少率ではありません。候補内の点数を調整するだけです。

根拠・詳しい条件

公開コードで確認できること

`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件は下がりませんか?

下がりません。最初の候補は1.0倍です。2件目以降に倍率が掛かるので、同じ投稿者の連続登場を抑える調整だと考えてください。

根拠のコード

xai-value-model/scoring.rs ↗
        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));
xai-value-model/scoring.rs ↗
    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
vm-ranker/params.rs ↗
    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,
06

表示が少ない投稿

表示が少ないポストを、条件付きで優先する

Xには、投稿からの時間や表示数などの条件に合うポストを選び、点数を引き上げる処理があります。すべてのポストに同じ表示数を保証するものではありません。

運用で確認すること

投稿直後に表示が少ないだけで、この処理の対象かは分かりません。対象の条件と、実際に表示された結果は分けて確認します。

条件に合う候補があることと、実際に表示が増えることは別です。

根拠・詳しい条件

公開コードで確認できること

`AuthorColdStart` は、返信やリポストを除き、`ColdStartFollowerCap=50000`、`ColdStartMaxPostAgeSecs=7200`、`ColdStartImpressionThreshold=200`、`LowImpressionsMaxPositionRatio=0.97` などの条件で候補を選びます。

`EnableViewerColdStart=true`、`EnableColdStartThompsonSampling=true` が既定です。

必要なら `pick_by_score` または Thompson sampling で1件を選び、その点数を目標値まで引き上げます。

条件標準値
投稿の種類返信・リポスト除外
フォロワー数50000以下
投稿後の時間7200秒以内
ホーム表示200未満

よくある疑問

200回未満なら必ず上に来ますか?

必ずではありません。候補を選ぶための条件であって、表示回数の保証ではありません。対象条件を通った候補から1件を選び、さらに点数の引き上げを行います。

新しいアカウントだけの仕組みですか?

違います。確認しているのはフォロワー数、経過時間、ホーム表示数などです。アカウント年齢で一律に決める仕組みではありません。

根拠のコード

home-mixer/scorers/author_cold_start.rs ↗
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};
home-mixer/scorers/author_cold_start.rs ↗
            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),
home-mixer/scorers/author_cold_start.rs ↗
    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)
        })
home-mixer/scorers/author_cold_start.rs ↗
        .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);
07

返信の届き方

フォローしていない人の返信を、おすすめでどう扱うか

読む人が投稿者をフォローしていない場合、その人の返信やリポストを候補から外す処理があります。一部の返信を残す例外は、公開コードの設定では使いません。

運用で確認すること

ホームのおすすめに載せるかを選ぶ処理です。返信欄やプロフィールでも同じ返信が見えなくなる、という意味ではありません。

対象はホームのおすすめ候補です。返信欄やプロフィールの見え方とは別です。

根拠・詳しい条件

公開コードで確認できること

`OONRetweetReplyFilter` は `in_network == false` の候補について、リポストと一部の返信を除外します。

`EnablePhoenixOonReplies=false`、`PhoenixOonRepliesRootOnly=false` が既定です。

`PhoenixReplyAncestorsHydrator` は `EnablePhoenixOonReplies` が有効なときに、TES から親や会話ルートを読み、`ancestors` が空の返信は残しません。

よくある疑問

公開設定のままでは、例外は使われますか?

使われません。`EnablePhoenixOonReplies` は初期値 false なので、公開コードのままではこの例外経路は動きません。

X側が設定を切り替えたときだけ、指定された返信を候補に残せます。

返信先の情報がない返信はどうなりますか?

`ancestors` が空の返信は外します。返信先をたどる情報がないと、会話の関係を確認できないためです。

根拠のコード

home-mixer/candidate_hydrators/phoenix_reply_ancestors_hydrator.rs ↗
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 {
home-mixer/candidate_hydrators/phoenix_reply_ancestors_hydrator.rs ↗
    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()
home-mixer/candidate_hydrators/phoenix_reply_ancestors_hydrator.rs ↗
            .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?;
home-mixer/filters/oon_retweet_reply_filter.rs ↗
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> {
08

フォロー外の探し方

過去に反応したポストから、別のポストを探す方法

読む人が反応したポストを手がかりに、別のポストを探す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側が設定を切り替えたときだけ使われます。

根拠のコード

phoenix/crates/serving/xai-recsys-sid-retrieval/src/index.rs ↗
// 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,
}
phoenix/crates/serving/xai-recsys-sid-retrieval/src/index.rs ↗
#[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()
phoenix/crates/serving/xai-recsys-sid-retrieval/src/index.rs ↗
        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);
home-mixer/sources/sid_source.rs ↗
    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>,
10

表示できる条件

点数が高くても、見せてよいかを別に確認

ポストの順番を決める計算とは別に、ブロックやミュート、内容の分類、国や地域の条件などから、表示してよいかを確認します。

運用で確認すること

点数が高ければ全員に表示する、という仕組みではありません。相手との関係や表示の制限によって、見え方が変わります。

ここでの判定はおすすめ候補の表示可否です。返信欄や検索結果の表示とは同じではありません。

根拠・詳しい条件

公開コードで確認できること

`visibility-filtering/` は、読む人とポストごとに `ALLOW`、`NOTICE`、`INTERSTITIAL`、`DROP` を返します。

ブロック、ミュート、フォロー関係、保護・停止状態、サブスク状態、国、設定を参照します。`VFCandidateHydrator` が判定を取り、`VFFilter` が drop を除きます。

よくある疑問

点数が高ければ、必ず表示されますか?

いいえ。ランキングで上位でも、表示判定で落ちることがあります。ブロックやミュート、アカウントやポストのラベル、国や年齢の条件が別にあります。

フォロワーなら必ず表示されますか?

必ずではありません。フォロー関係は条件の一つですが、ラベルや国、年齢、保護設定なども見ます。フォロワーでも表示されない場合があります。

根拠のコード

README.md ↗
┌───────────────────  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                          │
README.md ↗
│       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                │
README.md ↗
│    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.                                     │
│                                                                                          │
└──────────────────────────────────────────────────────────────────────────────────────────┘
                                             ▼
README.md ↗
┌───────────────  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                                                             │