videoContentList 推荐列表排序逻辑接口:
POST /contentPlatform/plan/videoContentList入口:ContentPlatformPlanServiceImpl.getVideoContentList数据源:content_platform_demand_video(先验/后验/先验-场景)+content_platform_video(全局热门)适用:当前分支
cooperation_video_candidate_pool_improved_lld_0509(含 commit2860bdce)。
title 非空 ───────────► 全站搜索 (getVideoContentListByTitle)
source=prior ───────────► 单源「粉丝喜欢」(getSingleSourcePage / prior)
source=posterior ───────► 单源「已发优质相似」(getSingleSourcePage / posterior)
source=hot ───────────► 单源「全局热门」(getHotSourcePaged)
source 空(默认) ──────► 四路随机穿插 (getInterleavedPage)
当前前端:
prior和posterior两个 tab 已 disabled(提示"功能正在完善中"),生产实际只走「全部」(四路穿插)和「全局热门」单源。
| 常量 | 值 | 含义 |
|---|---|---|
DEMAND_CANDIDATE_LIMIT |
10000 | 每个 demand 池最大候选条数 |
HOT_CANDIDATE_LIMIT |
10000 | hot 池候选条数 |
TOP_K_PER_DEMAND |
3 | demand 池组内取前 K |
DEMAND_STRATEGY_PRIOR |
"先验需求" |
先验池过滤值 |
DEMAND_STRATEGY_PRIOR_SCENE |
"先验需求-场景" |
场景池过滤值 |
DEMAND_STRATEGY_POSTERIOR |
"后验需求" |
后验池过滤值 |
PRIOR_PREMIUM_DIMENSION |
"传播的头部" |
先验池 dimension 强过滤 |
PRIOR_GROUP_KEEP_RATIO |
0.5 |
先验池"特征组"按 total_rov 分位保留比例 |
POSTERIOR_FILTER_ABS_LIKE |
"绝对高效率%" |
后验池 A 段 demand_filter_sort_strategy LIKE |
POSTERIOR_FILTER_REL_LIKE |
"相对裂变率%" |
后验池 B 段 demand_filter_sort_strategy LIKE |
POSTERIOR_DRIVE_DIMENSION_TIME |
"昨日" |
后验池强制 drive_dimension_time |
公共强过滤(所有 demand 池 SQL):dt = max(dt) AND status = 1 AND crowd_segment = user.channel。
fetchPriorSceneCandidates(场景池)目的:用户所属 channel 在"场景"维度命中的先验,按视频近 7 日表现(rov)排序。
SELECT ... FROM content_platform_demand_video
WHERE dt=:maxDt AND status=1
AND crowd_segment=:userChannel
AND demand_strategy='先验需求-场景'
AND channel_level3=:ghName -- 若传入
ORDER BY total_rov DESC, score DESC
LIMIT 10000
退化:如果 ghName 非空但查 0 条,去掉 ghName 再查一次(拿全渠道兜底)。
后处理(顺序):
videoId 去重,保留首次出现(SQL 已按 total_rov DESC, score DESC 排序 → 首次 = 该视频的"最强代表需求"行)rov 为 null 或 ≤0 的视频rov DESC 主键,total_rov DESC 次级 tiebreaker输出顺序:视频按 rov(近 7 日表现)DESC;同 rov 时按代表需求的 total_rov DESC。
设计动机:场景需求里同一视频会对应多个特征点,但前端只下发一条;用 total_rov 选"最强代表需求",再以视频自身的 rov 决定整体排序,避免按需求维度排序导致同 dimension/标准要素的视频在列表里扎堆。
fetchPriorCandidates(先验池)目的:先验需求里,只取 dimension='传播的头部' 维度,并按 channel 内"特征需求强度"分位裁掉弱题材。
单段查询:
SELECT ... WHERE ... AND demand_strategy='先验需求' AND dimension='传播的头部' ...
ORDER BY total_rov DESC, score DESC LIMIT 30000
退化:查询为空且 ghName 非空 → 去 ghName 重查。
[新] 特征组分位裁剪 (retainTopGroupsByTotalRov,keepRatio=0.5):
(point_type, standard_element) 分组,取每组 max(total_rov)(即该特征的人群需求强度)设计动机:
total_rov在 prior 池 = 群体对(point_type, standard_element)特征的需求强度。低 total_rov 说明群体不爱这个题材,把对应视频堆在列表底部没意义,直接剪掉。
进 groupAndTopK:
(point_type, standard_element)rov <= 0 或 null(近 7 日无表现)total_rov DESCscore DESC,组内 videoId 去重,每组最多 3 条最后按 limit=10000 截断。
最终顺序:保留 top 50% 特征组内,组按总 ROV,组内按 score。
fetchPosteriorCandidates(后验池)目的:后验需求里,"昨日"驱动的"绝对高效率"先出,再出"相对裂变率"。
A、B 两段独立查询:
-- A 段: demand_filter_sort_strategy LIKE '绝对高效率%'
SELECT ... WHERE ... AND demand_strategy='后验需求'
AND demand_filter_sort_strategy LIKE '绝对高效率%'
AND drive_dimension_time='昨日'
AND (title IS NULL OR demand_content_title IS NULL OR title <> demand_content_title)
ORDER BY total_rov DESC, score DESC LIMIT 30000
-- B 段: demand_filter_sort_strategy LIKE '相对裂变率%'
SELECT ... AND demand_filter_sort_strategy LIKE '相对裂变率%' AND drive_dimension_time='昨日' ...
退化:A、B 都空且 ghName 非空 → 去 ghName 重查(drive_dimension_time='昨日' 仍保留)。
每段进 groupAndTopK:
demand_content_idrov <= 0 或 nullexcludeSelfTitle=true → 用 TitleNormalizer.isSelfTitle 跳过自标题total_rov DESCscore DESC,去重,每组最多 3 条A 段 + B 段 顺序拼接 → 跨段 videoId 去重 → 截 10000。
最终顺序:A 段(绝对高效率) 在前;段内组按总 ROV,组内按 score。
fetchHotCandidates(热门池)复用现有 planMapperExt.getVideoList(...):
dt = videoMaxDt,datastatDt = videoDatastatMaxDtsort/type/channel/strategy 由请求和 param.getSort() 派生fission_rate DESC 或 score DESC)getInterleavedPage)priorScene → 标 source='prior'
prior → 标 source='prior'
posterior → 标 source='posterior'
hot → 标 source='hot'
priorScene 和 prior 对外都是
source='prior'(前端"粉丝喜欢"统一标签);浮层细节通过demandStrategy字段区分场景。
pointer[i] 和 exhausted[i],以及全局 emittedIds + emittedTitles。seed = userId ^ LocalDate.now().toString().hashCode()
shouldSkipForDedup)merged、记入 emittedIds 和 emittedTitlespaginateCandidates:totalSize = merged.size(),按 pageNum/pageSize 内存切片返回。标题去重用
TitleNormalizer.normalize(去 emoji/空白/全半角),应对运营把同段内容重复上传成多个 video_id(如🔴她走了,台湾再无洪秀柱!对应 67396144 / 67812469 两条)。 单源source=prior模式的interleavePriorWithScene也用同一套(video_id + 标题)去重。
priorScene / prior / posterior 内部相对顺序保留(场景按视频 rov;先验/后验按组 total_rov + 组内 score),随机只影响"哪一池先出"source=prior)scene = fetchPriorSceneCandidates(...)
prior = fetchPriorCandidates(...)
list = interleavePriorWithScene(scene, prior) // 严格 1:1
interleavePriorWithScene:
scene 取一条未发的 → 再从 prior 取一条未发的,严格 1:1 交替seen<videoId> 跨池去重,场景优先(先到先得)每条 VO 设 source='prior',然后 paginateCandidates 切片。
source=posterior)list = fetchPosteriorCandidates(...) // 顺序 = 绝对高效率段 → 相对裂变率段
每条 VO 设 source='posterior',paginateCandidates 切片。
source=hot)不走候选池:复用原 planMapperExt.getVideoCount + getVideoList 真分页链路,DB 端 LIMIT/OFFSET。VO source='hot'。
source 字段source |
含义 | 数据来源 | 浮层 demand 字段 |
|---|---|---|---|
prior |
粉丝喜欢(场景 + 先验头部 + 先验其他) | content_platform_demand_video |
有完整字段,可看 demandStrategy 区分 |
posterior |
已发优质相似 | content_platform_demand_video |
有完整字段 |
hot |
全局热门 | content_platform_video |
只有基础字段(demand 相关字段为空) |
┌────────────────────────────────────────────────────────┐
│ 1. 拉 4 个候选池 (每个池都自己分阶段、分组、去重、排序) │
└────────────────────────────────────────────────────────┘
│
┌───────────────────────┬──┴──┬─────────────────────────────┐
▼ ▼ ▼ ▼
priorScene(10000) prior(10000) posterior(10000) hot(10000)
视频维度 rov DESC 传播头部 A: 绝对高效率 + 昨日 SQL 默认
(代表需求 total_rov 选最大) B: 相对裂变率 + 昨日 (sort 决定)
组(point_type, standard_element) top3
组(demand_content_id) top3
│
▼
┌────────────────────────────────────────────────────────┐
│ 2. 四路随机穿插 │
│ - 种子 = userId ^ 今天 │
│ - 跨池 videoId 去重 │
│ - 池内顺序保留 │
└────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 3. paginateCandidates │
│ totalSize = 全量, 按 pageNum/pageSize 切片 │
└────────────────────────────────────────────────────────┘
| 内容 | 位置 |
|---|---|
| 主入口路由 | ContentPlatformPlanServiceImpl.java:626 |
| 单源分页 | ContentPlatformPlanServiceImpl.java:649 |
| 单源 hot | ContentPlatformPlanServiceImpl.java:698 |
| 四路穿插 | ContentPlatformPlanServiceImpl.java:743 |
| 场景池 fetcher | ContentPlatformPlanServiceImpl.java:812 |
| 先验池 fetcher | ContentPlatformPlanServiceImpl.java:840 |
| 后验池 fetcher | ContentPlatformPlanServiceImpl.java:875 |
groupAndTopK 通用排序 |
ContentPlatformPlanServiceImpl.java:912 |
| 段间拼接 + 去重 | ContentPlatformPlanServiceImpl.java:956 |
| 热门池 fetcher | ContentPlatformPlanServiceImpl.java:976 |
| Mapper SQL | mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.xml (selectForRecommend) |