协同过滤推荐的核心逻辑是“找得快”而非“算得准”,PHP 实现应优先选用 user-based 或 item-based 相似度计算,注重数据清洗、整型 ID 统一、缺失值正确标记、共现频次统计与 Redis 缓存优化,冷启动时 fallback 规则策略,负反馈需实时过滤。

协同过滤推荐的核心逻辑不是“算得准”,而是“找得快”
PHP 做协同过滤,别一上来就啃矩阵分解或 SVD——90% 的业务场景,用基于用户的 user-based 或基于物品的 item-based 相似度计算就足够了。关键不在算法多高级,而在怎么把稀疏的用户行为数据(比如点击、收藏、评分)快速映射成可比的相似关系。
真实项目里最常卡住的不是公式写不对,是数据没规整好:用户 ID 是字符串还是整数?物品 ID 有没有重复?评分字段是不是空值或 0?这些不提前清洗,array_diff_key() 和 array_intersect_key() 一跑就漏数据。
- 必须统一用整型 ID 表示用户和物品,避免字符串哈希不一致导致相似度为 0
- 缺失评分不能填 0,得用
null或单独标记,否则会错误拉低相似度 - 用户行为记录建议先按
user_id+item_id去重,防止同一人反复点击同个商品扭曲偏好
用 PHP 原生数组实现 item-based 相似度计算
不用装 ML 库,array_map()、array_reduce() 和基础数学函数就能跑通核心流程。重点是别直接算余弦相似度——PHP 没内置向量运算,容易在 sqrt() 和 pow() 里掉精度或报 NaN。
更稳的做法是先统计共现频次:A 和 B 同时被多少用户交互过,再用 Jaccard 或改进的 adjusted cosine(减去用户均值)。这样全是整数运算,不依赖浮点精度,也方便加缓存。
立即学习“PHP免费学习笔记(深入)”;
- 共现矩阵用二维关联数组:
$cooccurrence[$item_a][$item_b] = $count,别用嵌套 for 遍历全量,用foreach ($user_actions as $uid => $items)两层循环构建 - 计算相似度前,先过滤掉交互次数
- 结果必须限制 Top-K(比如
array_slice($similar_items, 0, 10)),不然每次推荐都扫几百个物品,响应直接上 500ms
function calcItemSimilarity($item_a, $item_b, $cooccurrence, $item_popularity) {
$co = $cooccurrence[$item_a][$item_b] ?? 0;
$pop_a = $item_popularity[$item_a] ?? 1;
$pop_b = $item_popularity[$item_b] ?? 1;
return $co / sqrt($pop_a * $pop_b); // 简化版,避免开方溢出
}Redis 缓存协同结果比实时计算靠谱得多
phpredis 扩展不是锦上添花,是必须项。用户请求推荐时,如果每秒要算 20 个用户的相似物品,纯 PHP 数组遍历在 QPS > 50 就开始抖动。而 hGetAll() 读一个用户的历史行为,zRangeByScore() 拿预计算好的相似物品,毫秒级返回。
缓存结构设计比算法本身影响更大:别存“用户 → 推荐列表”,改存“物品 → 相似物品集合”,然后运行时拼接。这样更新成本低——新用户行为只影响几个物品的相似池,不用全量重算。
- 相似物品用有序集合
sim:item:123存,score 存相似度分,便于zRangeByScore取 Top-N - 用户最近行为用哈希
user:act:456存,字段是item_id,值是时间戳,方便按时间衰减加权 - 缓存失效用懒更新:读不到时触发异步任务补,别用定时全量刷,DB 压力扛不住
冷启动和实时性之间没有银弹,只有取舍
新用户没行为?别硬套协同过滤。PHP 层该直接 fallback 到规则策略:比如按类目热度 SELECT item_id FROM items WHERE cat_id = ? ORDER BY click_cnt DESC LIMIT 10;新物品没共现?就塞进“新品池”人工打标签,等有 5 个用户交互再进协同流程。
所谓“实时推荐”,PHP 能做到的极限是“T+1 分钟”——行为日志进 Kafka,消费者用 PHP 脚本每分钟聚合一次共现,更新 Redis。想 sub-second 响应?那得换 Go 或 Rust 做在线部分,PHP 守好离线特征生成和缓存兜底就行。
最容易被忽略的是负反馈处理:用户点了“不感兴趣”,这个信号必须立刻写入 user_dislike:789 集合,并在推荐时用 zDiff() 过滤。否则模型越训越偏,用户越推越烦。



















