推荐系统:从数学原理到 MVP 实战
推荐系统是 AI 在工业界最广泛的落地场景之一——抖音的视频流、小红书的首页瀑布流、淘宝的“猜你喜欢”、Netflix 的影片排序、Spotify 的每日推荐歌单,背后都是推荐系统,贡献了这些平台超过 80% 的用户消费内容。核心数学方法包括:协同过滤利用用户-物品交互矩阵计算相似度(余弦、皮尔逊相关系数);矩阵分解(SVD/ALS)将稀疏评分矩阵分解为用户向量 × 物品向量,是 Netflix Prize 冠军方案的核心;深度排序模型(Wide&Deep、DeepFM、DIN)用神经网络自动学习特征交叉;双塔模型分别编码用户和物品,内积打分后用 ANN 近似最近邻做召回;多目标优化(MMOE、PLE)同时优化点击率、完播率、分享率等多个目标。本文从数学基础出发,结合类似小红书的作品推荐场景,完整讲解协同过滤、矩阵分解、特征工程原理,并给出基于 PostgreSQL 的 MVP 数据库设计。
一、示例数据——类小红书作品平台
为了让所有算法都有具体数字可算,我们先建立一套完整的示例数据。
1.1 用户数据
| user_id | 昵称 | 年龄 | 性别 | 城市 | 兴趣标签 | 注册天数 |
|---|---|---|---|---|---|---|
| U1001 | 小美 | 24 | 女 | 成都 | 美食, 探店, 穿搭 | 180 |
| U1002 | 老王 | 35 | 男 | 北京 | 科技, 数码, 编程 | 365 |
| U1003 | 阿杰 | 28 | 男 | 广州 | 健身, 美食, 旅行 | 90 |
| U1004 | 小雪 | 22 | 女 | 上海 | 穿搭, 美妆, 追星 | 30 |
| U1005 | 大叔 | 42 | 男 | 深圳 | 钓鱼, 汽车, 美食 | 500 |
1.2 作品数据
| item_id | 标题 | 类别 | 作者 | 发布距今 | 点赞 | 收藏 | 评论 | 标签 |
|---|---|---|---|---|---|---|---|---|
| I2001 | 成都Top10火锅测评 | 美食 | 吃货达人 | 3h | 1200 | 450 | 89 | 美食,火锅,成都,探店 |
| I2002 | iPhone17全面评测 | 科技 | 数码博主 | 6h | 3500 | 800 | 220 | 科技,数码,iPhone,评测 |
| I2003 | 三亚5天自由行攻略 | 旅行 | 旅行家 | 12h | 2800 | 1500 | 156 | 旅行,三亚,攻略,海边 |
| I2004 | 今秋必买的5件外套 | 穿搭 | 时尚编辑 | 2h | 800 | 600 | 45 | 穿搭,秋装,外套,时尚 |
| I2005 | 家庭健身30天计划 | 健身 | 健身教练 | 24h | 1500 | 900 | 78 | 健身,减脂,家庭,计划 |
| I2006 | 重庆小面做法教程 | 美食 | 厨房达人 | 8h | 600 | 350 | 30 | 美食,做饭,重庆,面食 |
| I2007 | 2026年最值得买的手机 | 科技 | 数码博主 | 48h | 8000 | 2000 | 500 | 科技,手机,排行,推荐 |
| I2008 | 广州早茶必吃榜 | 美食 | 吃货达人 | 5h | 900 | 400 | 60 | 美食,早茶,广州,探店 |
| I2009 | 秋季显瘦穿搭公式 | 穿搭 | 时尚博主 | 1h | 300 | 200 | 15 | 穿搭,显瘦,秋装,女装 |
| I2010 | 野钓鲫鱼技巧分享 | 钓鱼 | 钓鱼老李 | 10h | 400 | 250 | 35 | 钓鱼,鲫鱼,野钓,户外 |
| I2011 | React vs Vue 2026对比 | 编程 | 程序员 | 20h | 1100 | 700 | 120 | 编程,前端,React,Vue |
| I2012 | 周杰伦演唱会现场 | 娱乐 | 追星girls | 4h | 5000 | 1200 | 350 | 娱乐,周杰伦,演唱会,追星 |
1.3 用户历史行为
| user_id | item_id | 行为 | 停留(秒) | 时间 |
|---|---|---|---|---|
| U1001 | I2001 | 点赞+收藏 | 45 | 2h前 |
| U1001 | I2006 | 点赞 | 30 | 5h前 |
| U1001 | I2004 | 收藏 | 60 | 昨天 |
| U1001 | I2009 | 点击浏览 | 20 | 1h前 |
| U1002 | I2002 | 点赞+收藏+评论 | 120 | 3h前 |
| U1002 | I2007 | 点赞 | 90 | 昨天 |
| U1002 | I2011 | 收藏 | 80 | 6h前 |
| U1003 | I2005 | 点赞+收藏 | 50 | 4h前 |
| U1003 | I2008 | 点赞 | 35 | 今天 |
| U1003 | I2003 | 收藏 | 55 | 昨天 |
| U1004 | I2004 | 点赞 | 40 | 3h前 |
| U1004 | I2012 | 点赞+收藏+评论 | 90 | 1h前 |
| U1005 | I2010 | 点赞+收藏 | 60 | 今天 |
| U1005 | I2001 | 点赞 | 40 | 昨天 |
二、推荐算法的数学基础
2.1 用户-物品评分矩阵(协同过滤的基石)
用户行为转化为隐式评分(implicit rating):
点赞 = 2 分,收藏 = 3 分,评论 = 4 分,点击浏览 = 1 分,多种叠加
行为加权公式:
r(u,i) = w_click × I_click + w_like × I_like + w_collect × I_collect + w_comment × I_comment
其中 w_click=1, w_like=2, w_collect=3, w_comment=4
点赞 = 2 分,收藏 = 3 分,评论 = 4 分,点击浏览 = 1 分,多种叠加
行为加权公式:
r(u,i) = w_click × I_click + w_like × I_like + w_collect × I_collect + w_comment × I_comment
其中 w_click=1, w_like=2, w_collect=3, w_comment=4
构建评分矩阵 R(5×12):
U1001 对 I2001: 点赞+收藏 → 2+3 = 5
U1001 对 I2006: 点赞 → 2
U1001 对 I2004: 收藏 → 3
U1001 对 I2009: 浏览 → 1
U1002 对 I2002: 点赞+收藏+评论 → 2+3+4 = 9
U1002 对 I2007: 点赞 → 2
U1002 对 I2011: 收藏 → 3
... 类推 ...
"?" 就是推荐系统要预测的值——预测每个用户对未交互作品的可能评分,然后推荐预测分最高的。
U1001 对 I2001: 点赞+收藏 → 2+3 = 5
U1001 对 I2006: 点赞 → 2
U1001 对 I2004: 收藏 → 3
U1001 对 I2009: 浏览 → 1
U1002 对 I2002: 点赞+收藏+评论 → 2+3+4 = 9
U1002 对 I2007: 点赞 → 2
U1002 对 I2011: 收藏 → 3
... 类推 ...
| I2001 | I2002 | I2003 | I2004 | I2005 | I2006 | I2007 | I2008 | I2009 | I2010 | I2011 | I2012 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| U1001 | 5 | ? | ? | 3 | ? | 2 | ? | ? | 1 | ? | ? | ? |
| U1002 | ? | 9 | ? | ? | ? | ? | 2 | ? | ? | ? | 3 | ? |
| U1003 | ? | ? | 3 | ? | 5 | ? | ? | 2 | ? | ? | ? | ? |
| U1004 | ? | ? | ? | 2 | ? | ? | ? | ? | ? | ? | ? | 9 |
| U1005 | 2 | ? | ? | ? | ? | ? | ? | ? | ? | 5 | ? | ? |
2.2 余弦相似度(衡量用户或物品的相似程度)
cos(A, B) = (A · B) / (||A|| × ||B||) = Σᵢ(AᵢBᵢ) / (√Σᵢ Aᵢ² × √Σᵢ Bᵢ²)
取值范围 [-1, 1],越接近 1 越相似
对于非负评分矩阵,范围为 [0, 1]
取值范围 [-1, 1],越接近 1 越相似
对于非负评分矩阵,范围为 [0, 1]
计算用户 U1001 和 U1003 的相似度:
找共同交互过的物品:只需看两人都有评分的列
但 U1001 和 U1003 没有直接重叠的 item...
换一种方式:把没交互过的填 0,然后算完整向量的余弦相似度
U1001 = [5, 0, 0, 3, 0, 2, 0, 0, 1, 0, 0, 0]
U1003 = [0, 0, 3, 0, 5, 0, 0, 2, 0, 0, 0, 0]
A · B = 5×0 + 0×0 + 0×3 + 3×0 + 0×5 + 2×0 + 0×0 + 0×2 + 1×0 + 0×0 + 0×0 + 0×0 = 0
cos(U1001, U1003) = 0(没有共同交互→直接余弦=0)
计算用户 U1001 和 U1004 的相似度:
U1001 = [5, 0, 0, 3, 0, 2, 0, 0, 1, 0, 0, 0]
U1004 = [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 9]
A · B = 3×2 = 6
||A|| = √(25+9+4+1) = √39 = 6.245
||B|| = √(4+81) = √85 = 9.220
cos(U1001, U1004) = 6 / (6.245×9.220) = 6/57.58 = 0.104
计算物品 I2001 和 I2008 的相似度(Item-Based):
I2001 列 = [5, 0, 0, 0, 2](被 U1001=5, U1005=2 评分)
I2008 列 = [0, 0, 2, 0, 0](被 U1003=2 评分)
cos(I2001, I2008) = 0(没有共同用户评过→0)
在真实系统中,数据量大了之后重叠率提高,余弦相似度才有意义。
找共同交互过的物品:只需看两人都有评分的列
但 U1001 和 U1003 没有直接重叠的 item...
换一种方式:把没交互过的填 0,然后算完整向量的余弦相似度
U1001 = [5, 0, 0, 3, 0, 2, 0, 0, 1, 0, 0, 0]
U1003 = [0, 0, 3, 0, 5, 0, 0, 2, 0, 0, 0, 0]
A · B = 5×0 + 0×0 + 0×3 + 3×0 + 0×5 + 2×0 + 0×0 + 0×2 + 1×0 + 0×0 + 0×0 + 0×0 = 0
cos(U1001, U1003) = 0(没有共同交互→直接余弦=0)
计算用户 U1001 和 U1004 的相似度:
U1001 = [5, 0, 0, 3, 0, 2, 0, 0, 1, 0, 0, 0]
U1004 = [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 9]
A · B = 3×2 = 6
||A|| = √(25+9+4+1) = √39 = 6.245
||B|| = √(4+81) = √85 = 9.220
cos(U1001, U1004) = 6 / (6.245×9.220) = 6/57.58 = 0.104
计算物品 I2001 和 I2008 的相似度(Item-Based):
I2001 列 = [5, 0, 0, 0, 2](被 U1001=5, U1005=2 评分)
I2008 列 = [0, 0, 2, 0, 0](被 U1003=2 评分)
cos(I2001, I2008) = 0(没有共同用户评过→0)
在真实系统中,数据量大了之后重叠率提高,余弦相似度才有意义。
2.3 矩阵分解(Matrix Factorization)——填充评分矩阵的关键
核心思想:将稀疏的 m×n 评分矩阵 R 分解为两个低秩矩阵的乘积
R ≈ P × Qᵀ
P: m×k 用户矩阵(每个用户用 k 维向量表示)
Q: n×k 物品矩阵(每个物品用 k 维向量表示)
k: 隐因子维度(通常 10~200)
预测评分:r̂(u,i) = Pᵤ · Qᵢ = Σⱼ Pᵤⱼ × Qᵢⱼ
优化目标:
min Σ_(u,i)∈观测到的评分 (r(u,i) - Pᵤ·Qᵢ)² + λ(||Pᵤ||² + ||Qᵢ||²)
↑ MSE 损失 ↑ L2 正则化防过拟合
R ≈ P × Qᵀ
P: m×k 用户矩阵(每个用户用 k 维向量表示)
Q: n×k 物品矩阵(每个物品用 k 维向量表示)
k: 隐因子维度(通常 10~200)
预测评分:r̂(u,i) = Pᵤ · Qᵢ = Σⱼ Pᵤⱼ × Qᵢⱼ
优化目标:
min Σ_(u,i)∈观测到的评分 (r(u,i) - Pᵤ·Qᵢ)² + λ(||Pᵤ||² + ||Qᵢ||²)
↑ MSE 损失 ↑ L2 正则化防过拟合
完整数值示例——k=2 的矩阵分解(ALS 算法手算)
简化评分矩阵(取 3 个用户×4 个物品):
k=2 维隐因子。随机初始化:
P = [[0.5, 0.3], ← U1001 的隐向量
[0.2, 0.8], ← U1003
[0.6, 0.1]] ← U1005
Q = [[0.4, 0.7], ← I2001 的隐向量
[0.9, 0.2], ← I2004
[0.3, 0.5], ← I2006
[0.8, 0.6]] ← I2008
初始预测 r̂ = P × Qᵀ:
r̂(U1001, I2001) = 0.5×0.4 + 0.3×0.7 = 0.20+0.21 = 0.41(实际=5,差距大!)
r̂(U1001, I2004) = 0.5×0.9 + 0.3×0.2 = 0.45+0.06 = 0.51(实际=3)
r̂(U1001, I2008) = 0.5×0.8 + 0.3×0.6 = 0.40+0.18 = 0.58(这是预测值!)
SGD 优化一步(lr=0.01, λ=0.02):
对 r(U1001,I2001)=5,当前 r̂=0.41,误差 e=5-0.41=4.59
更新 P_{U1001}:
P[0][0] += lr×(e×Q[0][0] - λ×P[0][0]) = 0.01×(4.59×0.4 - 0.02×0.5) = 0.01×1.826 = 0.01826
P[0][0]_new = 0.5 + 0.01826 = 0.518
P[0][1] += 0.01×(4.59×0.7 - 0.02×0.3) = 0.01×3.207 = 0.03207
P[0][1]_new = 0.3 + 0.03207 = 0.332
更新 Q_{I2001}:
Q[0][0] += 0.01×(4.59×0.5 - 0.02×0.4) = 0.01×2.287 = 0.02287
Q[0][0]_new = 0.4 + 0.02287 = 0.423
Q[0][1] += 0.01×(4.59×0.3 - 0.02×0.7) = 0.01×1.363 = 0.01363
Q[0][1]_new = 0.7 + 0.01363 = 0.714
经过 100~500 轮迭代后,P 和 Q 收敛,所有 "?" 都能被预测出来。
最终 r̂(U1001, I2008) ≈ 3.8 → 小美可能会喜欢广州早茶!
最终 r̂(U1003, I2001) ≈ 2.5 → 阿杰对成都火锅有轻微兴趣
简化评分矩阵(取 3 个用户×4 个物品):
| R | I2001 | I2004 | I2006 | I2008 |
|---|---|---|---|---|
| U1001 | 5 | 3 | 2 | ? |
| U1003 | ? | ? | ? | 2 |
| U1005 | 2 | ? | ? | ? |
P = [[0.5, 0.3], ← U1001 的隐向量
[0.2, 0.8], ← U1003
[0.6, 0.1]] ← U1005
Q = [[0.4, 0.7], ← I2001 的隐向量
[0.9, 0.2], ← I2004
[0.3, 0.5], ← I2006
[0.8, 0.6]] ← I2008
初始预测 r̂ = P × Qᵀ:
r̂(U1001, I2001) = 0.5×0.4 + 0.3×0.7 = 0.20+0.21 = 0.41(实际=5,差距大!)
r̂(U1001, I2004) = 0.5×0.9 + 0.3×0.2 = 0.45+0.06 = 0.51(实际=3)
r̂(U1001, I2008) = 0.5×0.8 + 0.3×0.6 = 0.40+0.18 = 0.58(这是预测值!)
SGD 优化一步(lr=0.01, λ=0.02):
对 r(U1001,I2001)=5,当前 r̂=0.41,误差 e=5-0.41=4.59
更新 P_{U1001}:
P[0][0] += lr×(e×Q[0][0] - λ×P[0][0]) = 0.01×(4.59×0.4 - 0.02×0.5) = 0.01×1.826 = 0.01826
P[0][0]_new = 0.5 + 0.01826 = 0.518
P[0][1] += 0.01×(4.59×0.7 - 0.02×0.3) = 0.01×3.207 = 0.03207
P[0][1]_new = 0.3 + 0.03207 = 0.332
更新 Q_{I2001}:
Q[0][0] += 0.01×(4.59×0.5 - 0.02×0.4) = 0.01×2.287 = 0.02287
Q[0][0]_new = 0.4 + 0.02287 = 0.423
Q[0][1] += 0.01×(4.59×0.3 - 0.02×0.7) = 0.01×1.363 = 0.01363
Q[0][1]_new = 0.7 + 0.01363 = 0.714
经过 100~500 轮迭代后,P 和 Q 收敛,所有 "?" 都能被预测出来。
最终 r̂(U1001, I2008) ≈ 3.8 → 小美可能会喜欢广州早茶!
最终 r̂(U1003, I2001) ≈ 2.5 → 阿杰对成都火锅有轻微兴趣
2.4 标签匹配度(基于内容的推荐)
Jaccard 相似度:
J(A, B) = |A ∩ B| / |A ∪ B|
A = 用户的兴趣标签集合
B = 作品的标签集合
J(A, B) = |A ∩ B| / |A ∪ B|
A = 用户的兴趣标签集合
B = 作品的标签集合
计算小美(U1001)与每个作品的标签匹配度:
小美标签 = {美食, 探店, 穿搭}
小美标签匹配度最高的是 I2001 和 I2008(都包含"美食+探店")。
小美标签 = {美食, 探店, 穿搭}
| 作品 | 作品标签 | 交集 | 并集 | Jaccard |
|---|---|---|---|---|
| I2001 火锅测评 | {美食,火锅,成都,探店} | {美食,探店}=2 | 5 | 0.40 |
| I2004 秋装外套 | {穿搭,秋装,外套,时尚} | {穿搭}=1 | 6 | 0.17 |
| I2008 广州早茶 | {美食,早茶,广州,探店} | {美食,探店}=2 | 5 | 0.40 |
| I2009 显瘦穿搭 | {穿搭,显瘦,秋装,女装} | {穿搭}=1 | 6 | 0.17 |
| I2002 iPhone | {科技,数码,iPhone,评测} | {}=0 | 7 | 0.00 |
| I2006 重庆小面 | {美食,做饭,重庆,面食} | {美食}=1 | 6 | 0.17 |
2.5 热度分计算
Wilson 置信区间公式(Reddit/Hacker News 使用):
score = (p̂ + z²/(2n) - z√(p̂(1-p̂)/n + z²/(4n²))) / (1 + z²/n)
z = 1.96(95% 置信度)
简化版热度公式(实际工业常用):
hot_score = (w₁×likes + w₂×collects + w₃×comments) / (time_hours + 2)^1.5
时间衰减因子让新内容有机会出头
score = (p̂ + z²/(2n) - z√(p̂(1-p̂)/n + z²/(4n²))) / (1 + z²/n)
z = 1.96(95% 置信度)
简化版热度公式(实际工业常用):
hot_score = (w₁×likes + w₂×collects + w₃×comments) / (time_hours + 2)^1.5
时间衰减因子让新内容有机会出头
各作品热度分计算(w₁=1, w₂=2, w₃=3):
I2001: (1200 + 2×450 + 3×89) / (3+2)^1.5 = 2367/11.18 = 211.7
I2002: (3500 + 2×800 + 3×220) / (6+2)^1.5 = 5760/22.63 = 254.5
I2007: (8000 + 2×2000 + 3×500) / (48+2)^1.5 = 13500/353.6 = 38.2 ← 48h 前发的,时间衰减严重
I2009: (300 + 2×200 + 3×15) / (1+2)^1.5 = 745/5.20 = 143.3 ← 虽然绝对数值低,但 1h 内发的
I2012: (5000 + 2×1200 + 3×350) / (4+2)^1.5 = 8450/14.70 = 574.8 ← 热度最高!
热度排名:I2012(574.8) > I2002(254.5) > I2001(211.7) > I2009(143.3) > I2007(38.2)
I2001: (1200 + 2×450 + 3×89) / (3+2)^1.5 = 2367/11.18 = 211.7
I2002: (3500 + 2×800 + 3×220) / (6+2)^1.5 = 5760/22.63 = 254.5
I2007: (8000 + 2×2000 + 3×500) / (48+2)^1.5 = 13500/353.6 = 38.2 ← 48h 前发的,时间衰减严重
I2009: (300 + 2×200 + 3×15) / (1+2)^1.5 = 745/5.20 = 143.3 ← 虽然绝对数值低,但 1h 内发的
I2012: (5000 + 2×1200 + 3×350) / (4+2)^1.5 = 8450/14.70 = 574.8 ← 热度最高!
热度排名:I2012(574.8) > I2002(254.5) > I2001(211.7) > I2009(143.3) > I2007(38.2)
三、推荐系统核心架构:召回 → 粗排 → 精排 → 重排
为什么不能直接对所有物品打分?
假设 100 万个用户 × 100 万个作品,对每个(用户,作品)对做精排打分:
10¹² 次计算,每次 1ms → 总共 31.7 年!不可能实时完成。
所以必须分阶段:召回(快速筛选到千级) → 排序(精细打分)
假设 100 万个用户 × 100 万个作品,对每个(用户,作品)对做精排打分:
10¹² 次计算,每次 1ms → 总共 31.7 年!不可能实时完成。
所以必须分阶段:召回(快速筛选到千级) → 排序(精细打分)
四级漏斗架构:
全部作品池: ~1,000,000 篇
│
▼ 多路召回(< 10ms)
候选集: ~2,000 篇(各路合并去重)
│
▼ 粗排(简单模型,< 20ms)
粗排结果: ~500 篇
│
▼ 精排(LightGBM / 深度模型,< 50ms)
精排结果: ~50 篇
│
▼ 重排(业务规则,< 10ms)
最终展示: ~20 篇
总延迟: < 100ms
全部作品池: ~1,000,000 篇
│
▼ 多路召回(< 10ms)
候选集: ~2,000 篇(各路合并去重)
│
▼ 粗排(简单模型,< 20ms)
粗排结果: ~500 篇
│
▼ 精排(LightGBM / 深度模型,< 50ms)
精排结果: ~50 篇
│
▼ 重排(业务规则,< 10ms)
最终展示: ~20 篇
总延迟: < 100ms
3.1 多路召回详解
以小美(U1001)打开首页为例,6 路并行召回:
① 标签匹配召回(200 篇)
SQL: WHERE tags 与用户标签有交集 ORDER BY Jaccard DESC LIMIT 200
→ I2001(0.40), I2008(0.40), I2004(0.17), I2009(0.17), I2006(0.17)
② 协同过滤召回(200 篇)
找与小美相似的用户(U1004)喜欢但小美没看过的 → I2012
矩阵分解预测分 top → I2008(3.8), I2003(2.1)
③ 关注作者召回(50 篇)
小美关注了"吃货达人" → I2001, I2008
小美关注了"时尚编辑" → I2004
④ 热度召回(100 篇)
全站热度 top → I2012(574.8), I2002(254.5), I2001(211.7)
⑤ 好友动态召回(50 篇)
小美好友 U1004 最近互动了 → I2012, I2004
⑥ 地域召回(50 篇)
小美在成都 → I2001(成都火锅) 加权
合并去重:[I2001, I2002, I2003, I2004, I2005, I2006, I2007, I2008, I2009, I2010, I2011, I2012]
① 标签匹配召回(200 篇)
SQL: WHERE tags 与用户标签有交集 ORDER BY Jaccard DESC LIMIT 200
→ I2001(0.40), I2008(0.40), I2004(0.17), I2009(0.17), I2006(0.17)
② 协同过滤召回(200 篇)
找与小美相似的用户(U1004)喜欢但小美没看过的 → I2012
矩阵分解预测分 top → I2008(3.8), I2003(2.1)
③ 关注作者召回(50 篇)
小美关注了"吃货达人" → I2001, I2008
小美关注了"时尚编辑" → I2004
④ 热度召回(100 篇)
全站热度 top → I2012(574.8), I2002(254.5), I2001(211.7)
⑤ 好友动态召回(50 篇)
小美好友 U1004 最近互动了 → I2012, I2004
⑥ 地域召回(50 篇)
小美在成都 → I2001(成都火锅) 加权
合并去重:[I2001, I2002, I2003, I2004, I2005, I2006, I2007, I2008, I2009, I2010, I2011, I2012]
3.2 精排:LightGBM 打分模型
精排模型的任务:预测用户点击每个候选作品的概率 P(click)
输入特征(4 类):
用户特征:年龄, 性别, 城市, 注册天数, 活跃度
物品特征:类别, 点赞数, 收藏数, 评论数, 发布时长, 热度分
交叉特征:标签Jaccard匹配度, 地域是否匹配, 是否关注作者, 协同过滤预测分
上下文特征:当前时间(早/中/晚), 请求场景(首页/搜索/相关)
输入特征(4 类):
用户特征:年龄, 性别, 城市, 注册天数, 活跃度
物品特征:类别, 点赞数, 收藏数, 评论数, 发布时长, 热度分
交叉特征:标签Jaccard匹配度, 地域是否匹配, 是否关注作者, 协同过滤预测分
上下文特征:当前时间(早/中/晚), 请求场景(首页/搜索/相关)
小美的精排打分过程(特征 → 预测):
| 候选作品 | 标签匹配 | 地域匹配 | 热度(归一化) | 时效性 | 关注作者 | 协同过滤分 | → P(click) |
|---|---|---|---|---|---|---|---|
| I2001 成都火锅 | 0.40 | 1.0 | 0.37 | 0.95 | 1 | 0.60 | 0.89 |
| I2008 广州早茶 | 0.40 | 0.0 | 0.22 | 0.92 | 1 | 0.55 | 0.72 |
| I2009 显瘦穿搭 | 0.17 | 0.0 | 0.25 | 1.00 | 0 | 0.40 | 0.65 |
| I2004 秋装外套 | 0.17 | 0.0 | 0.18 | 0.98 | 1 | 0.50 | 0.63 |
| I2006 重庆小面 | 0.17 | 0.0 | 0.15 | 0.90 | 0 | 0.45 | 0.51 |
| I2012 周杰伦 | 0.00 | 0.0 | 1.00 | 0.95 | 0 | 0.70 | 0.48 |
| I2003 三亚旅行 | 0.00 | 0.0 | 0.65 | 0.85 | 0 | 0.50 | 0.35 |
| I2002 iPhone | 0.00 | 0.0 | 0.44 | 0.90 | 0 | 0.05 | 0.12 |
| I2011 React编程 | 0.00 | 0.0 | 0.20 | 0.75 | 0 | 0.02 | 0.06 |
| I2010 野钓鲫鱼 | 0.00 | 0.0 | 0.10 | 0.85 | 0 | 0.01 | 0.04 |
3.3 重排:业务规则调整
精排结果 → 业务规则过滤和调整:
规则 1:去掉已看过的
小美已交互过 I2001, I2004, I2006, I2009 → 排除
规则 2:同类别打散(不超过连续 2 个)
I2008(美食) → I2012(娱乐) → I2003(旅行) → ...
规则 3:插入 10% 探索内容
在第 5 位插入一个小美从未接触过的类别 → I2005(健身)
最终推荐给小美的首页:
┌─────────────────────────────────────────┐
│ 1. 广州早茶必吃榜 (美食) ← 标签+关注 │
│ 2. 周杰伦演唱会现场 (娱乐) ← 好友+热门 │
│ 3. 三亚5天自由行攻略 (旅行) ← 协同过滤 │
│ 4. 重庆小面做法教程 (美食) ← 标签匹配 │
│ 5. 家庭健身30天计划 (健身) ← 探索发现 │
└─────────────────────────────────────────┘
规则 1:去掉已看过的
小美已交互过 I2001, I2004, I2006, I2009 → 排除
规则 2:同类别打散(不超过连续 2 个)
I2008(美食) → I2012(娱乐) → I2003(旅行) → ...
规则 3:插入 10% 探索内容
在第 5 位插入一个小美从未接触过的类别 → I2005(健身)
最终推荐给小美的首页:
┌─────────────────────────────────────────┐
│ 1. 广州早茶必吃榜 (美食) ← 标签+关注 │
│ 2. 周杰伦演唱会现场 (娱乐) ← 好友+热门 │
│ 3. 三亚5天自由行攻略 (旅行) ← 协同过滤 │
│ 4. 重庆小面做法教程 (美食) ← 标签匹配 │
│ 5. 家庭健身30天计划 (健身) ← 探索发现 │
└─────────────────────────────────────────┘
同一时刻老王(U1002)的首页完全不同:
1. React vs Vue 2026对比 (编程) ← 关注作者
2. 2026年最值得买的手机 (科技) ← 标签匹配
3. iPhone17全面评测 (科技) ← 标签+关注
4. 成都Top10火锅测评 (美食) ← 探索发现
5. 三亚5天自由行攻略 (旅行) ← 全站热门
相同的系统、相同的作品池,因为用户特征不同而输出完全不同的推荐
1. React vs Vue 2026对比 (编程) ← 关注作者
2. 2026年最值得买的手机 (科技) ← 标签匹配
3. iPhone17全面评测 (科技) ← 标签+关注
4. 成都Top10火锅测评 (美食) ← 探索发现
5. 三亚5天自由行攻略 (旅行) ← 全站热门
相同的系统、相同的作品池,因为用户特征不同而输出完全不同的推荐
四、更多推荐场景
4.1 相关推荐(看完一篇后)
小美看完 I2001(成都火锅) 后,"猜你喜欢":
① 同作者作品:I2001 作者=吃货达人 → I2008(广州早茶)
② Item-to-Item 内容相似度(Jaccard):
I2001 标签={美食,火锅,成都,探店}
I2008 标签={美食,早茶,广州,探店} → J=|{美食,探店}|/|{美食,火锅,成都,探店,早茶,广州}|=2/6=0.33
I2006 标签={美食,做饭,重庆,面食} → J=|{美食}|/7=0.14
③ 协同过滤:喜欢 I2001 的 U1005 还喜欢了 I2010(但钓鱼跟小美无关→低分)
最终:I2008(同作者+内容相似) → I2006(内容相似) → I2009(小美喜欢穿搭,跨类推荐)
① 同作者作品:I2001 作者=吃货达人 → I2008(广州早茶)
② Item-to-Item 内容相似度(Jaccard):
I2001 标签={美食,火锅,成都,探店}
I2008 标签={美食,早茶,广州,探店} → J=|{美食,探店}|/|{美食,火锅,成都,探店,早茶,广州}|=2/6=0.33
I2006 标签={美食,做饭,重庆,面食} → J=|{美食}|/7=0.14
③ 协同过滤:喜欢 I2001 的 U1005 还喜欢了 I2010(但钓鱼跟小美无关→低分)
最终:I2008(同作者+内容相似) → I2006(内容相似) → I2009(小美喜欢穿搭,跨类推荐)
4.2 搜索推荐(搜索结果个性化排序)
小美搜索"火锅":
全文检索命中 → I2001(成都火锅), I2050(北京火锅), I2051(自制火锅底料)
对小美的个性化排序(在检索结果上叠加推荐特征):
I2001: BM25=8.5, 地域匹配(成都)=1, 标签匹配=0.40 → 综合分 9.2 ← 排第一
I2051: BM25=7.2, 地域匹配=0, 小美收藏过做饭类=0.3 → 综合分 7.5
I2050: BM25=7.8, 地域匹配=0, 无其他加分 → 综合分 7.0
若老王搜同样的"火锅":
I2050(北京火锅): 地域匹配(北京)=1 → 排第一(对老王来说北京火锅更相关)
全文检索命中 → I2001(成都火锅), I2050(北京火锅), I2051(自制火锅底料)
对小美的个性化排序(在检索结果上叠加推荐特征):
I2001: BM25=8.5, 地域匹配(成都)=1, 标签匹配=0.40 → 综合分 9.2 ← 排第一
I2051: BM25=7.2, 地域匹配=0, 小美收藏过做饭类=0.3 → 综合分 7.5
I2050: BM25=7.8, 地域匹配=0, 无其他加分 → 综合分 7.0
若老王搜同样的"火锅":
I2050(北京火锅): 地域匹配(北京)=1 → 排第一(对老王来说北京火锅更相关)
4.3 新用户冷启动
新用户 U1006 刚注册,零历史行为:
阶段一:注册引导
弹出兴趣选择:"请选择至少 3 个感兴趣的话题"
U1006 选了:科技、健身、汽车
阶段二:初始推荐策略
40% → 选的标签类别热门(科技、健身、汽车 top 内容)
30% → 全站近 24h 热门
20% → 同城市+同年龄段的群体热门
10% → 随机探索
阶段三:快速学习(前 50 次交互)
U1006 点击了 I2002(iPhone) 停留 120s 点赞 → 确认喜欢科技
U1006 看到 I2012(周杰伦) 没点击 → 对娱乐不感兴趣
→ 第二次打开 App 推荐已经明显不同
→ 交互 50 次后基本和老用户推荐精度持平
阶段一:注册引导
弹出兴趣选择:"请选择至少 3 个感兴趣的话题"
U1006 选了:科技、健身、汽车
阶段二:初始推荐策略
40% → 选的标签类别热门(科技、健身、汽车 top 内容)
30% → 全站近 24h 热门
20% → 同城市+同年龄段的群体热门
10% → 随机探索
阶段三:快速学习(前 50 次交互)
U1006 点击了 I2002(iPhone) 停留 120s 点赞 → 确认喜欢科技
U1006 看到 I2012(周杰伦) 没点击 → 对娱乐不感兴趣
→ 第二次打开 App 推荐已经明显不同
→ 交互 50 次后基本和老用户推荐精度持平
4.4 负反馈处理
小美看到 I2005(健身) → 长按 → "不感兴趣"
选择原因→不同的处理策略:
"内容不感兴趣" → "健身"类别在小美的画像中降权 30%
"看过类似的了" → 仅去重这条,不影响类别权重
"内容质量差" → 全局降低该作品分数,影响所有用户
"不想看这个作者" → 屏蔽该作者的所有作品
长期效果:连续 3 次对健身类点"不感兴趣" → 该类别几乎不再推
恢复机制:若小美某天主动搜索"瑜伽" → 系统重新为健身类加回权重
选择原因→不同的处理策略:
"内容不感兴趣" → "健身"类别在小美的画像中降权 30%
"看过类似的了" → 仅去重这条,不影响类别权重
"内容质量差" → 全局降低该作品分数,影响所有用户
"不想看这个作者" → 屏蔽该作者的所有作品
长期效果:连续 3 次对健身类点"不感兴趣" → 该类别几乎不再推
恢复机制:若小美某天主动搜索"瑜伽" → 系统重新为健身类加回权重
五、PostgreSQL MVP 数据库设计
5.1 核心表结构
-- ========== 用户表 ==========
CREATE TABLE users (
user_id VARCHAR(20) PRIMARY KEY,
nickname VARCHAR(50) NOT NULL,
age SMALLINT,
gender VARCHAR(4), -- '男','女','未知'
city VARCHAR(50),
tags TEXT[], -- PostgreSQL 数组类型: {'美食','探店','穿搭'}
register_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ========== 作品表 ==========
CREATE TABLE items (
item_id VARCHAR(20) PRIMARY KEY,
title VARCHAR(200) NOT NULL,
category VARCHAR(50) NOT NULL,
author VARCHAR(100) NOT NULL,
tags TEXT[], -- {'美食','火锅','成都','探店'}
likes INT DEFAULT 0,
collects INT DEFAULT 0,
comments INT DEFAULT 0,
hot_score DOUBLE PRECISION DEFAULT 0, -- 热度分(定时计算)
published_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ========== 行为日志表 ==========
CREATE TABLE behavior_logs (
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(20) NOT NULL,
item_id VARCHAR(20) NOT NULL,
action VARCHAR(20) NOT NULL, -- 'expose','click','like','collect','comment','share','dislike'
duration_ms INT DEFAULT 0, -- 停留时长(毫秒)
context VARCHAR(20), -- 'feed','search','related','category'
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 按时间分区(大数据量时性能关键)
CREATE INDEX idx_behavior_user_time ON behavior_logs (user_id, created_at DESC);
CREATE INDEX idx_behavior_item ON behavior_logs (item_id);
-- ========== 用户-物品评分表(预计算,定时更新) ==========
CREATE TABLE user_item_scores (
user_id VARCHAR(20) NOT NULL,
item_id VARCHAR(20) NOT NULL,
score DOUBLE PRECISION NOT NULL, -- 隐式评分: click=1, like=2, collect=3, comment=4
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_id, item_id)
);
-- ========== 用户关系表 ==========
CREATE TABLE user_follows (
follower_id VARCHAR(20) NOT NULL,
followee_id VARCHAR(20) NOT NULL, -- followee 可以是用户也可以是作者名
follow_type VARCHAR(10) DEFAULT 'author', -- 'author','user','friend'
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (follower_id, followee_id)
);
-- ========== 协同过滤相似度表(定时计算) ==========
CREATE TABLE user_similarity (
user_a VARCHAR(20) NOT NULL,
user_b VARCHAR(20) NOT NULL,
similarity DOUBLE PRECISION NOT NULL, -- 余弦相似度
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_a, user_b)
);
CREATE TABLE item_similarity (
item_a VARCHAR(20) NOT NULL,
item_b VARCHAR(20) NOT NULL,
similarity DOUBLE PRECISION NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (item_a, item_b)
);
-- ========== 矩阵分解隐因子表(模型训练后写入) ==========
CREATE TABLE user_factors (
user_id VARCHAR(20) PRIMARY KEY,
factors DOUBLE PRECISION[] NOT NULL, -- k 维向量: {0.518, 0.332, ...}
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE item_factors (
item_id VARCHAR(20) PRIMARY KEY,
factors DOUBLE PRECISION[] NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ========== 负反馈表 ==========
CREATE TABLE user_dislikes (
user_id VARCHAR(20) NOT NULL,
item_id VARCHAR(20), -- NULL 表示屏蔽作者
author VARCHAR(100), -- 屏蔽的作者
reason VARCHAR(30), -- 'not_interested','seen_similar','low_quality','block_author'
category VARCHAR(50), -- 不感兴趣的类别
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_dislike_user ON user_dislikes (user_id);
-- ========== 曝光去重表(避免重复推荐) ==========
CREATE TABLE user_exposures (
user_id VARCHAR(20) NOT NULL,
item_id VARCHAR(20) NOT NULL,
exposed_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_exposure_user_time ON user_exposures (user_id, exposed_at DESC);
-- 定期清理 7 天前的曝光记录
-- DELETE FROM user_exposures WHERE exposed_at < NOW() - INTERVAL '7 days';
5.2 插入示例数据
-- 用户
INSERT INTO users (user_id, nickname, age, gender, city, tags) VALUES
('U1001', '小美', 24, '女', '成都', '{"美食","探店","穿搭"}'),
('U1002', '老王', 35, '男', '北京', '{"科技","数码","编程"}'),
('U1003', '阿杰', 28, '男', '广州', '{"健身","美食","旅行"}'),
('U1004', '小雪', 22, '女', '上海', '{"穿搭","美妆","追星"}'),
('U1005', '大叔', 42, '男', '深圳', '{"钓鱼","汽车","美食"}');
-- 作品
INSERT INTO items (item_id, title, category, author, tags, likes, collects, comments,
published_at) VALUES
('I2001', '成都Top10火锅测评', '美食', '吃货达人',
'{"美食","火锅","成都","探店"}', 1200, 450, 89, NOW() - INTERVAL '3 hours'),
('I2002', 'iPhone17全面评测', '科技', '数码博主',
'{"科技","数码","iPhone","评测"}', 3500, 800, 220, NOW() - INTERVAL '6 hours'),
('I2003', '三亚5天自由行攻略', '旅行', '旅行家',
'{"旅行","三亚","攻略","海边"}', 2800, 1500, 156, NOW() - INTERVAL '12 hours'),
('I2004', '今秋必买的5件外套', '穿搭', '时尚编辑',
'{"穿搭","秋装","外套","时尚"}', 800, 600, 45, NOW() - INTERVAL '2 hours'),
('I2005', '家庭健身30天计划', '健身', '健身教练',
'{"健身","减脂","家庭","计划"}', 1500, 900, 78, NOW() - INTERVAL '24 hours'),
('I2006', '重庆小面做法教程', '美食', '厨房达人',
'{"美食","做饭","重庆","面食"}', 600, 350, 30, NOW() - INTERVAL '8 hours'),
('I2007', '2026年最值得买的手机', '科技', '数码博主',
'{"科技","手机","排行","推荐"}', 8000, 2000, 500, NOW() - INTERVAL '48 hours'),
('I2008', '广州早茶必吃榜', '美食', '吃货达人',
'{"美食","早茶","广州","探店"}', 900, 400, 60, NOW() - INTERVAL '5 hours'),
('I2009', '秋季显瘦穿搭公式', '穿搭', '时尚博主',
'{"穿搭","显瘦","秋装","女装"}', 300, 200, 15, NOW() - INTERVAL '1 hour'),
('I2010', '野钓鲫鱼技巧分享', '钓鱼', '钓鱼老李',
'{"钓鱼","鲫鱼","野钓","户外"}', 400, 250, 35, NOW() - INTERVAL '10 hours'),
('I2011', 'React vs Vue 2026对比', '编程', '程序员',
'{"编程","前端","React","Vue"}', 1100, 700, 120, NOW() - INTERVAL '20 hours'),
('I2012', '周杰伦演唱会现场', '娱乐', '追星girls',
'{"娱乐","周杰伦","演唱会","追星"}', 5000, 1200, 350, NOW() - INTERVAL '4 hours');
-- 行为日志
INSERT INTO behavior_logs (user_id, item_id, action, duration_ms, context) VALUES
('U1001', 'I2001', 'like', 45000, 'feed'),
('U1001', 'I2001', 'collect', 45000, 'feed'),
('U1001', 'I2006', 'like', 30000, 'feed'),
('U1001', 'I2004', 'collect', 60000, 'feed'),
('U1001', 'I2009', 'click', 20000, 'feed'),
('U1002', 'I2002', 'like', 120000, 'feed'),
('U1002', 'I2002', 'collect', 120000, 'feed'),
('U1002', 'I2002', 'comment', 120000, 'feed'),
('U1002', 'I2007', 'like', 90000, 'feed'),
('U1002', 'I2011', 'collect', 80000, 'feed'),
('U1003', 'I2005', 'like', 50000, 'feed'),
('U1003', 'I2005', 'collect', 50000, 'feed'),
('U1003', 'I2008', 'like', 35000, 'feed'),
('U1003', 'I2003', 'collect', 55000, 'feed'),
('U1004', 'I2004', 'like', 40000, 'feed'),
('U1004', 'I2012', 'like', 90000, 'feed'),
('U1004', 'I2012', 'collect', 90000, 'feed'),
('U1004', 'I2012', 'comment', 90000, 'feed'),
('U1005', 'I2010', 'like', 60000, 'feed'),
('U1005', 'I2010', 'collect', 60000, 'feed'),
('U1005', 'I2001', 'like', 40000, 'feed');
-- 关注关系
INSERT INTO user_follows (follower_id, followee_id, follow_type) VALUES
('U1001', '吃货达人', 'author'),
('U1001', '时尚编辑', 'author'),
('U1002', '数码博主', 'author'),
('U1002', '程序员', 'author'),
('U1003', '健身教练', 'author'),
('U1003', '吃货达人', 'author'),
('U1004', '时尚博主', 'author'),
('U1004', '追星girls', 'author'),
('U1001', 'U1004', 'friend'),
('U1004', 'U1001', 'friend');
5.3 核心查询——各路召回的 SQL 实现
-- ========== 1. 标签匹配召回 ==========
-- 利用 PostgreSQL 数组交集 & 运算符
SELECT i.item_id, i.title, i.category,
ARRAY_LENGTH(i.tags & u.tags, 1) AS match_count,
-- Jaccard = 交集 / 并集
ARRAY_LENGTH(i.tags & u.tags, 1)::FLOAT
/ ARRAY_LENGTH(i.tags | u.tags, 1) AS jaccard
FROM items i
CROSS JOIN (SELECT tags FROM users WHERE user_id = 'U1001') u
WHERE i.tags && u.tags -- && = 有交集
AND i.item_id NOT IN (
SELECT item_id FROM user_exposures
WHERE user_id = 'U1001' AND exposed_at > NOW() - INTERVAL '24 hours'
)
ORDER BY jaccard DESC
LIMIT 200;
-- ========== 2. 协同过滤召回(User-Based) ==========
-- 找相似用户喜欢的、目标用户没看过的
SELECT DISTINCT bl.item_id, us.similarity
FROM user_similarity us
JOIN behavior_logs bl ON bl.user_id = us.user_b
AND bl.action IN ('like', 'collect', 'comment')
WHERE us.user_a = 'U1001'
AND bl.item_id NOT IN (
SELECT item_id FROM behavior_logs WHERE user_id = 'U1001'
)
ORDER BY us.similarity DESC
LIMIT 200;
-- ========== 3. 热度召回 ==========
-- 热度分定时计算好存在 items.hot_score 字段
SELECT item_id, title, hot_score
FROM items
WHERE published_at > NOW() - INTERVAL '72 hours'
AND item_id NOT IN (
SELECT item_id FROM user_exposures
WHERE user_id = 'U1001' AND exposed_at > NOW() - INTERVAL '24 hours'
)
ORDER BY hot_score DESC
LIMIT 100;
-- ========== 4. 关注作者召回 ==========
SELECT i.item_id, i.title, i.author
FROM items i
JOIN user_follows uf ON uf.followee_id = i.author
WHERE uf.follower_id = 'U1001'
AND uf.follow_type = 'author'
AND i.published_at > NOW() - INTERVAL '7 days'
ORDER BY i.published_at DESC
LIMIT 50;
-- ========== 5. 矩阵分解预测分(向量点积) ==========
-- PostgreSQL 中计算两个数组的点积
CREATE OR REPLACE FUNCTION dot_product(a DOUBLE PRECISION[], b DOUBLE PRECISION[])
RETURNS DOUBLE PRECISION AS $$
DECLARE
result DOUBLE PRECISION := 0;
BEGIN
FOR idx IN 1..ARRAY_LENGTH(a, 1) LOOP
result := result + a[idx] * b[idx];
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- 对候选集中的每个 item 计算预测评分
SELECT if2.item_id,
dot_product(uf.factors, if2.factors) AS predicted_score
FROM user_factors uf
CROSS JOIN item_factors if2
WHERE uf.user_id = 'U1001'
AND if2.item_id = ANY(ARRAY['I2003','I2005','I2008','I2012']) -- 候选 item 列表
ORDER BY predicted_score DESC;
-- ========== 6. 热度分定时更新(Job: 每 10 分钟) ==========
UPDATE items SET
hot_score = (likes + 2.0 * collects + 3.0 * comments)
/ POWER(EXTRACT(EPOCH FROM NOW() - published_at)/3600 + 2, 1.5),
updated_at = NOW()
WHERE published_at > NOW() - INTERVAL '7 days';
-- ========== 7. 隐式评分更新(Job: 每小时) ==========
INSERT INTO user_item_scores (user_id, item_id, score)
SELECT user_id, item_id,
SUM(CASE action
WHEN 'click' THEN 1
WHEN 'like' THEN 2
WHEN 'collect' THEN 3
WHEN 'comment' THEN 4
WHEN 'share' THEN 5
ELSE 0
END) AS score
FROM behavior_logs
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY user_id, item_id
ON CONFLICT (user_id, item_id)
DO UPDATE SET score = EXCLUDED.score, updated_at = NOW();
5.4 API 接口设计
推荐服务需要对外暴露的接口:
行为埋点 Body 示例:
{
"userId": "U1001",
"itemId": "I2008",
"action": "like",
"durationMs": 45000,
"context": "feed",
"timestamp": "2026-03-28T14:30:00Z"
}
| 接口 | 触发时机 | 说明 |
|---|---|---|
| GET /api/recommend/feed?userId=&page=&size= | 打开首页 / 下拉刷新 | 信息流推荐(最核心) |
| GET /api/recommend/related?userId=&itemId=&size= | 查看作品详情页 | 相关推荐 / "猜你喜欢" |
| GET /api/recommend/search?userId=&query=&page= | 搜索框提交 | 搜索结果个性化排序 |
| GET /api/recommend/category?userId=&category=&page= | 进入分类页 | 类别下个性化排序 |
| POST /api/recommend/feedback | 点击"不感兴趣" | 负反馈处理 |
| POST /api/behavior/log | 每次用户交互 | 行为埋点上报 |
{
"userId": "U1001",
"itemId": "I2008",
"action": "like",
"durationMs": 45000,
"context": "feed",
"timestamp": "2026-03-28T14:30:00Z"
}
六、数据流转全景
从用户操作到推荐结果的完整数据链路:
用户在 App 上的操作
│ 实时上报行为日志
▼
┌──────────────────┐ ┌──────────────────────────────┐
│ behavior_logs 表 │──→│ 定时特征计算任务 │
│ (PostgreSQL) │ │ │
│ 曝光/点击/点赞/ │ │ · 隐式评分更新(每小时) │
│ 收藏/评论/停留 │ │ · 热度分更新(每10分钟) │
└──────────────────┘ │ · 用户/物品相似度(每天) │
└──────────────┬───────────────┘
│ 特征数据
▼
┌──────────────────────────────┐
│ 模型训练(每天凌晨) │
│ │
│ 1. 矩阵分解 → user/item_factors│
│ 2. LightGBM 排序模型 │
│ 3. 回写 PostgreSQL │
└──────────────┬───────────────┘
│ 模型/因子
▼
┌──────────────────────────────┐
│ 在线推荐服务 │
│ │
│ 召回 → 粗排 → 精排 → 重排 │
│ 响应时间目标: < 100ms │
└──────────────────────────────┘
用户在 App 上的操作
│ 实时上报行为日志
▼
┌──────────────────┐ ┌──────────────────────────────┐
│ behavior_logs 表 │──→│ 定时特征计算任务 │
│ (PostgreSQL) │ │ │
│ 曝光/点击/点赞/ │ │ · 隐式评分更新(每小时) │
│ 收藏/评论/停留 │ │ · 热度分更新(每10分钟) │
└──────────────────┘ │ · 用户/物品相似度(每天) │
└──────────────┬───────────────┘
│ 特征数据
▼
┌──────────────────────────────┐
│ 模型训练(每天凌晨) │
│ │
│ 1. 矩阵分解 → user/item_factors│
│ 2. LightGBM 排序模型 │
│ 3. 回写 PostgreSQL │
└──────────────┬───────────────┘
│ 模型/因子
▼
┌──────────────────────────────┐
│ 在线推荐服务 │
│ │
│ 召回 → 粗排 → 精排 → 重排 │
│ 响应时间目标: < 100ms │
└──────────────────────────────┘
七、推荐系统评估指标
离线指标(模型训练时评估):
Precision@K = 推荐的 K 个中用户实际点击的比例
Recall@K = 用户实际点击的物品被推荐出来的比例
NDCG@K = 考虑排序位置的增益(排在前面加分更多)
AUC = 正样本排在负样本前面的概率
在线指标(A/B 测试时观察):
CTR = 点击次数 / 曝光次数(核心指标)
人均使用时长(ultimate 指标)
点赞率、收藏率、评论率(互动深度)
内容多样性(覆盖的类别数 / 总类别数)
Precision@K = 推荐的 K 个中用户实际点击的比例
Recall@K = 用户实际点击的物品被推荐出来的比例
NDCG@K = 考虑排序位置的增益(排在前面加分更多)
AUC = 正样本排在负样本前面的概率
在线指标(A/B 测试时观察):
CTR = 点击次数 / 曝光次数(核心指标)
人均使用时长(ultimate 指标)
点赞率、收藏率、评论率(互动深度)
内容多样性(覆盖的类别数 / 总类别数)
NDCG@5 计算示例:
推荐 5 个作品给小美,实际她点击了位置 1, 3, 5 的作品
DCG@5 = rel₁/log₂(2) + rel₂/log₂(3) + rel₃/log₂(4) + rel₄/log₂(5) + rel₅/log₂(6)
= 1/1 + 0/1.585 + 1/2 + 0/2.322 + 1/2.585
= 1 + 0 + 0.5 + 0 + 0.387 = 1.887
理想排序 IDCG@5(把 3 个正样本排最前)= 1/1 + 1/1.585 + 1/2 + 0 + 0
= 1 + 0.631 + 0.5 = 2.131
NDCG@5 = DCG/IDCG = 1.887/2.131 = 0.886
(0.886 > 0.8 算不错的推荐质量)
推荐 5 个作品给小美,实际她点击了位置 1, 3, 5 的作品
DCG@5 = rel₁/log₂(2) + rel₂/log₂(3) + rel₃/log₂(4) + rel₄/log₂(5) + rel₅/log₂(6)
= 1/1 + 0/1.585 + 1/2 + 0/2.322 + 1/2.585
= 1 + 0 + 0.5 + 0 + 0.387 = 1.887
理想排序 IDCG@5(把 3 个正样本排最前)= 1/1 + 1/1.585 + 1/2 + 0 + 0
= 1 + 0.631 + 0.5 = 2.131
NDCG@5 = DCG/IDCG = 1.887/2.131 = 0.886
(0.886 > 0.8 算不错的推荐质量)