---
title: "Phoenix 召回索引：rankall 与事件层"
description: "phoenix-rankall-strato 根据发帖与点赞事件决定帖子进入哪个索引（先查可见性过滤）；phoenix-rankall 把索引事件写成按时间窗切分的 parquet 快照，供召回语料与 topic 过滤读取"
---

> Documentation Index
> Fetch the complete documentation index at: https://x-algorithm.vergego.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Phoenix 召回索引：rankall 与事件层

## 这一页回答什么

Phoenix 召回在“全站候选帖”里查找最近邻。本页回答三个问题：这份候选帖名单从哪里来，多久更新一次，哪些帖子有资格进入。答案在 2026-08 新开源的两个目录里：

- `phoenix-rankall-strato/`：用 Strato（X 内部的数据列/流处理 DSL）写的事件层。它监听发帖与点赞事件，查询帖子元数据和可见性过滤结论，决定帖子进入哪些索引，并把“索引事件”写入 Kafka。
- `phoenix-rankall/`：Rust 服务 `xai-recsys-rankall`。它消费这些 Kafka 事件，按“索引名 × 时间窗”维护内存表，并定期写成 parquet 快照文件。

本页不讲召回模型。召回模型见 [Phoenix 召回：双塔检索架构](https://x-algorithm.vergego.ai/concepts/phoenix-retrieval/)。

## 核心结论

1. **事件驱动索引更新，不做批量全量构建。** 索引有两个入口：发帖事件（`postCreationEventForwarder.strato`）和点赞事件（`favoriteEventForwarder.strato`）。两个入口都汇入同一个 Strato 列 `phoenixRankAllCandidateProcessor`。
2. **点赞事件按 2 的幂次限流。** 系统第一次处理一条帖子时立即触发。之后，只有点赞数越过下一个 2 的幂次阈值，才再次触发。阈值存在 memcache 中，TTL 为 50 小时（`phoenix-rankall-strato/columns/tweetUpdateThresholdsCache.strato:19`）。
3. **帖子进入索引前，系统先查可见性过滤。** `shouldDropPostByVF` 以 `safetyLevel = TimelineHomeRecommendations` 调用可见性过滤服务（`phoenix-rankall-strato/lib/eventProcessing.strato:260-279`）。结论为 drop 的帖子和标为 NSFW/成人的帖子不进入主索引。唯一例外是 NSFW 长视频，它进入 `nsfw_video` 专用索引。
4. **有两条路径绕过可见性过滤：** 系统在 VF 检查之前写入 `search_unfiltered` 索引；`EvergreenVideo` / `EvergreenNsfwVideo` 两种事件源完全不查 VF（`phoenixRankAllCandidateProcessor.strato:459-475`）。
5. **索引的单位是“索引名 + 时间窗”。** 例如 `1fav_1day`、`video_4to14day`。系统按帖子 ID（snowflake，内含发帖时间）裁剪时间窗，不按事件时间裁剪。
6. **产物是本地 parquet 快照加符号链接。** 系统每 180 秒（开源默认值）重写一次快照，保留 3 个版本（`phoenix-rankall/src/config/mod.rs:55-59`）。
7. **下游消费方位于 `phoenix/xrex/`：** 召回语料加载器按文件名读取 `1fav_1day.parquet` 等窗口（`phoenix/xrex/data/retrieval_dataset.py:290-325`）；排序/召回推理进程读取 `1fav_topic_*_1day.parquet`，做 topic 位图过滤（`phoenix/xrex/inference/model_runner.py:99-107`）。源码未包含 parquet 从 rankall 输出目录搬到 `PHOENIX_INDEX_BASE` 的过程。

## 整体数据流

```mermaid
flowchart TB
    PC["发帖事件<br/>content_understanding_realtime_unified_posts_v3"] --> PCP[postCreationEventProcessor]
    FV["时间线事件<br/>timeline_events_prototype"] --> FEP[favoriteEventProcessor]
    FEP -- "跨过 2 的幂次阈值" --> FK["Kafka: phoenix_rankall_fav_event"]
    FK --> CP[phoenixRankAllCandidateProcessor]
    PCP --> CP
    CP -- "查元数据、互动数、注解" --> META["Tweetypie、UecAggTotal、帖子注解"]
    CP -- "safetyLevel=TimelineHomeRecommendations" --> VF[可见性过滤 shouldDropTweet]
    CP --> K1["Kafka: phoenix_rank_all_indexing_event"]
    CP --> K2["Kafka: phoenix_rank_all_indexing_event_backup"]
    CP --> K3["Kafka: phoenix_rankall_metadata_event"]
    CP --> MH["Manhattan: filtered_topics_by_experiment"]
    K1 --> MAIN["rankall --pipeline main"]
    K1 --> SID["rankall --pipeline sid"]
    K2 --> TOPIC["rankall --pipeline topic"]
    K3 --> MD["rankall --pipeline metadata"]
    K3 --> TAIL["rankall --pipeline sid-tail"]
    MAIN --> PQ["parquet 快照<br/>window_epoch.parquet + 符号链接"]
    SID --> PQ
    TOPIC --> PQ
    MD --> PQ
    TAIL --> PQ
    PQ -. "搬运方式源码未包含" .-> XREX["phoenix/xrex<br/>召回语料 + topic 位图"]
```

## 事件层：phoenix-rankall-strato

### 两个入口

| 入口 | 源 Kafka topic | 转发目标 | 锚点 |
|------|---------------|---------|------|
| 发帖 | `content_understanding_realtime_unified_posts_v3` | `postCreationEventProcessor` → 构造 `{postId, eventSource = PostCreation}`，调用候选处理器 | `phoenix-rankall-strato/stream_forwarders/postCreationEventForwarder.strato:5`、`columns/phoenix_rank_all/postCreationEventProcessor.strato:7-15` |
| 点赞 | `timeline_events_prototype` | `favoriteEventProcessor` → 达到阈值才写入 `phoenix_rankall_fav_event` → 再转发给候选处理器 | `stream_forwarders/favoriteEventForwarder.strato:5`、`stream_forwarders/favoritePhoenixIndexingEventForwarder.strato:5,15` |

两个上游转发器都设置了 `delay = 2.minutes` 和 `slo = { maxLag = 120.minutes }`（`favoriteEventForwarder.strato:17-18`、`postCreationEventForwarder.strato:19-20`）。因此，帖子最早在事件发生约 2 分钟后才进入索引流程。

候选处理器为 `EvergreenVideo`、`EvergreenNsfwVideo` 两种事件源设了分支（`phoenixRankAllCandidateProcessor.strato:459-468`）。但开源代码里没有产生这两种事件的转发器。源码未包含它们的来源。

### 点赞限流：2 的幂次阈值

`favoriteEventProcessor` 只处理 `Favorite` 事件。它对取消点赞等其他事件只计数，然后忽略（`phoenix-rankall-strato/columns/favoriteEventProcessor.strato:96-157`）。它从事件中读取帖子当前的点赞数，再查询 `tweetUpdateThresholdsCache`：

- **缓存中没有这条帖子**：帖子年龄小于 49 小时且点赞数小于 2^30 时触发（`lib/eventProcessing.strato:62-70`）。
- **缓存中有这条帖子**：必须同时满足以下五个条件才触发（`lib/eventProcessing.strato:42-60`）。
  1. 点赞数 ≥ 缓存中的 `nextTriggerThreshold`。
  2. 距离上次触发 ≥ 1 分钟。
  3. 距离第一次触发 ≤ 48 小时。
  4. 帖子年龄 < 49 小时。
  5. 点赞数 < `1073741824`。

触发后，系统把下一个阈值设为 `nextPowerOfTwo(count)`，即 `2^(floor(log2(count)) + 1)`（`favoriteEventProcessor.strato:14-21`）。例如，当前有 5 个赞时，下一个阈值是 8。

| 常量（开源默认值） | 值 | 锚点 |
|------|----|------|
| `maxTimeSinceFirstUpdate` | 48 小时 | `lib/eventProcessing.strato:28` |
| `minTimeSinceLatestUpdate` | 1 分钟 | `lib/eventProcessing.strato:30` |
| `maxTweetAge` | 49 小时 | `lib/eventProcessing.strato:32` |
| `maxEngagements` | 1073741824 | `lib/eventProcessing.strato:34` |
| 阈值缓存 TTL | 50 小时 | `columns/tweetUpdateThresholdsCache.strato:19` |

效果：在 48 小时内，系统对一条帖子最多重新评估约 log2(点赞数) 次，而不是每个赞评估一次。每次重新评估都会重新查询 VF 和互动数。

### 候选处理器：决定帖子进入哪个索引

`phoenixRankAllCandidateProcessor` 收到 `{postId, eventSource}` 后，先补齐以下数据（`phoenixRankAllCandidateProcessor.strato:437-456`）：

- `getPostMetadataLightweight`：帖子本体（Tweetypie）。
- `getUnifiedPostAnnotations`：内容理解注解（topic 实体、NSFW 标记、标签）。
- `getEngagementCount`：从 `unified-counter/service/UecAggTotal` 读取 9 类计数（转发、回复、点赞、引用、收藏、曝光、不感兴趣、举报、拉黑），`dataVersion = Safe`（`lib/eventProcessing.strato:281-302`）。
- `getAuthorFollowers`：作者粉丝数。

缺少帖子元数据或作者 ID 的事件被直接丢弃（`:532-537`）。

然后，处理器按下图的顺序分流（`phoenixRankAllCandidateProcessor.strato:469-530`）：

```mermaid
flowchart TB
    S{eventSource} -- "EvergreenVideo / EvergreenNsfwVideo" --> EV["有沉浸式视频即进<br/>evergreen_video / evergreen_nsfw_video<br/>不查 VF"]
    S -- "Fav / PostCreation" --> SU{"Fav 且非社区帖、非转发?"}
    SU -- 是 --> SUI["点赞数 ≥ 1 即进 search_unfiltered<br/>此时尚未查 VF"]
    SU --> D["shouldDropPost = VF drop 或 isNsfw"]
    SUI --> D
    D --> C1{社区帖?}
    C1 -- 是 --> X1[跳过]
    C1 -- 否 --> C2{回复?}
    C2 -- "是，且 Fav、非自回复、未被 drop" --> R["1fav_reply<br/>32fav_reply（≥32 赞）<br/>metadata"]
    C2 -- 否 --> C3{转发?}
    C3 -- 是 --> X2[跳过]
    C3 -- 否 --> C4{shouldDropPost?}
    C4 -- "是，且 NSFW + 沉浸式视频 + Fav" --> NV["nsfw_video + metadata + mm_emb_metadata"]
    C4 -- "是，其余" --> X3[跳过]
    C4 -- 否 --> C5{eventSource}
    C5 -- Fav --> F["1fav、1fav 备份、1fav_topic 系列、32fav（≥32 赞）<br/>有沉浸式视频时加 video、1fav_video、imagine<br/>metadata + mm_emb_metadata"]
    C5 -- PostCreation --> P["post_creation<br/>有沉浸式视频时加 video<br/>metadata"]
```

各判定的定义如下：

- **可见性过滤**：`shouldDropPostByVF` 先读取开关 `enable_vf_rust_should_drop_tweet`。开关为真时，它调用 `visibility/xai/shouldDropTweet`；否则调用 `visibility/service/shouldDropTweetV2`。两条路径都使用 `safetyLevel = TimelineHomeRecommendations`，不带观看者（`forUserId = None`）。Rust 路径在调用失败时默认不 drop（`lib/eventProcessing.strato:260-279`）。Rust 版可见性过滤在 `visibility-filtering/rules/safety_level.rs:123-124` 定义这个安全级别。规则细节见 [可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/)。
- **NSFW**：只检查注解里的 `isNsfw`（`lib/eventProcessing.strato:332-340`）。同一文件另有一个同时检查 `isAdult` 的 `isAdultPost`（`:322-330`）。但候选处理器里名为 `isAdultPost` 的变量实际调用的是 `isNsfwPost`（`phoenixRankAllCandidateProcessor.strato:478`）。
- **沉浸式视频**：`hasImmersiveVideo` 只要求帖子含有 `TweetVideo` 或 `AmplifyVideo` 类媒体（`lib/eventProcessing.strato:393-401`）。`hasValidImmersiveVideo` 更严格：所有媒体都必须属于这两类，且时长 > 10 秒（`:23-24`、`:403-418`）。`video`、`1fav_video`、`nsfw_video` 三个索引的构建函数在内部再用后者检查。
- **imagine**：媒体元数据带有 `grokPostId`，即该视频由 Grok Imagine 生成（`lib/eventProcessing.strato:385-391`）。

`buildAdsIndex`（写入 `ads` 索引）有定义，但没有调用点（`phoenixRankAllCandidateProcessor.strato:16-24`）。rankall 侧的 `Ads` 管线也标记为未实现（`phoenix-rankall/src/config/mod.rs:157-162`）。

### Topic 索引与 filteredTopicsByExperiment

点赞路径上的 `build1FavTopicIndex` 为同一条帖子计算 6 套 topic 实体列表，对应 6 个实验选项（`phoenixRankAllCandidateProcessor.strato:213-261`、`lib/eventProcessing.strato:600-617`）：

| 选项 | 索引名 | topic 来源 |
|------|-------|-----------|
| 0 | `1fav_topic` | 帖子注解里的全部 topic 实体 |
| 1 | `1fav_topic_option_1` | 只保留作者在人工整理名单 V0 中所属类别的 topic |
| 2 | `1fav_topic_option_2` | 规则同上，名单为 V1 |
| 3 | `1fav_topic_option_3` | 作者最近 15 条原创/引用帖中占比 ≥ 0.90 的类别 |
| 4 | `1fav_topic_option_4` | 规则同上，占比 ≥ 0.75 |
| 5 | `1fav_topic_option_5` | 规则同上，占比 ≥ 0.50 |

阈值与样本数见 `lib/eventProcessing.strato:475-479`（开源默认值）。人工整理名单 `curatedAuthorLists` 通过 `import <hydra/curatedAuthorLists>` 引入，源码未包含名单内容。

另有两条硬编码的“紧急 topic”。第一条：带 xAI News 实体且带 `Iran` 标签的帖子，追加实体 `2000000000000000001`（`usIranWarTopicEntityId`）。第二条：带 Business & Finance 实体，且带利率、美联储、股票等 11 个标签之一的帖子，追加实体 `2000000000000000002`（`phoenixRankAllCandidateProcessor.strato:168-241`）。Rust 侧的 topic 名称表里没有这两个 ID（`phoenix-rankall/src/processor/topic_names.rs:4`）。

处理器把 6 套列表整体写入 Manhattan 数据集 `filtered_topics_by_experiment`。存储 TTL 为 2 天；读缓存命中时保留 1 小时，未命中时保留 5 分钟（`columns/mh/filteredTopicsByExperiment.strato:5-25`）。只有候选处理器有写权限（`:27-30`）。处理器再把非空列表作为索引事件写入备份 topic `phoenix_rank_all_indexing_event_backup`（`phoenixRankAllCandidateProcessor.strato:146-153`）。

### 输出的三个 Kafka topic

| topic | 载荷 | 写入者 | 锚点 |
|-------|------|-------|------|
| `phoenix_rank_all_indexing_event` | `PhoenixRankAllObject{postId, authorId, indexName}` | 绝大多数索引 | `columns/kafka/indexingEventKafkaTopic.strato:10` |
| `phoenix_rank_all_indexing_event_backup` | 同上；topic 索引额外带 `topicEntityIds` | `1fav` 备份、`1fav_topic*` | `columns/kafka/indexingEventKafkaBackupTopic.strato:10` |
| `phoenix_rankall_metadata_event` | `PhoenixRankAllCandidateFlat`：是否有图/视频、粉丝数、9 类互动数、视频时长、是否回复；或多模态嵌入 v3 / v5_1 / v6_dev | `buildMetadataDump`、`buildMMEmbMetadataDump` | `columns/kafka/metadataEventKafkaTopic.strato:12`、`phoenixRankAllCandidateProcessor.strato:356-417` |

## 索引服务：phoenix-rankall

### 一个二进制，六种管线

`xai-recsys-rankall` 用 `--pipeline` 参数选择管线。每种管线由 Kafka 消费者、一种 `RecordProcessor` 和一种快照存储组成（`phoenix-rankall/src/main.rs:76-254`）。

| 管线 | 默认 topic | 处理器 | 时间窗（开源默认值） |
|------|-----------|-------|------|
| `main` | `phoenix_rank_all_indexing_event` | `MainProcessor`：反序列化，丢弃 ID 为 0 或索引名为空的记录 | 19 个，见下表 |
| `sid` | 同上 | `SidProcessor`：只保留配置窗口里的索引名，批量查询语义 ID | 18 个 |
| `topic` | `phoenix_rank_all_indexing_event_backup` | `TopicProcessor`：另做黑名单过滤 | 7 个，均为 24 小时 |
| `metadata` | `phoenix_rankall_metadata_event` | `MetadataProcessor` | `metadata` 1/2/3 天 |
| `sid-tail` | 同上 | `SidTailProcessor`：只保留粉丝数 < 1000 的作者，再查询语义 ID | `tail` 1 天 |
| `analysis` | `home_mixer_phoenix_scored_candidates` | `AnalysisProcessor`：解析 home-mixer 写出的打分记录，读取前缀为 `phoenix.HomeExperiment1Lap7.` 的分数 | 不分窗，按日期目录写分析 dump |

topic 与窗口的定义在 `phoenix-rankall/src/config/mod.rs:147-228`。`sid-tail` 的粉丝阈值 `tail_max_author_followers = 1000` 和点赞下限 `tail_min_fav_count = 0` 见 `config/mod.rs:104-107`。`analysis` 的分数前缀见 `config/mod.rs:64-68`。该管线跳过 `suggest_type` 为 `rankedfollowing` 的记录（`phoenix-rankall/src/processor/analysis_processor.rs:26-30`）。

`main` 管线的 19 个窗口如下（`config/mod.rs:170-190`）：

| 索引名 | 时间窗 |
|-------|-------|
| `post_creation` | 1 天 |
| `1fav` | 1 天、2 天 |
| `1fav_video` | 2 天、2–4 天、4–14 天、4–30 天 |
| `32fav`、`1fav_reply`、`32fav_reply` | 各 1 天 |
| `video` | 2、4、7、14、30 天 |
| `nsfw_video` | 2 天、7 天 |
| `evergreen_video`、`evergreen_nsfw_video` | 各 1825 天（5 年） |

`sid` 管线另有 `video_6h`、`video_6hto2day`、`imagine_4day`、`nsfw_video_14day` 等窗口（`config/mod.rs:205-224`）。窗口文件名规则为 `{索引名}_{天数}day`；有下限时写成 `{索引名}_{下限}to{上限}day`（`config/mod.rs:270-293`）。

### 黑名单

`topic` 管线启动时从输出目录读取 `blacklisted_post_ids.txt` 与 `blacklisted_author_ids.txt`。文件每行一个 ID，以 `#` 开头的行是注释（`phoenix-rankall/src/processor/blacklist.rs:13-63`）。`TopicProcessor` 丢弃命中黑名单的帖子或作者（`src/processor/topic_processor.rs:56-65`）。只有 `topic` 管线加载黑名单（`src/main.rs:101`）。源码未包含名单内容与维护方式。

### 语义 ID 补全

`sid` 与 `sid-tail` 管线通过 gRPC `SidLookupService.LookupSids` 批量查询每条帖子的语义 ID 码（`phoenix-rankall/proto/sid_lookup.proto:5-19`）。语义 ID 是帖子多模态嵌入的残差量化码，默认 6 层（`config/mod.rs:101-102`）。语义 ID 在模型侧的含义见 [哈希嵌入](https://x-algorithm.vergego.ai/concepts/hash-based-embeddings/)。

- **新鲜度门槛**：管线不为年龄 < 180 秒的帖子发请求（`config/mod.rs:83-84`、`src/sid/client.rs:75-86`）。原因是语义 ID 要等多模态嵌入计算完成。
- **回填循环**：管线每 30 秒扫描一次存储里“1 小时内、语义 ID 仍缺失”的帖子。每批 5000 条，每轮最多 800000 条（`config/mod.rs:89-99`、`src/sid/backfill.rs:70-80`）。
- **缺失占位**：写 parquet 时，语义 ID 长度不等于层数的行写成 6 个 `-1`（`src/store/base.rs:721-775`）。

### 快照存储

`BaseSnapshotStore` 按窗口名维护 `post_id → 值` 的内存表（`phoenix-rankall/src/store/base.rs`）：

1. **写入**：新记录先进入 pending 区，窗口路由器决定它进入哪些窗口。`main` 与 `sid` 按索引名精确匹配；`metadata` 进入全部窗口；`topic` 使用写死的映射表（`src/store/base.rs:1195-1238`）。同一帖子重复到达时，存储合并记录。互动计数逐项取最大值，防止后到的 0 覆盖先到的计数（`src/processor/record.rs:65-80`）。
2. **裁剪**：存储每 `dump_interval_secs = 180` 秒（`config/mod.rs:55-56`）删除帖子 ID 小于 `now - retention` 的记录（`src/store/base.rs:354-360`）。帖子 ID 是 snowflake，高位是发帖时间（`src/store/snowflake.rs:1-9`）。因此，窗口按**发帖时间**计算，不按点赞时间计算。带下限的窗口还会去掉过新的帖子，并另写一份放宽 6 小时的 `prefloor_` 文件（`src/store/base.rs:110`、`:340-342`）。
3. **落盘**：存储先写 `{窗口}.parquet.tmp`，再改名为 `{窗口}_{epoch秒}.parquet`，然后原子替换符号链接 `{窗口}.parquet`，保留最近 3 个版本（`src/store/writer.rs:9-49`、`config/mod.rs:58-59`）。压缩格式为 Snappy。
4. **重启恢复**：服务启动时读回最新版本文件，重建内存表（`src/store/base.rs:130-242`）。Kafka 消费者默认回拨 12 小时（`config/mod.rs:43-44`）。
5. **提交位点**：默认只在一次落盘完成后才提交 Kafka 位点（`commit_after_dump = true`，`config/mod.rs:71-72`、`src/pipeline/mod.rs:98-101`）。服务崩溃时，最多重放一个周期的事件。

parquet 列：`main` 管线只有 `post_id`、`author_id`；`sid` 增加 `post_sid`；`topic` 增加 `topic_entity_ids` 与 `topic_names`；`metadata` 增加媒体标记、粉丝数和 9 类计数（`src/store/base.rs:684-960`）。

存储没有删除单条记录的路径。帖子只会因窗口过期而离开索引。如果一条已入索引的帖子之后被 VF 判为 drop，它不会再收到新的索引事件，但已写入的记录会保留到窗口过期。这一点是按代码推断：开源代码中未见主动撤回逻辑。

`src/hydrate/` 模块实现了带优先级队列的帖子特征补全（`Live` / `Refresh` / `Backfill`，`src/hydrate/scheduler.rs:23-28`），但 `main.rs` 没有实例化它。源码未包含它在线上是否启用。

## 谁在读这些 parquet

| 消费方 | 读取的文件 | 用途 | 锚点 |
|-------|-----------|------|------|
| 召回语料加载 `RetrievalDataset` | `post_sid_v8_256x6_snapshots/` 下的 `1fav_1day`、`video_2day`、`video_4day`、`video_4to14day`、`nsfw_video_2day`、`imagine_4day`；`post_sid_v5_256x6_tail_snapshots/tail_1day` | 每个枚举值对应召回语料里的一个子集（`dataset_type`）；`HOME` 再拆成 `HOME_COLD` / `HOME_HOT` 两片 | `phoenix/xrex/data/retrieval_dataset.py:62-68`、`:290-325` |
| 训练侧语料暂存进程 | 同上，按文件名里的 10 位 epoch 识别版本 | 子进程轮询新版本；训练器保存 checkpoint 时，把候选塔嵌入烘进 `post_embeddings` | `phoenix/xrex/train/retrieval_corpus_stager.py:20`、`phoenix/xrex/train/trainer_recsys.py:3935-3958` |
| 推理进程 topic 过滤 | `post_creation_snapshots_topic/1fav_topic*_1day.parquet` | 读取 `topic_entity_ids` 并转成位图，按 `topic_filter_mode` 选 6 个选项之一来过滤召回候选 | `phoenix/xrex/inference/model_runner.py:99-107`、`:4760-4800`、`:5154-5169` |

消费方读取的文件名与 rankall 的窗口名一致（`1fav_1day`、`video_4to14day` 等）。版本号格式 `_(\d{10})\.parquet$` 也与 `atomic_write_parquet` 的命名一致。由此可以确认 rankall 生产召回语料。仍有两点未确认：

- rankall 写入本地 `output_dir`，消费方读取 `PHOENIX_INDEX_BASE`（线上还有 O2 / GCS 两个下载源，`retrieval_dataset.py:255-280`）。源码未包含两者之间的同步作业。
- `RetrievalDataset` 引用了 `32fav_video_14to30day.parquet`（`retrieval_dataset.py:325`），但开源的 rankall 窗口配置里没有对应窗口，Strato 侧也没有 `32fav_video` 索引名。

召回服务如何用这份语料打分，见 [Phoenix 召回：双塔检索架构](https://x-algorithm.vergego.ai/concepts/phoenix-retrieval/)。训练时怎样把语料嵌入写进 checkpoint，见 [Phoenix 训练](https://x-algorithm.vergego.ai/concepts/phoenix-training/)。

## 设计决策

| 决策 | 理由 |
|------|------|
| 点赞按 2 的幂次触发 | 热帖每秒可能收到大量点赞。按 log2 触发把重新评估次数从 O(点赞数) 降到 O(log 点赞数)，同时保证 1、2、4、8……等关键门槛都会被检查到。 |
| 入索引前查 VF，安全级别用 `TimelineHomeRecommendations` | 召回候选是“推荐给陌生人”的帖子。不带观看者的推荐级判定可以提前一次算完，避免每个请求都对百万级语料做过滤。 |
| `search_unfiltered`、evergreen 不查 VF | 这些索引用途不同（命名暗示用于搜索与长青视频），由使用方做过滤。源码未包含具体用途。 |
| 窗口按发帖时间裁剪 | 召回需要“最近发布、已有互动的帖子”。直接比较 snowflake ID 即可，不需要另存时间戳。 |
| 版本文件 + 符号链接原子替换 | 读取方（训练暂存进程、推理进程）在任何时刻都读到完整文件，不会读到写了一半的 parquet。 |
| 落盘后才提交 Kafka 位点 | 内存表丢失时，服务可以从上次落盘点重放。代价是最多重复处理一个 180 秒周期。 |
| topic 列表同时写 Manhattan 和 Kafka | Manhattan 供在线按帖子查询；Kafka → parquet 供推理进程整批构建位图。 |

## 源码锚点

- `phoenix-rankall-strato/stream_forwarders/postCreationEventForwarder.strato:3-23` —— 发帖事件入口
- `phoenix-rankall-strato/stream_forwarders/favoriteEventForwarder.strato:3-21` —— 点赞事件入口
- `phoenix-rankall-strato/columns/favoriteEventProcessor.strato:14-160` —— 2 的幂次限流
- `phoenix-rankall-strato/lib/eventProcessing.strato:28-70` —— 限流常量与触发条件
- `phoenix-rankall-strato/lib/eventProcessing.strato:260-279` —— `shouldDropPostByVF`
- `phoenix-rankall-strato/columns/phoenix_rank_all/phoenixRankAllCandidateProcessor.strato:419-539` —— 索引分流主逻辑
- `phoenix-rankall-strato/columns/phoenix_rank_all/phoenixRankAllCandidateProcessor.strato:213-261` —— 6 套 topic 实验选项
- `phoenix-rankall-strato/columns/mh/filteredTopicsByExperiment.strato:5-30` —— topic 列表存储
- `phoenix-rankall-strato/columns/tweetUpdateThresholdsCache.strato:13-25` —— 阈值缓存
- `phoenix-rankall/src/config/mod.rs:9-228` —— CLI 默认值、管线 topic、时间窗
- `phoenix-rankall/src/main.rs:76-254` —— 管线装配
- `phoenix-rankall/src/processor/blacklist.rs:13-34` —— 黑名单
- `phoenix-rankall/src/sid/client.rs:64-100` —— 语义 ID 查询与新鲜度门槛
- `phoenix-rankall/src/store/base.rs:322-420` —— 裁剪与落盘
- `phoenix-rankall/src/store/writer.rs:9-49` —— 原子写 parquet
- `phoenix/xrex/data/retrieval_dataset.py:290-325` —— 召回语料子集定义
- `phoenix/xrex/inference/model_runner.py:99-107` —— topic parquet 路径

## 相关页面

- [Phoenix 召回：双塔检索架构](https://x-algorithm.vergego.ai/concepts/phoenix-retrieval/) —— 读取这些索引的召回模型
- [Phoenix 训练](https://x-algorithm.vergego.ai/concepts/phoenix-training/) —— 训练时如何把语料烘进 checkpoint
- [可见性过滤](https://x-algorithm.vergego.ai/concepts/visibility-filtering/) —— `shouldDropTweet` 背后的规则
- [哈希嵌入](https://x-algorithm.vergego.ai/concepts/hash-based-embeddings/) —— 语义 ID 在模型里的用法
- [多模态帖子嵌入器](https://x-algorithm.vergego.ai/concepts/multimodal-embedders/) —— 语义 ID 与 `mm_emb_metadata` 的上游
- [home-mixer 编排层](https://x-algorithm.vergego.ai/concepts/home-mixer-orchestration/) —— 召回在 home-mixer 候选源中的位置
- [系统架构总览](https://x-algorithm.vergego.ai/concepts/system-architecture/)

Source: https://x-algorithm.vergego.ai/concepts/phoenix-rankall/index.mdx
