1. 项目背景与核心需求
在信息爆炸的时代,图书推荐系统已经成为现代图书馆和在线书店的标配功能。传统的推荐方式往往基于简单的分类或热门排行,难以满足用户的个性化需求。这正是我们选择开发基于协同过滤算法的图书推荐系统的原因。
这个系统采用SpringBoot+Vue的前后端分离架构,主要解决三个核心问题:
- 如何准确捕捉用户的阅读兴趣偏好
- 如何实现"千人千面"的个性化推荐
- 如何构建可扩展的推荐服务架构
从技术选型来看,SpringBoot提供了稳定的后端服务能力,Vue则能构建流畅的前端交互体验。协同过滤算法作为推荐系统领域的经典算法,特别适合处理用户-物品评分矩阵,通过发现用户之间的相似性或物品之间的关联性来产生推荐。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 整体技术栈
系统采用典型的三层架构:
code复制前端展示层:Vue 3 + Element Plus + Axios
业务逻辑层:SpringBoot 2.7 + MyBatis Plus
数据存储层:MySQL 8.0 + Redis
算法层:基于用户的协同过滤算法
这种架构设计有以下几个优势:
- 前后端完全解耦,便于独立开发和部署
- 使用Redis缓存热门数据和推荐结果,减轻数据库压力
- MyBatis Plus简化了数据访问层的开发
- Vue 3的Composition API使前端代码更易维护
2.2 数据库设计
核心表结构设计如下:
用户表(users)
sql复制CREATE TABLE `users` (
`user_id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
图书表(books)
sql复制CREATE TABLE `books` (
`book_id` int NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL,
`author` varchar(100) NOT NULL,
`publisher` varchar(100) DEFAULT NULL,
`publish_date` date DEFAULT NULL,
`isbn` varchar(20) DEFAULT NULL,
`cover_url` varchar(255) DEFAULT NULL,
`category_id` int DEFAULT NULL,
`description` text,
PRIMARY KEY (`book_id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
用户评分表(ratings)
sql复制CREATE TABLE `ratings` (
`rating_id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`book_id` int NOT NULL,
`score` decimal(3,1) NOT NULL COMMENT '评分1-5分',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`rating_id`),
UNIQUE KEY `idx_user_book` (`user_id`,`book_id`),
KEY `idx_book` (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
提示:评分表设计了联合唯一索引,确保每个用户对每本书只能评分一次,这对推荐算法的准确性至关重要。
3. 协同过滤算法实现
3.1 算法原理
协同过滤算法主要分为两类:
- 基于用户的协同过滤(UserCF):找到与目标用户兴趣相似的用户群体,推荐这个群体喜欢的物品
- 基于物品的协同过滤(ItemCF):计算物品之间的相似度,推荐与用户历史喜欢物品相似的物品
本项目采用基于用户的协同过滤,核心公式如下:
用户相似度计算(余弦相似度):
code复制sim(u,v) = ∑(r_ui * r_vi) / (√∑r_ui² * √∑r_vi²)
预测评分:
code复制pred(u,i) = r̄_u + [∑sim(u,v)(r_vi - r̄_v)] / ∑|sim(u,v)|
其中:
- u,v表示用户
- i表示物品
- r_ui表示用户u对物品i的评分
- r̄_u表示用户u的平均评分
3.2 Java实现代码
java复制public class UserCFRecommender {
// 用户相似度矩阵
private Map<Integer, Map<Integer, Double>> userSimilarities;
// 用户-物品评分矩阵
private Map<Integer, Map<Integer, Double>> userItemMatrix;
public UserCFRecommender(List<Rating> ratings) {
this.userItemMatrix = buildUserItemMatrix(ratings);
this.userSimilarities = calculateUserSimilarities();
}
// 构建用户-物品评分矩阵
private Map<Integer, Map<Integer, Double>> buildUserItemMatrix(List<Rating> ratings) {
Map<Integer, Map<Integer, Double>> matrix = new HashMap<>();
for (Rating rating : ratings) {
matrix.computeIfAbsent(rating.getUserId(), k -> new HashMap<>())
.put(rating.getBookId(), rating.getScore());
}
return matrix;
}
// 计算用户相似度
private Map<Integer, Map<Integer, Double>> calculateUserSimilarities() {
Map<Integer, Map<Integer, Double>> similarities = new HashMap<>();
List<Integer> users = new ArrayList<>(userItemMatrix.keySet());
for (int i = 0; i < users.size(); i++) {
int u = users.get(i);
similarities.put(u, new HashMap<>());
for (int j = i + 1; j < users.size(); j++) {
int v = users.get(j);
double sim = cosineSimilarity(u, v);
if (sim > 0) {
similarities.get(u).put(v, sim);
similarities.computeIfAbsent(v, k -> new HashMap<>()).put(u, sim);
}
}
}
return similarities;
}
// 余弦相似度计算
private double cosineSimilarity(int u, int v) {
Map<Integer, Double> uRatings = userItemMatrix.get(u);
Map<Integer, Double> vRatings = userItemMatrix.get(v);
Set<Integer> commonItems = new HashSet<>(uRatings.keySet());
commonItems.retainAll(vRatings.keySet());
if (commonItems.isEmpty()) return 0;
double dotProduct = 0, normU = 0, normV = 0;
for (int item : commonItems) {
double uScore = uRatings.get(item);
double vScore = vRatings.get(item);
dotProduct += uScore * vScore;
normU += uScore * uScore;
normV += vScore * vScore;
}
return dotProduct / (Math.sqrt(normU) * Math.sqrt(normV));
}
// 预测用户对物品的评分
public double predictRating(int userId, int itemId) {
if (!userItemMatrix.containsKey(userId)) {
return 0;
}
double sumSim = 0, weightedSum = 0;
double userAvg = userItemMatrix.get(userId).values().stream()
.mapToDouble(Double::doubleValue).average().orElse(0);
for (Map.Entry<Integer, Double> entry : userSimilarities.getOrDefault(userId, new HashMap<>()).entrySet()) {
int similarUser = entry.getKey();
double similarity = entry.getValue();
if (userItemMatrix.get(similarUser).containsKey(itemId)) {
double similarUserAvg = userItemMatrix.get(similarUser).values().stream()
.mapToDouble(Double::doubleValue).average().orElse(0);
double rating = userItemMatrix.get(similarUser).get(itemId);
weightedSum += similarity * (rating - similarUserAvg);
sumSim += Math.abs(similarity);
}
}
if (sumSim == 0) return userAvg;
return userAvg + weightedSum / sumSim;
}
// 为指定用户生成推荐
public List<Integer> recommendItems(int userId, int topN) {
Set<Integer> ratedItems = userItemMatrix.getOrDefault(userId, new HashMap<>()).keySet();
Map<Integer, Double> itemScores = new HashMap<>();
for (Map.Entry<Integer, Double> entry : userSimilarities.getOrDefault(userId, new HashMap<>()).entrySet()) {
int similarUser = entry.getKey();
double similarity = entry.getValue();
for (Map.Entry<Integer, Double> ratingEntry : userItemMatrix.get(similarUser).entrySet()) {
int item = ratingEntry.getKey();
if (!ratedItems.contains(item)) {
double predicted = predictRating(userId, item);
itemScores.merge(item, predicted, Double::max);
}
}
}
return itemScores.entrySet().stream()
.sorted(Map.Entry.<Integer, Double>comparingByValue().reversed())
.limit(topN)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}
3.3 算法优化策略
在实际应用中,我们发现基础算法有几个性能瓶颈:
- 用户数量大时,相似度矩阵计算耗时
- 新用户冷启动问题
- 数据稀疏性问题
针对这些问题,我们实施了以下优化:
1. 相似度计算优化
java复制// 使用多线程加速相似度计算
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < users.size(); i++) {
final int u = users.get(i);
futures.add(executor.submit(() -> {
for (int j = i + 1; j < users.size(); j++) {
int v = users.get(j);
double sim = cosineSimilarity(u, v);
if (sim > 0) {
synchronized (similarities) {
similarities.get(u).put(v, sim);
similarities.computeIfAbsent(v, k -> new HashMap<>()).put(u, sim);
}
}
}
}));
}
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
2. 冷启动处理方案
- 对于新用户,采用基于内容的推荐作为补充
- 收集用户注册时的兴趣标签
- 结合热门图书和同标签图书进行混合推荐
3. 数据稀疏性处理
- 引入隐式反馈数据(浏览记录、收藏行为)
- 使用矩阵分解技术补充缺失值
- 设置最小共同评分阈值,过滤不可靠的相似度计算
4. 前后端实现细节
4.1 SpringBoot后端实现
推荐API接口设计
java复制@RestController
@RequestMapping("/api/recommend")
public class RecommendController {
@Autowired
private RecommendService recommendService;
@GetMapping("/forUser/{userId}")
public Result<List<Book>> recommendForUser(
@PathVariable Integer userId,
@RequestParam(defaultValue = "10") Integer size) {
List<Book> books = recommendService.recommendForUser(userId, size);
return Result.success(books);
}
@GetMapping("/similar/{bookId}")
public Result<List<Book>> recommendSimilar(
@PathVariable Integer bookId,
@RequestParam(defaultValue = "10") Integer size) {
List<Book> books = recommendService.recommendSimilar(bookId, size);
return Result.success(books);
}
}
定时更新推荐结果
java复制@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void refreshRecommendations() {
log.info("开始刷新推荐数据...");
long start = System.currentTimeMillis();
List<Integer> userIds = userMapper.selectAllUserIds();
userIds.parallelStream().forEach(userId -> {
List<Integer> bookIds = recommender.recommendItems(userId, 100);
String key = "recommend:user:" + userId;
redisTemplate.opsForValue().set(key, bookIds, 48, TimeUnit.HOURS);
});
log.info("推荐数据刷新完成,耗时{}ms", System.currentTimeMillis() - start);
}
4.2 Vue前端实现
推荐组件设计
vue复制<template>
<div class="recommend-container">
<h3>为您推荐</h3>
<div v-if="loading" class="loading">加载中...</div>
<div v-else class="book-list">
<div v-for="book in books" :key="book.id" class="book-item">
<img :src="book.coverUrl" :alt="book.title" @click="viewDetail(book.id)">
<div class="book-info">
<h4>{{ book.title }}</h4>
<p>{{ book.author }}</p>
<el-rate v-model="book.rating" disabled show-score></el-rate>
</div>
</div>
</div>
</div>
</template>
<script>
import { getRecommendations } from '@/api/recommend';
export default {
data() {
return {
books: [],
loading: false
};
},
mounted() {
this.loadRecommendations();
},
methods: {
async loadRecommendations() {
this.loading = true;
try {
const userId = this.$store.state.user.id;
const res = await getRecommendations(userId);
this.books = res.data;
} catch (error) {
console.error('获取推荐失败:', error);
} finally {
this.loading = false;
}
},
viewDetail(bookId) {
this.$router.push(`/book/${bookId}`);
}
}
};
</script>
<style scoped>
.recommend-container {
margin: 20px 0;
}
.book-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.book-item {
cursor: pointer;
transition: transform 0.3s;
}
.book-item:hover {
transform: translateY(-5px);
}
.book-item img {
width: 100%;
height: 250px;
object-fit: cover;
border-radius: 4px;
}
.book-info {
padding: 10px 0;
}
.loading {
text-align: center;
padding: 50px;
}
</style>
评分交互实现
vue复制<template>
<div class="rating-box">
<el-rate
v-model="rating"
:colors="colors"
@change="handleRateChange"
></el-rate>
<span v-if="hasRated" class="rated-tip">已评分</span>
</div>
</template>
<script>
import { rateBook } from '@/api/rating';
export default {
props: {
bookId: {
type: Number,
required: true
},
initialRating: {
type: Number,
default: 0
}
},
data() {
return {
rating: this.initialRating,
colors: ['#99A9BF', '#F7BA2A', '#FF9900'],
hasRated: this.initialRating > 0
};
},
methods: {
async handleRateChange(value) {
try {
await rateBook({
bookId: this.bookId,
score: value
});
this.hasRated = true;
this.$message.success('评分成功!');
this.$emit('rated', value);
} catch (error) {
console.error('评分失败:', error);
this.$message.error('评分失败,请重试');
this.rating = this.initialRating;
}
}
}
};
</script>
5. 系统部署与性能优化
5.1 部署架构
我们采用Docker容器化部署方案,架构如下:
code复制前端服务:Nginx容器(Vue打包后的静态文件)
后端服务:SpringBoot应用容器
数据库:MySQL容器(主从复制)
缓存:Redis集群
算法服务:独立容器(定期训练模型)
使用docker-compose编排:
yaml复制version: '3.8'
services:
frontend:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./dist:/usr/share/nginx/html
restart: always
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/book_recommend
- REDIS_HOST=redis
depends_on:
- mysql
- redis
restart: always
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_DATABASE=book_recommend
- MYSQL_USER=appuser
- MYSQL_PASSWORD=apppass
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
restart: always
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: always
algorithm:
build: ./algorithm
volumes:
- ./data:/app/data
restart: always
volumes:
mysql_data:
redis_data:
5.2 性能优化措施
1. 推荐结果缓存
java复制@Cacheable(value = "recommendations", key = "'user:' + #userId")
public List<Book> recommendForUser(Integer userId, Integer size) {
// 从数据库获取用户评分数据
List<Rating> ratings = ratingMapper.selectByUserId(userId);
// 如果数据不足,使用混合推荐
if (ratings.size() < 5) {
return hybridRecommender.recommend(userId, size);
}
// 否则使用协同过滤
return userCFRecommender.recommendItems(userId, size).stream()
.map(bookMapper::selectById)
.filter(Objects::nonNull)
.limit(size)
.collect(Collectors.toList());
}
2. 数据库查询优化
- 为常用查询添加适当索引
- 使用MyBatis二级缓存
- 批量插入评分数据
3. 前端性能优化
- 图片懒加载
- 推荐列表分页加载
- 使用Web Worker处理大量数据计算
5.3 监控与日志
我们使用Spring Boot Actuator提供健康检查端点,并集成Prometheus和Grafana监控系统状态。关键指标包括:
- API响应时间
- 推荐算法执行时间
- 缓存命中率
- 系统负载
日志配置示例:
properties复制logging.level.root=INFO
logging.level.com.example.recommend=DEBUG
logging.file.name=logs/app.log
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
6. 项目总结与改进方向
在实际开发过程中,我们积累了一些宝贵经验:
-
数据质量至关重要:初期由于评分数据稀疏,推荐效果不佳。后来通过引入隐式反馈和混合推荐策略,显著提升了推荐质量。
-
实时性权衡:完全实时的推荐计算成本高,最终采用"离线计算+实时修正"的折中方案,每天全量更新一次推荐结果,用户新评分通过实时修正少量更新。
-
多样性问题:单纯的协同过滤容易导致推荐结果过于集中,后来加入了随机性和新颖性因子,确保推荐列表有一定多样性。
未来可能的改进方向:
- 引入深度学习模型增强推荐效果
- 增加社交网络因素,考虑好友推荐
- 实现多模态推荐(结合图书封面、内容摘要等)
- 开发移动端应用,获取更多用户行为数据
这个项目让我深刻体会到,一个好的推荐系统不仅需要强大的算法支撑,更需要考虑工程实现、性能优化和用户体验等多方面因素。在实际操作中,算法效果和系统性能往往需要权衡,找到最适合业务场景的平衡点才是关键。
