背景:推荐系统的尴尬
不知道大家有没有遇到过这种情况:刷首页推荐,发现推过来的话题点进去一看,"诶,这个我昨天看过了啊!"
这就是推荐系统的一个常见问题——重复推荐。用户已经浏览过的内容,算法不知情,还当成新鲜内容推过来,体验确实不太好。
最近抽空给智柴加了个小功能来解决这个问题:在 Redis 里记录用户最近浏览的 Topic,推荐算法在排序前先过滤掉这些内容。
设计思路:简单够用
核心需求
- 快速记录:用户浏览话题时实时记录,不能拖慢页面加载
- 快速查询:推荐计算时需要获取,不能成为瓶颈
- 自动清理:不需要永久保存,过期自动删
- 不丢核心交互:用户发帖/回复的权重比浏览高,要区分开
技术选型
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| SQLite 表 | 持久化 | 写操作频繁,性能差 | ❌ 太重 |
| Redis String (JSON) | 简单 | 追加麻烦,需要读出再写入 | ❌ 不是原子操作 |
| Redis List | LPUSH/LTRIM 原子操作,天然有序 | 需要去重逻辑 | ✅ 最合适 |
| Redis SortedSet | 自带时间戳,去重 | 内存占用高,查询稍慢 | ❌ 过度设计 |
最终选择 Redis List,满足需求且轻量。
实现细节
1. 存储结构
# 用户最近浏览的话题
Key: user:views:topic:{userId}
Type: List
Value: [topicId1, topicId2, ...] (最新的在前面)
Max Length: 10
TTL: 7天
# 示例
LPUSH user:views:topic:123 456
LTRIM user:views:topic:123 0 9
EXPIRE user:views:topic:123 604800
为什么只存 10 条?
- 推荐算法一次最多返回 20 条
- 加上用户已交互的内容(发帖/回复),通常能排除 15-20 条
- 存太多没必要,还占内存
2. 代码实现
class UserViewHistoryService
{
private const MAX_HISTORY_SIZE = 10;
private const TTL_SECONDS = 604800; // 7 天
public function recordTopicView(int {{LATEX:0}}topicId): bool
{
{{LATEX:1}}userId}";
// LPUSH 添加到头部(最新的在前面)
\(this->redis->lpush(\)key, \(topicId);
// LTRIM 裁剪,只保留前 10 个\)this->redis->ltrim(\(key, 0, self::MAX_HISTORY_SIZE - 1);
// 设置 7 天过期\)this->redis->expire(\(key, self::TTL_SECONDS);
}
public function getRecentTopicViews(int\)userId): array
{
\(key = "user:views:topic:{\)userId}";
{{LATEX:7}}this->redis->lrange({{LATEX:8}}items));
}
}
3. 集成到推荐流程
// 1. 用户浏览话题时记录
class TopicController
{
public function showTopic({{LATEX:9}}isLoggedIn) {
{{LATEX:10}}userId, {{LATEX:11}}pdo, {{LATEX:12}}targetInteractions = {{LATEX:13}}pdo, {{LATEX:14}}recentViews = {{LATEX:15}}userId);
// 合并排除列表
{{LATEX:16}}targetInteractions), // 自己发/回的
{{LATEX:17}}ranked = {{LATEX:18}}userId,
{{LATEX:19}}allInteractions,
{{LATEX:20}}exclude, // <-- 这里过滤
$limit
);
}
}
效果验证
加了日志之后,可以看到实际排除的数量:
[AsyncSQLiteWriter] handleUserRecommendations:
userId=123,
排除已交互=5, // 用户发帖/回复的话题
排除已浏览=8 // 用户浏览过的话题(新功能)
结果:
- 推荐结果中不会再出现用户最近浏览过的 10 个话题
- 如果用户交互少(只浏览不发帖),这个功能尤其有用
- 7 天自动过期,不会无限累积
为什么不记录全部浏览历史?
可能有同学会问:既然要做,为什么不记录 100 条、1000 条,甚至全部?
几个考虑:
-
内存成本:智柴用 Redis 做缓存,内存有限。1000 用户 × 100 条 × 8 字节 ≈ 800KB,看着不多,但累积起来要考虑。
-
推荐多样性:推荐算法一次只返回 10-20 条,过滤最近 10 条已经足够避免"重复感"。过滤太多反而可能导致推荐池变小,质量下降。
-
隐私边界:浏览历史是敏感数据,7 天自动过期是比较平衡的选择。
-
冷数据价值低:用户 3 个月前浏览的内容,对当前推荐的参考价值不大。
后续可以做的
-
Reply 浏览记录:目前只记录了 Topic,Reply 也可以记录,用于更精细的过滤。
-
负反馈机制:用户点击"不感兴趣",可以记录到另一个集合,长期排除。
-
时间衰减:最近 1 天浏览的权重高,6 天前的可以逐步降低排除优先级。
-
跨设备同步:如果以后有多设备场景,浏览历史可以同步到用户中心。
总结
这个小改动其实很简单,核心代码不到 200 行,但能实实在在提升用户体验。
技术选型上坚持简单够用的原则:
- 不引入新组件(用现有 Redis)
- 不修改数据库结构
- 异步处理,不影响主流程性能
推荐系统的优化往往是这种小细节堆起来的。如果你有其他关于推荐算法的想法,欢迎交流!
相关代码 PR: a67d53f
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!
推荐
智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。