广场动态推荐改造复盘:一套「配置化 + 热点召回」的信息流架构

封面:广场推荐方案

周一早上,运营在群里问:广场能不能把调仓动态也放进推荐流,下午就要上。旧逻辑按时间序写死,改一次展示就要发一次版。内容写入、召回、配置、清理由四拨人各管一段,这次改造把五个环节串成一条链路。

本篇你能学到 ​

  • 用一张轻量索引表收口多类动态召回,避免直查业务大表。
  • 用 biz_type 加 ext_config 的 JSON 配置,把展示形态从发版里解耦。
  • 按用户状态分流召回,给没有行为数据的冷启动用户留时间降序兜底。
  • 用穷举表格核对跨 4 个组件 7 个写入入口,避免推荐池缺内容。
  • 给索引表配每日清理任务,控制表体积。

一、整体链路:五个环节串成一条信息流 ​

广场模块要支持动态推荐能力:不再按单一时间序吐内容,而是把分享、调仓、发帖等各类动态按类型组织起来,运营侧还能灵活调整展示形态。

这类需求容易把注意力全放在推荐算法上。真正决定上线质量和迭代速度的,往往是工程侧的结构:内容怎么进池、怎么召回、怎么展示、怎么治理。

1. 五个环节分别解决什么问题 ​

这次改造的核心改动五条:

  1. 新增业务开关配置表,支持广场功能开关与展示类型配置化;
  2. 新增广场热点召回索引表,提升热点内容召回效率;
  3. 新增动态推荐接口,根据用户状态使用不同召回策略;
  4. 分享、调仓、发帖等内容写入时同步生成热点索引数据;
  5. 新增定时任务清理过期索引数据。

把五条改动放回业务流程,就是一条从写入到清理的链路:

text
内容写入
  → 同步生成热点索引
  → 推荐接口按用户状态分流召回
  → 开关配置控制展示
  → 每日清理过期索引

每个环节都有明确的交付物:写入环节保证内容进池,索引环节保证召回够快,分流环节保证不同用户看到合适的内容,配置环节保证运营能调整,清理环节保证索引表不膨胀。

配图

图 1:推荐五环节,内容写入、热点索引、状态分流、开关配置、每日清理

图 1:推荐五环节,内容写入、热点索引、状态分流、开关配置、每日清理

2. 为什么把链路拆成五段 ​

推荐类需求最常见的失败,不是算法不够好,而是链路缺一段。写入漏一个入口,池子里就少一类内容;召回直查业务大表,接口就会被拖慢;配置写死在代码里,运营每改一次展示都要排期。

拆成五段之后,每一段都可以单独验收。写入看入口清单,召回看索引表,分流看用户状态,配置看开关表,清理看定时任务。出了问题也能快速定位是哪一段断的。

3. 预期结果 ​

上线后,分享、调仓、发帖等动态都能按类型进入推荐池。登录且已加圈的用户看到按类型轮询的内容,游客和未加圈用户看到时间降序内容。运营调整展示类型不需要发版,过期索引每天 01:00 自动清理。

二、两张核心表:把“会变的”挪出代码 ​

推荐流有两个东西天然会变:展示形态和热点内容。前者变化频繁,后者有保质期。用两张表分别承接,变化就不会传导到代码里。

1. 开关配置表:展示配置不写死 ​

t_biz_switch_config 承担业务开关与扩展配置,动态展示类型配置放在 ext_config 字段里。

这里有个值得说的选择:展示配置用 JSON 扩展列而不是结构化列建模。理由有三:

  • 展示配置天然是嵌套结构(总开关 → 类型子开关 → 排序方式),拍平成关系列会很别扭;
  • JSON 列加新配置项不需要改表结构;
  • 配置调整不需要发版,改数据即生效,推荐流这种“展示形态要反复试”的功能,把发版从迭代环里摘出去,价值非常大。

同一张表通过 biz_type 区分业务域(搜索栏 SEARCH_BAR、动态列表 DYNAMIC_LIST),一个配置模式服务多个功能模块,后续接新的展示位不用重复建表。

搜索栏配置(biz_type = SEARCH_BAR):

text
id=1 搜索栏展示总开关
├── id=2 动态-搜索框(分享/调仓/话题/发帖)
├── id=3 圈子-搜索框
└── id=4 关注-搜索框

动态列表配置(biz_type = DYNAMIC_LIST):

text
id=9 动态列表展示总开关
├── id=10 分享
├── id=11 调仓
├── id=12 话题
└── id=13 发帖

两级结构:总开关控制功能入口,类型级开关控制每种动态是否展示。一次配置同时决定广场展示哪些类型的内容。

排序逻辑上还做了一个预留:PlazaServiceImpl#buildSortTemplate 把排序模板从主流程中拆分出来,后续支持后台动态配置。排序规则是推荐流里最常被调整的部分,独立成模板,等于给“策略会变”这件事留好了口子。

2. 热点召回索引表:召回不直查业务大表 ​

t_plaza_hot_recall_index 是整个方案的查询核心。设计上的取舍在于:召回逻辑是直接查业务表,还是单独建一张轻量索引表?

直查业务表的问题很实际:各类动态分散在不同的业务表里,字段多、行宽大,只为召回却要扛出整行数据;查询要跨表 UNION,逻辑越写越散;过期内容没法独立清理。

单独建索引表之后,三个问题同时解决:只存召回必需字段,查询轻快;所有类型的动态统一收口到一张表,召回逻辑收敛在单表;过期数据按时间整批清理,完全不碰业务数据。

3. 预期结果 ​

展示配置全部落在 ext_config,运营改数据即可调整搜索栏和动态列表。召回查询只扫 t_plaza_hot_recall_index,不再跨业务大表 UNION。新增展示位时复用同一张开关表,按 biz_type 扩展。

