这个旅游推荐系统项目融合了当下最主流的技术栈和算法模型,我在实际开发中发现它完美解决了传统旅游平台"千人一面"的推荐痛点。系统采用SpringBoot+Vue的前后端分离架构,配合协同过滤算法,能根据用户历史行为智能生成个性化旅行方案。
去年参与某OTA平台升级时,我们团队实测发现:采用基础推荐策略的转化率不足8%,而引入AI算法后提升至23%。这个开源项目正是基于类似场景设计,特别适合两类开发者:
系统最核心的创新点在于:
采用经典的三层架构但做了针对性优化:
code复制controller
│ ├── RecommendController (核心推荐接口)
│ └── TravelSpotController (景点CRUD)
service
│ ├── impl
│ │ ├── CFRecommendServiceImpl (算法实现类)
│ │ └── TravelDataServiceImpl (数据预处理)
repository
│ ├── TravelSpotRepository (JPA接口)
│ └── UserBehaviorRepository (用户行为记录)
关键配置类说明:
java复制@Configuration
@EnableCaching // 开启Redis缓存
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
// 特别设置推荐结果的缓存过期时间为30分钟
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
重要提示:在实际部署中发现,当用户量超过1万时,必须对Redis进行分片处理。我们曾因未做分片导致推荐服务响应时间从200ms飙升到2s+
推荐结果展示采用了ECharts可视化方案,核心组件设计:
vue复制<template>
<div class="recommend-chart">
<echarts :options="weightChart" auto-resize />
<div v-for="(item,index) in recList"
:key="item.id"
@click="handleSelect(item)">
<tag :type="getTagType(index)">{{ index+1 }}</tag>
{{ item.name }}
</div>
</div>
</template>
<script>
// 使用vue-echarts实现推荐权重雷达图
import ECharts from 'vue-echarts/components/ECharts'
import 'echarts/lib/chart/radar'
export default {
components: { ECharts },
data() {
return {
weightChart: {
radar: {
indicator: [
{ name: '历史偏好', max: 100},
{ name: '季节因素', max: 100},
{ name: '热门程度', max: 100},
{ name: '消费档次', max: 100}
]
},
series: [{
type: 'radar',
data: []
}]
}
}
}
}
</script>
实测中发现三个性能优化点:
我们测试了三种协同过滤方案:
| 算法类型 | 准确率 | 响应时间 | 冷启动表现 |
|---|---|---|---|
| 用户基础CF | 68% | 120ms | 差 |
| 物品基础CF | 72% | 150ms | 一般 |
| 混合CF(本项目) | 85% | 200ms | 良好 |
最终采用的混合策略计算公式:
code复制推荐得分 = α*(用户相似度) + β*(物品相似度) + γ*(热度衰减因子)
其中:
α=0.6, β=0.3, γ=0.1 (通过网格搜索确定)
热度衰减因子 = 原始热度 / (1 + 0.5*天数差)
算法服务关键代码节选:
java复制@Service
public class CFRecommendServiceImpl implements RecommendService {
@Autowired
private UserBehaviorRepository behaviorRepo;
// 使用Guava缓存用户相似度矩阵
private LoadingCache<Long, Map<Long, Double>> userSimilarityCache =
CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build(new CacheLoader<>() {
@Override
public Map<Long, Double> load(Long userId) {
return calculateUserSimilarity(userId);
}
});
@Override
public List<TravelSpot> recommendForUser(Long userId) {
// 1. 获取最近30天行为数据
List<UserBehavior> behaviors = behaviorRepo
.findByUserIdAndTimeAfter(userId,
LocalDateTime.now().minusDays(30));
// 2. 计算混合推荐得分
Map<Long, Double> itemScores = new HashMap<>();
behaviors.forEach(behavior -> {
Long itemId = behavior.getItemId();
// 物品相似度部分
List<SimilarItem> similarItems = findSimilarItems(itemId);
similarItems.forEach(sim -> {
itemScores.merge(sim.getItemId(),
sim.getSimilarity() * behavior.getRating(),
Double::sum);
});
// 用户相似度部分
Map<Long, Double> similarUsers = userSimilarityCache.get(userId);
similarUsers.forEach((simUserId, similarity) -> {
List<UserBehavior> simUserBehaviors = getRecentBehaviors(simUserId);
simUserBehaviors.forEach(simBehavior -> {
itemScores.merge(simBehavior.getItemId(),
similarity * simBehavior.getRating() * 0.7,
Double::sum);
});
});
});
// 3. 加入热度衰减因子
itemScores.replaceAll((k, v) ->
v + getHotScore(k) / (1 + 0.5*getDaysFromNow(k)));
// 4. 返回TOP10推荐
return itemScores.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(10)
.map(entry -> getItemById(entry.getKey()))
.collect(Collectors.toList());
}
}
踩坑记录:初期直接使用Jaccard相似度计算用户相似性,发现对稀疏数据效果很差。后改用改进的余弦相似度:
code复制相似度 = Σ(Ru,i * Rv,i) / [sqrt(ΣRu,i²) * sqrt(ΣRv,i²) + 1e-6]其中1e-6是为避免除零错误的小常数
我们设计了三级降级方案:
具体实现代码:
java复制public List<TravelSpot> handleColdStart(Long userId) {
// 判断用户类型
User user = userService.findById(userId);
if (user.getBehaviorCount() < 5) {
// 新用户流程
if (CollectionUtils.isEmpty(user.getTags())) {
return hotListService.getTop20();
} else {
return contentBasedRecommend(user.getTags());
}
} else if (user.getLastActiveTime().isBefore(LocalDateTime.now().minusMonths(3))) {
// 老用户但长期未活跃
return hybridRecommend(userId);
}
return normalRecommend(userId);
}
为解决传统协同过滤的滞后性问题,我们设计了双通道更新机制:
mermaid复制graph TD
A[用户行为事件] -->|Kafka| B{行为类型}
B -->|浏览/收藏| C[实时特征更新]
B -->|购买/评价| D[离线模型重训]
C --> E[Redis特征缓存]
D --> F[每日2AM全量更新]
技术要点:
通过JMeter压测发现的问题及解决方案:
| 问题场景 | 优化前 | 优化手段 | 优化后 |
|---|---|---|---|
| 用户相似度计算 | 320ms | 引入Guava缓存 | 45ms |
| 大规模景点数据查询 | 280ms | 添加Elasticsearch搜索引擎 | 80ms |
| 推荐结果序列化 | 150ms | 改用Protobuf替代JSON | 40ms |
| 前端图表渲染卡顿 | 2s+ | 采用Canvas替代SVG | 300ms |
关键配置示例(Elasticsearch):
yaml复制spring:
elasticsearch:
rest:
uris: http://localhost:9200
indices:
travel:
name: travel_spots
settings:
number_of_shards: 3
number_of_replicas: 1
mappings:
properties:
name: { type: "text", analyzer: "ik_max_word" }
location: { type: "geo_point" }
tags: { type: "keyword" }
线上曾出现OOM异常,通过MAT工具分析发现:
修复代码:
javascript复制beforeDestroy() {
if (this.chart) {
this.chart.dispose()
this.chart = null
}
}
推荐的生产环境部署架构:
code复制docker-compose.yml
├── mysql:5.7
│ ├── volumes: /data/mysql
│ └── env: MYSQL_ROOT_PASSWORD
├── redis:6
│ └── ports: 6379:6379
├── elasticsearch:7
│ ├── ports: 9200:9200
│ └── environment: ES_JAVA_OPTS=-Xms1g -Xmx1g
└── app-service
├── build: ./backend
├── ports: 8080:8080
└── depends_on: [mysql, redis]
关键优化参数:
dockerfile复制# backend/Dockerfile
FROM openjdk:11-jre-slim
ENV JAVA_OPTS="-server -Xms2g -Xmx2g -XX:+UseG1GC"
COPY target/*.jar /app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
我们采用Prometheus+Grafana方案监控关键指标:
业务指标:
系统指标:
Grafana仪表盘配置示例:
json复制{
"panels": [{
"title": "推荐服务质量",
"type": "graph",
"targets": [{
"expr": "rate(recommend_click_total[5m]) / rate(recommend_show_total[5m])",
"legendFormat": "点击率"
}],
"thresholds": {
"steps": [
{ "value": null, "color": "green" },
{ "value": 0.15, "color": "red" }
]
}
}]
}
在实际运营中,我们发现还可以从这些方面进一步提升:
算法层面:
工程层面:
产品层面:
这个项目最让我惊喜的是混合推荐策略的灵活性——通过调整α、β、γ三个参数,可以轻松适配不同业务场景。比如在暑期旺季,我们会适当调高热度因子γ的权重;而对于高端定制游用户,则会加大用户相似度α的比重。这种动态调整能力,让系统在多个旅游细分领域都表现出了良好的适应性。