技术架构设计

采用Spring Boot作为后端框架,MySQL存储用户数据和书籍信息,协同过滤算法实现个性化推荐。前端可通过Vue.js或React构建交互界面,后端通过RESTful API提供数据服务。

数据库设计

用户表(user)

CREATE TABLE user (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

书籍表(book)

CREATE TABLE book (
    book_id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(100),
    author VARCHAR(50),
    category VARCHAR(30),
    description TEXT
);

用户阅读历史表(user_reading_history)

CREATE TABLE user_reading_history (
    history_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    book_id INT,
    read_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES user(user_id),
    FOREIGN KEY (book_id) REFERENCES book(book_id)
);

协同过滤算法实现

基于用户的协同过滤(UserCF)计算相似度矩阵: $$ sim(u,v) = \frac{\sum_{i \in I_{uv}}(r_{ui} - \bar{r}u)(r{vi} - \bar{r}v)}{\sqrt{\sum{i \in I_{uv}}(r_{ui} - \bar{r}u)^2} \sqrt{\sum{i \in I_{uv}}(r_{vi} - \bar{r}_v)^2}} $$

Java核心代码片段

// 计算用户相似度矩阵
public Map<Integer, Map<Integer, Double>> calculateUserSimilarity() {
    List<User> users = userRepository.findAll();
    Map<Integer, Map<Integer, Double>> similarityMatrix = new HashMap<>();

    for (User u : users) {
        Map<Integer, Double> similarities = new HashMap<>();
        for (User v : users) {
            if (!u.equals(v)) {
                double sim = cosineSimilarity(u, v);
                similarities.put(v.getUserId(), sim);
            }
        }
        similarityMatrix.put(u.getUserId(), similarities);
    }
    return similarityMatrix;
}

private double cosineSimilarity(User u, User v) {
    Set<Integer> commonBooks = findCommonBooks(u, v);
    if (commonBooks.isEmpty()) return 0.0;

    double dotProduct = 0.0;
    double normU = 0.0;
    double normV = 0.0;

    for (Integer bookId : commonBooks) {
        double ratingU = getNormalizedRating(u, bookId);
        double ratingV = getNormalizedRating(v, bookId);
        dotProduct += ratingU * ratingV;
        normU += Math.pow(ratingU, 2);
        normV += Math.pow(ratingV, 2);
    }

    return dotProduct / (Math.sqrt(normU) * Math.sqrt(normV));
}

推荐逻辑实现

基于最近邻的推荐策略

public List<Book> recommendBooks(int userId, int topN) {
    Map<Integer, Double> userSimilarities = similarityMatrix.get(userId);
    List<Integer> nearestNeighbors = userSimilarities.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .limit(50)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());

    Set<Integer> alreadyRead = readingHistoryRepository.findByUserId(userId)
            .stream()
            .map(ReadingHistory::getBookId)
            .collect(Collectors.toSet());

    Map<Integer, Double> candidateBooks = new HashMap<>();
    for (Integer neighborId : nearestNeighbors) {
        List<ReadingHistory> neighborHistory = readingHistoryRepository.findByUserId(neighborId);
        for (ReadingHistory rh : neighborHistory) {
            if (!alreadyRead.contains(rh.getBookId())) {
                candidateBooks.merge(rh.getBookId(), 
                    userSimilarities.get(neighborId), 
                    (oldVal, newVal) -> oldVal + newVal);
            }
        }
    }

    return candidateBooks.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .limit(topN)
            .map(e -> bookRepository.findById(e.getKey()).orElse(null))
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
}

性能优化方案

建立阅读历史索引加速查询:

CREATE INDEX idx_user_book ON user_reading_history(user_id, book_id);

使用Redis缓存推荐结果:

@Cacheable(value = "recommendations", key = "#userId")
public List<Book> getCachedRecommendations(int userId) {
    return recommendBooks(userId, 10);
}

效果评估指标

采用准确率评估推荐质量: $$ Precision@k = \frac{| { \text{推荐书籍} } \cap { \text{实际喜欢书籍} } |}{k} $$

实现AB测试框架:

public class RecommendationEvaluator {
    public double calculatePrecision(int userId, List<Integer> recommended, List<Integer> actuallyLiked) {
        Set<Integer> recommendedSet = new HashSet<>(recommended);
        recommendedSet.retainAll(actuallyLiked);
        return (double) recommendedSet.size() / recommended.size();
    }
}

更多推荐