去年在开发一个旅游推荐小程序时,我发现传统的关键词搜索和分类浏览已经无法满足用户需求。用户经常陷入"信息过载"的困境——面对数百家酒店和景点,选择困难症愈发严重。这正是我们决定引入协同过滤算法的初衷。
这个系统本质上是一个智能化的旅游服务平台,核心功能包括:
技术栈选择上,我们采用了微信小程序作为前端载体,后端使用Spring Boot构建微服务,数据库采用MySQL+Redis的组合。这种架构既保证了系统的响应速度,又能处理大规模的用户数据。
提示:在实际开发中,推荐系统的冷启动问题是个常见挑战。我们通过引入热门推荐和标签匹配作为初始策略,等积累足够用户数据后再切换到协同过滤算法。
协同过滤算法主要分为两类:
我们最终选择了混合策略,原因如下:
| 算法类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 用户协同 | 发现潜在兴趣 | 用户矩阵稀疏 | 用户群体稳定 |
| 物品协同 | 推荐结果稳定 | 难以发现新兴趣 | 物品数量稳定 |
具体实现时,我们使用Pearson相关系数计算相似度:
java复制public double pearsonSimilarity(Map<String, Double> user1,
Map<String, Double> user2) {
// 获取共同评分项
Set<String> commonItems = new HashSet<>(user1.keySet());
commonItems.retainAll(user2.keySet());
// 计算相似度
double sum1 = 0, sum2 = 0, sum1Sq = 0, sum2Sq = 0, pSum = 0;
for (String item : commonItems) {
double rating1 = user1.get(item);
double rating2 = user2.get(item);
sum1 += rating1;
sum2 += rating2;
sum1Sq += Math.pow(rating1, 2);
sum2Sq += Math.pow(rating2, 2);
pSum += rating1 * rating2;
}
double num = commonItems.size();
double numerator = pSum - (sum1 * sum2 / num);
double denominator = Math.sqrt((sum1Sq - Math.pow(sum1, 2)/num) *
(sum2Sq - Math.pow(sum2, 2)/num));
return denominator == 0 ? 0 : numerator / denominator;
}
整个系统采用分层架构:
code复制客户端层(微信小程序)
↓
API网关(Spring Cloud Gateway)
↓
微服务层(Spring Boot)
├── 推荐服务
├── 订单服务
├── 核销服务
└── 用户服务
↓
数据层
├── MySQL(事务数据)
├── Redis(缓存)
└── MongoDB(用户行为日志)
这种架构的优势在于:
数据收集阶段:
相似度计算:
实时推荐:
java复制public List<String> recommendItems(String userId, int numRecommendations) {
// 获取相似用户
Map<String, Double> similarUsers = findSimilarUsers(userId);
// 计算推荐得分
Map<String, Double> recommendations = new HashMap<>();
for (Map.Entry<String, Double> entry : similarUsers.entrySet()) {
String similarUser = entry.getKey();
double similarity = entry.getValue();
for (Map.Entry<String, Double> rating : userRatings.get(similarUser).entrySet()) {
String item = rating.getKey();
double score = rating.getValue();
if (!userRatings.get(userId).containsKey(item)) {
recommendations.merge(item, similarity * score, Double::sum);
}
}
}
// 排序并返回TopN
return recommendations.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(numRecommendations)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
核销是连接线上线下的关键环节,我们设计了双重验证机制:
二维码生成规则:
核销流程:
mermaid复制sequenceDiagram
用户->>小程序: 展示核销二维码
商家->>核销终端: 扫描二维码
核销终端->>服务器: 验证请求
服务器-->>核销终端: 返回验证结果
核销终端->>商家: 显示核销成功
服务器->>数据库: 更新订单状态
服务器->>推荐系统: 反馈消费数据
注意:在实际部署中发现,网络延迟可能导致核销超时。我们最终加入了本地缓存机制,在网络异常时允许离线核销,待网络恢复后再同步数据。
初期方案是每天离线计算推荐结果,但用户反馈推荐不够及时。我们改进为:
java复制// 实时推荐混合策略
public List<Recommendation> getRecommendations(String userId) {
// 实时行为
List<RecentBehavior> behaviors = behaviorService.getRecent(userId);
// 基于实时行为的临时推荐
List<Recommendation> realtimeRecs = realtimeEngine.process(behaviors);
// 离线推荐结果
List<Recommendation> offlineRecs = offlineService.getForUser(userId);
// 热门补充
List<Recommendation> hotRecs = hotService.getTop(10);
// 混合排序
return hybridStrategy.merge(realtimeRecs, offlineRecs, hotRecs);
}
随着用户量增长,我们遇到了几个典型问题:
用户行为表过大:
推荐计算耗时:
高并发下的订单创建:
初期新用户得不到好的推荐,我们通过以下方法解决:
用户-物品矩阵非常稀疏(填充率<5%),导致推荐质量不高。我们尝试了:
最终方案是采用LightFM混合模型,结合显式反馈和隐式反馈。
为了评估推荐效果,我们设计了以下指标:
| 指标类型 | 具体指标 | 测量方法 |
|---|---|---|
| 点击率 | CTR | 推荐曝光点击比 |
| 转化率 | CVR | 推荐点击到下单比 |
| 多样性 | 推荐熵 | 推荐结果分布 |
| 新颖性 | 新物品占比 | 推荐中新物品比例 |
测试结果显示,混合策略比纯协同过滤在CTR上提升了32%,同时保持了良好的多样性。
使用Docker Compose编排服务:
yaml复制version: '3'
services:
recommender:
image: travel-recommender:1.2
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=secret
volumes:
redis_data:
mysql_data:
使用Prometheus+Grafana监控关键指标:
推荐服务:
订单服务:
核销服务:
目前系统已经稳定运行1年多,后续计划从以下几个方向改进:
引入深度学习模型:
增强解释性:
场景化推荐:
这个项目给我的最大启示是:推荐系统不是一蹴而就的,需要持续迭代和优化。从最初的简单协同过滤到现在混合推荐策略,我们经历了数十次AB测试和算法调整。建议刚开始做推荐系统的同学,不要追求完美的初始方案,而是先搭建可运行的最小版本,然后通过数据驱动不断优化。