三、召回策略:按用户状态分流 ​

推荐策略的前提是用户行为数据。没有数据时的“个性化”是瞎猜,分流的意义就在于承认这一点。

1. 推荐接口与分流规则 ​

新增的推荐接口 /option/plaza/dynamicRecall,核心分流逻辑只有一张表:

用户状态召回策略排序规则设计意图
登录且已加圈按类型轮询召回默认或配置排序保证内容多样性
未登录或未加圈时间降序召回生产时间倒序保证内容时效性

登录且加了圈的用户,我们有他的兴趣偏好,可以讲究多样性;游客和没加圈的用户没有任何行为数据,任何“个性化”都是瞎猜。这种时候,时间倒序反而是最稳的策略,至少保证用户看到的是最新内容。

2. 分流逻辑怎么写 ​

接口层只做一件事:判断用户状态,选择对应召回策略。伪代码如下。

text
recall(userId):
    若 已登录 且 已加入圈子:
        return 按类型轮询召回(排序 = 默认或配置排序)
    否则:
        return 按生产时间降序召回()

这也是冷启动场景的通用二分法:有行为数据才上策略,没行为数据就上时间。分流条件放在接口入口,后续新增用户分层时只改这一处判断,不用动两套召回实现。

3. 预期结果 ​

登录且已加圈用户的首屏能同时看到分享、调仓、话题、发帖等类型,不会连续刷出同一种内容。游客和未加圈用户按生产时间倒序看到最新内容,冷启动不再出现“猜错兴趣”的推荐。

四、写入链路与清理:让索引表持续可用 ​

索引表要有人写、有人清。写入漏入口,池子缺内容;清理不配,轻量表迟早膨胀。

1. 写入链路:跨 4 个组件 7 个入口 ​

这次改造里工程量最大、也最容易漏的环节是写入链路:所有内容生产入口都要同步写热点索引,漏一个,推荐池里就缺一类内容。

按内容类型穷举如下。

分享类(内容组件):

接口/入口说明
/api/app/decision/tool/share/execute决策工具分享
/community/strategyTradingOrderShare策略交易分享

调仓类(定时任务组件):

入口说明
ShareChangeServiceImpl#genTrendingTrade股票调仓动态生成
FundsMessageProcessServiceImpl#saveMessage基金调仓动态生成

发帖类(社区核心组件 / 社区管理后台):

组件写入时机核心入口
社区核心组件AI 审核通过 / 人工审核通过 / 帖子发布到圈子AuditStateMachine#fireEvent
社区管理后台帖子上架CommunityPostServiceImpl#insertPostRecallIndex

两条经验值得单独说。

第一,写入时机挂在“过审”之后。帖子的索引写入挂在 AI 审核 / 人工审核通过、上架之后,而不是用户提交时。这样保证进推荐池的内容都是合规内容。如果挂在提交时,池子里就可能混进后来被审核拒绝的内容,还得再做反向清理。

第二,跨组件的入口必须穷举。本例的写入入口跨 4 个组件 7 个入口,代码分散在不同团队手里。这种范围靠“记忆和感觉”必然漏,唯一靠谱的办法就是像上面三张表一样按内容类型列表格、逐个入口确认。穷举表格本身就是方案交付物的一部分。

2. 清理任务:索引表的最后一环 ​

热点索引是“有保质期”的数据,过期的热点不再是热点。配套的清理任务 PlazaHotRecallIndexDataClearJob 每天 01:00 执行,清理过期的召回索引。

text
PlazaHotRecallIndexDataClearJob(每天 01:00):
    扫描 t_plaza_hot_recall_index 中已过期的记录
    按时间条件批量删除

这一环经常被忽视:不配清理任务,“轻量索引表”迟早膨胀成没人敢碰的大表,建索引表时换来的查询优势会被慢慢吃掉。执行时间选凌晨低峰期,对白天业务零影响。

3. 预期结果 ​

分享、调仓、发帖三类内容从各自的入口写入后,都能在推荐池里被召回。过审前的内容不会提前进池,被驳回的内容不需要反向清理。索引表每天凌晨清掉过期数据,查询性能保持稳定。

常见错误与排错 ​

  1. 现象:某类动态在推荐流里始终不出现。原因:写入入口漏了,或者索引写在了用户提交时。解法:按内容类型列出 4 个组件 7 个入口逐个核对,写入时机统一挂到审核通过或上架之后。
  2. 现象:推荐池里出现已被驳回的帖子。原因:索引在提交时就写入,审核拒绝后没有反向清理。解法:把写入后移到 AI 审核、人工审核通过、上架之后。
  3. 现象:索引表越来越大,召回查询逐渐变慢。原因:没有配置过期清理任务。解法:启用 PlazaHotRecallIndexDataClearJob,每天 01:00 清理过期记录。
  4. 现象:游客刷到的内容点击率反而低。原因:对没有行为数据的用户也走了类型轮询。解法:未登录或未加圈统一走时间降序,登录且已加圈才按类型轮询。
  5. 现象:运营调整广场展示类型要排期发版。原因:展示配置写死在代码里。解法:把配置放进 t_biz_switch_config.ext_config,按 SEARCH_BAR / DYNAMIC_LIST 区分业务域,改数据生效。

本篇小结 ​

  • 五个环节要逐段验收:内容写入、热点索引、状态分流、开关配置、每日清理。
  • 召回走 t_plaza_hot_recall_index,不直查业务大表。
  • 展示配置放 ext_config,按 biz_type 复用同一张开关表。
  • 写入入口按内容类型穷举,时机挂在过审之后。
  • 索引表配每日清理,避免轻量表膨胀成大表。