圖書推薦(協同過濾)算法的實現:基于訂單購買實現相似用戶的圖書推薦

代碼部分

package com.ruoyi.system.service.impl;import com.ruoyi.system.domain.Book;
import com.ruoyi.system.domain.MyOrder;
import com.ruoyi.system.mapper.BookMapper;
import com.ruoyi.system.mapper.MyOrderMapper;
import com.ruoyi.system.service.IBookRecommendService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;@Service
public class BookRecommendServiceImpl implements IBookRecommendService {private static final Logger log = LoggerFactory.getLogger(BookRecommendServiceImpl.class);@Autowiredprivate MyOrderMapper orderMapper;@Autowiredprivate BookMapper bookMapper;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private static final String USER_SIMILARITY_KEY = "recommend:user:similarity";private static final double SIMILARITY_THRESHOLD = 0.000001; // 相似度閾值/*** 應用啟動時初始化推薦數據*/@PostConstructpublic void init() {log.info("檢查推薦數據初始化狀態...");try {if(!hasRecommendationData()) {log.info("未檢測到推薦數據,開始初始化計算...");preComputeUserSimilarities();} else {log.info("推薦數據已存在,跳過初始化計算");}} catch (Exception e) {log.error("推薦數據初始化失敗", e);}}/*** 檢查是否存在推薦數據*/private boolean hasRecommendationData() {Set<String> keys = redisTemplate.keys(USER_SIMILARITY_KEY + ":*");return keys != null && !keys.isEmpty();}@Override@Transactional(readOnly = true)public List<Book> recommendBooksByUserCF(Long userId, int limit) {if (userId == null || limit <= 0) {return Collections.emptyList();}try {// 1. 從Redis獲取用戶相似度數據Map<Object, Object> similarityScoresObj = redisTemplate.opsForHash().entries(USER_SIMILARITY_KEY + ":" + userId);if (similarityScoresObj == null || similarityScoresObj.isEmpty()) {log.debug("用戶 {} 無相似用戶數據", userId);return Collections.emptyList();}// 2. 轉換數據類型Map<Long, Double> similarityScores = convertSimilarityMap(similarityScoresObj);// 3. 獲取最相似的N個用戶List<Long> similarUserIds = getTopSimilarUsers(similarityScores, 10);if (similarUserIds.isEmpty()) {return Collections.emptyList();}// 4. 獲取推薦圖書return generateRecommendations(userId, similarUserIds, limit);} catch (Exception e) {log.error("為用戶 {} 生成推薦時發生錯誤", userId, e);return Collections.emptyList();}}/*** 轉換相似度Map數據類型*/private Map<Long, Double> convertSimilarityMap(Map<Object, Object> rawMap) {return rawMap.entrySet().stream().collect(Collectors.toMap(e -> Long.parseLong(e.getKey().toString()),e -> Double.parseDouble(e.getValue().toString())));}/*** 獲取最相似的用戶ID列表*/private List<Long> getTopSimilarUsers(Map<Long, Double> similarityScores, int topN) {return similarityScores.entrySet().stream().filter(e -> e.getValue() >= SIMILARITY_THRESHOLD).sorted(Map.Entry.<Long, Double>comparingByValue().reversed()).limit(topN).map(Map.Entry::getKey).collect(Collectors.toList());}/*** 生成推薦圖書列表*/private List<Book> generateRecommendations(Long targetUserId, List<Long> similarUserIds, int limit) {// 1. 獲取相似用戶訂單List<MyOrder> similarUserOrders = orderMapper.selectCompletedOrdersByUserIds(similarUserIds);// 2. 獲取目標用戶已購圖書Set<Long> purchasedBooks = getPurchasedBooks(targetUserId);// 3. 計算圖書推薦分數Map<Long, Double> bookScores = calculateBookScores(similarUserOrders, purchasedBooks);// 4. 獲取推薦圖書return getTopRecommendedBooks(bookScores, limit);}/*** 獲取用戶已購圖書ID集合*/private Set<Long> getPurchasedBooks(Long userId) {List<MyOrder> orders = orderMapper.selectCompletedOrdersByUserId(userId);if (orders == null || orders.isEmpty()) {return Collections.emptySet();}return orders.stream().map(order -> order.getBookId()).collect(Collectors.toSet());}/*** 計算圖書推薦分數*/private Map<Long, Double> calculateBookScores(List<MyOrder> similarUserOrders, Set<Long> purchasedBooks) {Map<Long, Double> bookScores = new HashMap<>();for (MyOrder order : similarUserOrders) {Long bookId = order.getBookId();if (!purchasedBooks.contains(bookId)) {bookScores.merge(bookId, (double) order.getQuantity(), Double::sum);}}return bookScores;}/*** 獲取評分最高的推薦圖書*/private List<Book> getTopRecommendedBooks(Map<Long, Double> bookScores, int limit) {if (bookScores.isEmpty()) {return Collections.emptyList();}List<Long> recommendedBookIds = bookScores.entrySet().stream().sorted(Map.Entry.<Long, Double>comparingByValue().reversed()).limit(limit).map(Map.Entry::getKey).collect(Collectors.toList());return bookMapper.selectBookByIds(recommendedBookIds);}@Override@Transactionalpublic void preComputeUserSimilarities() {log.info("開始計算用戶相似度矩陣...");long startTime = System.currentTimeMillis();try {// 1. 清空舊數據clearExistingSimilarityData();// 2. 獲取所有用戶ID(有完成訂單的)List<Long> userIds = orderMapper.selectAllUserIdsWithCompletedOrders();log.info("找到{}個有訂單的用戶", userIds.size());if (userIds.isEmpty()) {log.warn("沒有找到任何用戶訂單數據!");return;}// 3. 構建用戶-圖書評分矩陣Map<Long, Map<Long, Integer>> ratingMatrix = buildRatingMatrix(userIds);// 4. 計算并存儲相似度computeAndStoreSimilarities(userIds, ratingMatrix);long duration = (System.currentTimeMillis() - startTime) / 1000;log.info("用戶相似度矩陣計算完成,耗時{}秒", duration);} catch (Exception e) {log.error("計算用戶相似度矩陣失敗", e);throw e;}}/*** 清空現有相似度數據*/private void clearExistingSimilarityData() {Set<String> keys = redisTemplate.keys(USER_SIMILARITY_KEY + ":*");if (keys != null && !keys.isEmpty()) {redisTemplate.delete(keys);log.info("已清除{}個舊的用戶相似度記錄", keys.size());}}/*** 構建用戶-圖書評分矩陣*/private Map<Long, Map<Long, Integer>> buildRatingMatrix(List<Long> userIds) {Map<Long, Map<Long, Integer>> ratingMatrix = new HashMap<>();for (Long userId : userIds) {List<MyOrder> orders = orderMapper.selectCompletedOrdersByUserId(userId);if (orders == null || orders.isEmpty()) {continue;}Map<Long, Integer> userRatings = new HashMap<>();for (MyOrder order : orders) {if (order == null || order.getBookId() == null) {continue;}Long bookId = order.getBookId();Integer quantity = Math.toIntExact(order.getQuantity() != null ? order.getQuantity() : 0);userRatings.merge(bookId, quantity, (oldVal, newVal) -> oldVal + newVal);}ratingMatrix.put(userId, userRatings);}return ratingMatrix;}/*** 計算并存儲用戶相似度*/private void computeAndStoreSimilarities(List<Long> userIds, Map<Long, Map<Long, Integer>> ratingMatrix) {int computedPairs = 0;for (int i = 0; i < userIds.size(); i++) {Long userId1 = userIds.get(i);Map<Long, Integer> ratings1 = ratingMatrix.get(userId1);Map<String, String> similarities = new HashMap<>();// 只計算后續用戶,避免重復計算for (int j = i + 1; j < userIds.size(); j++) {Long userId2 = userIds.get(j);Map<Long, Integer> ratings2 = ratingMatrix.get(userId2);double similarity = computeCosineSimilarity(ratings1, ratings2);if (similarity >= SIMILARITY_THRESHOLD) {similarities.put(userId2.toString(), String.valueOf(similarity));computedPairs++;}}if (!similarities.isEmpty()) {String key = USER_SIMILARITY_KEY + ":" + userId1;redisTemplate.opsForHash().putAll(key, similarities);redisTemplate.expire(key, 7, TimeUnit.DAYS);}// 定期打印進度if (i % 100 == 0 || i == userIds.size() - 1) {log.info("已處理 {}/{} 用戶", i + 1, userIds.size());}}log.info("共計算{}對用戶相似關系", computedPairs);}/*** 計算余弦相似度*/private double computeCosineSimilarity(Map<Long, Integer> ratings1, Map<Long, Integer> ratings2) {// 獲取共同評價的圖書Set<Long> commonBooks = new HashSet<>(ratings1.keySet());commonBooks.retainAll(ratings2.keySet());if (commonBooks.isEmpty()) {return 0.0;}// 計算點積和范數double dotProduct = 0.0;double norm1 = 0.0;double norm2 = 0.0;// 計算共同圖書的部分for (Long bookId : commonBooks) {int r1 = ratings1.get(bookId);int r2 = ratings2.get(bookId);dotProduct += r1 * r2;norm1 += r1 * r1;norm2 += r2 * r2;}// 計算各自獨有的部分norm1 += ratings1.keySet().stream().filter(bookId -> !commonBooks.contains(bookId)).mapToDouble(bookId -> Math.pow(ratings1.get(bookId), 2)).sum();norm2 += ratings2.keySet().stream().filter(bookId -> !commonBooks.contains(bookId)).mapToDouble(bookId -> Math.pow(ratings2.get(bookId), 2)).sum();if (norm1 == 0 || norm2 == 0) {return 0.0;}return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));}/*** 定時任務:每天凌晨3點更新相似度矩陣*/@Scheduled(cron = "0 0 3 * * ?")public void scheduledSimilarityUpdate() {log.info("定時任務:開始更新用戶相似度矩陣...");try {preComputeUserSimilarities();log.info("定時任務:用戶相似度矩陣更新完成");} catch (Exception e) {log.error("定時任務:更新用戶相似度矩陣失敗", e);}}
}

協同過濾解析

協同過濾推薦算法原理與實現分析

這個代碼實現了一個基于用戶的協同過濾(User-based Collaborative Filtering)推薦系統,下面我將結合代碼詳細解析協同過濾算法的原理和實現方式。

1. 協同過濾基本概念

協同過濾(CF)是推薦系統中最經典的算法之一,核心思想是:
“相似用戶喜歡的物品,你也可能喜歡”

1.1 兩種主要類型

  • 基于用戶的協同過濾(User-CF):代碼中實現的就是這種
  • 基于物品的協同過濾(Item-CF)

2. 代碼實現的User-CF流程

2.1 整體流程

  1. 離線計算階段

    • 構建用戶-物品評分矩陣
    • 計算并存儲用戶相似度
  2. 在線推薦階段

    • 查找相似用戶
    • 基于相似用戶的偏好生成推薦

2.2 核心代碼解析

(1) 離線計算階段 (preComputeUserSimilarities)
public void preComputeUserSimilarities() {// 1. 清空舊數據clearExistingSimilarityData();// 2. 獲取所有用戶IDList<Long> userIds = orderMapper.selectAllUserIdsWithCompletedOrders();// 3. 構建評分矩陣Map<Long, Map<Long, Integer>> ratingMatrix = buildRatingMatrix(userIds);// 4. 計算并存儲相似度computeAndStoreSimilarities(userIds, ratingMatrix);
}

評分矩陣構建

  • 用戶為行,圖書為列
  • 值為購買數量(作為評分)

相似度計算

  • 使用余弦相似度(Cosine Similarity)
  • 只存儲相似度高于閾值(SIMILARITY_THRESHOLD)的關系
(2) 相似度計算 (computeCosineSimilarity)
private double computeCosineSimilarity(Map<Long, Integer> ratings1, Map<Long, Integer> ratings2) {// 獲取共同評價的圖書Set<Long> commonBooks = new HashSet<>(ratings1.keySet());commonBooks.retainAll(ratings2.keySet());// 計算點積和范數double dotProduct = 0.0;double norm1 = 0.0;double norm2 = 0.0;// 計算余弦相似度return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}

余弦相似度公式:

similarity = (A·B) / (||A|| * ||B||)
(3) 在線推薦階段 (recommendBooksByUserCF)
public List<Book> recommendBooksByUserCF(Long userId, int limit) {// 1. 獲取用戶相似度數據Map<Long, Double> similarityScores = getSimilarityScores(userId);// 2. 獲取最相似的N個用戶List<Long> similarUserIds = getTopSimilarUsers(similarityScores, 10);// 3. 生成推薦return generateRecommendations(userId, similarUserIds, limit);
}

3. 關鍵技術點

3.1 數據存儲優化

  • Redis存儲:用戶相似度矩陣存儲在Redis中,鍵格式為recommend:user:similarity:{userId}
  • 定期更新:通過@Scheduled每天凌晨3點更新

3.2 性能優化

  • 只計算上三角矩陣:避免重復計算(A-B和B-A的相似度相同)
  • 閾值過濾:只存儲有意義的相似關系(SIMILARITY_THRESHOLD)
  • 分批處理:每處理100個用戶打印進度

3.3 推薦生成邏輯

  1. 排除用戶已購買的圖書
  2. 根據相似用戶的購買記錄加權計算推薦分數
  3. 按分數排序返回TopN推薦

4. 協同過濾的優缺點

優點:

  • 不需要物品內容信息:僅依賴用戶行為數據
  • 能發現潛在興趣:可能推薦出意想不到的物品
  • 解釋性強:可以告訴用戶"因為和你相似的用戶也喜歡"

缺點:

  • 冷啟動問題:新用戶/新物品缺乏數據
  • 稀疏性問題:用戶-物品矩陣通常非常稀疏
  • 計算復雜度高:用戶增長時相似度計算成本呈平方級增長

5. 代碼中的工程化實踐

  1. 定時任務@Scheduled定期更新相似度矩陣
  2. 啟動初始化@PostConstruct檢查數據是否需要初始化
  3. 異常處理:完善的try-catch和日志記錄
  4. 性能監控:記錄關鍵步驟的執行時間
  5. 資源清理:計算前先清除舊數據

這個實現是一個典型的生產級協同過濾推薦系統,平衡了算法效果和工程實踐,適合中等規模的電商平臺使用。對于更大規模的系統,可能需要考慮更復雜的優化如分布式計算、近似算法等。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/904712.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/904712.shtml
英文地址,請注明出處:http://en.pswp.cn/news/904712.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JMeter快速指南:命令行生成HTML測試報告(附樣例命令解析)

一、核心命令解析 jmeter -g Dash_CapacityTest_01_AllModules_1000.jtl -o report/ 參數 作用 示例文件說明 -g 指定.jtl結果文件路徑 -o 指定報告輸出目錄 自動創建report文件夾 二、操作步驟&#xff08;Windows/Linux/Mac通用&#xff09; 進入JMe…

2025年滲透測試面試題總結-滲透崗位全職工作面試(附回答)(題目+回答)

網絡安全領域各種資源&#xff0c;學習文檔&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各種好玩的項目及好用的工具&#xff0c;歡迎關注。 目錄 一、通用基礎類問題 1. 自我介紹 2. 職業動機與規劃 3. 加班/出差接受度 二、安全技術類問題 1. 漏…

使用DEEPSEEK快速修改QT創建的GUI

QT的GUI&#xff0c;本質上是使用XML進行描述的&#xff0c;在QT CREATOR的界面編輯處&#xff0c;按CTRL2 切換到代碼視圖&#xff0c;CTRL3切換到編輯器視圖。 CTRL2 切換到代碼視圖 CTRL3 切換到編輯器視圖 鼠標左鍵點擊代碼視圖中&#xff0c;按CTRLA → CTRLC復制XML代碼…

draw.io流程圖使用筆記

文章目錄 圖形較少的問題安裝版好還是非安裝版好業務系統嵌入的draw.io如何導入呢?如何判斷組合和取消組合如何快速選中框里面的內容有時候選不到文本怎么辦連接線如何不走直角 航點和取消航點支持多少種圖形多個連接點?多個圖形對齊雙向箭頭如何畫圖形的大小 其他流程圖圖標…

音頻相關基礎知識

主要參考&#xff1a; 音頻基本概念_音頻和音調的關系-CSDN博客 音頻相關基礎知識&#xff08;采樣率、位深度、通道數、PCM、AAC&#xff09;_音頻2通道和8ch的區別-CSDN博客 概述 聲音的本質 聲音的本質是波在介質中的傳播現象&#xff0c;聲波的本質是一種波&#xff0c;是一…

MySQL中隔離級別那點事

引言 在MySQL中&#xff0c;事務隔離級別和二進制日志&#xff08;binlog&#xff09;的格式密切相關&#xff0c;直接影響數據的一致性和復制的正確性。尤其是在“已提交讀”&#xff08;Read Committed&#xff09;隔離級別下&#xff0c;由于沒有使用間隙鎖&#xff0c;某些…

LeetCode 熱題 100 238. 除自身以外數組的乘積

LeetCode 熱題 100 | 238. 除自身以外數組的乘積 大家好&#xff0c;今天我們來解決一道經典的算法問題——除自身以外數組的乘積。這道題在 LeetCode 上被標記為中等難度&#xff0c;要求在不使用除法的情況下&#xff0c;計算數組中每個元素的乘積&#xff0c;其中每個元素的…

【網絡編程】三、TCP網絡套接字編程

文章目錄 TCP通信流程Ⅰ. 服務器日志類實現Ⅱ. TCP服務端1、服務器創建流程2、創建套接字 -- socket3、綁定服務器 -- bind&#x1f38f;4、服務器監聽 -- listen&#x1f38f;5、獲取客戶端連接請求 -- acceptaccept函數返回的套接字描述符是什么&#xff0c;不是已經有一個了…

STM32的SysTick

SysTick介紹 定義&#xff1a;Systick&#xff0c;即滴答定時器&#xff0c;是內核中的一個特殊定時器&#xff0c;用于提供系統級的定時服務。該定時器是一個24位的遞減計數器&#xff0c;具有自動重載值寄存器的功能。當計數器到達自動重載值時&#xff0c;它會自動重新加載…

【Java項目腳手架系列】第一篇:Maven基礎項目腳手架

【Java項目腳手架系列】第一篇:Maven基礎項目腳手架 前言 在Java開發中,一個好的項目腳手架可以大大提高開發效率,減少重復工作。本系列文章將介紹各種常用的Java項目腳手架,幫助開發者快速搭建項目。今天,我們先從最基礎的Maven項目腳手架開始。 什么是項目腳手架? …

Kafka的消息保留策略是怎樣的? (基于時間log.retention.hours或大小log.retention.bytes,可配置刪除或壓縮策略)

Kafka 消息保留策略詳解 1. 核心保留機制 # Broker 基礎配置示例&#xff08;server.properties&#xff09; log.retention.hours168 # 默認7天保留時間 log.retention.bytes1073741824 # 1GB 大小限制2. 策略類型對比 策略類型配置參數執行邏輯適用場景時間刪除log.re…

五一の自言自語 2025/5/5

今天開學了&#xff0c;感覺還沒玩夠。 假期做了很多事&#xff0c;弄了好幾天的路由器、監控、錄像機&#xff0c;然后不停的出現問題&#xff0c;然后問ai&#xff0c;然后解決問題。這次假期的實踐&#xff0c;更像是計算機網絡的實驗&#xff0c;把那些交換機&#xff0c;…

安卓基礎(靜態方法)

靜態方法的特點?? ??無需實例化??&#xff1a;直接用 類名.方法名() 調用。 ??不能訪問實例成員??&#xff1a;只能訪問類的靜態變量或靜態方法。 ??內存中只有一份??&#xff1a;隨類加載而初始化&#xff0c;生命周期與類相同。 // 工具類 MathUtils publi…

EasyRTC嵌入式音視頻通話SDK驅動智能硬件音視頻應用新發展

一、引言 在數字化浪潮下&#xff0c;智能硬件蓬勃發展&#xff0c;從智能家居到工業物聯網&#xff0c;深刻改變人們的生活與工作。音視頻通訊作為智能硬件交互與協同的核心&#xff0c;重要性不言而喻。但嵌入式設備硬件資源受限&#xff0c;傳統音視頻方案集成困難。EasyRT…

《數字圖像處理(面向新工科的電工電子信息基礎課程系列教材)》封面顏色空間一圖的選圖歷程

禹晶、肖創柏、廖慶敏《數字圖像處理&#xff08;面向新工科的電工電子信息基礎課程系列教材&#xff09;》 學圖像處理的都知道&#xff0c;彩色圖像的顏色空間很多&#xff0c;而且又是三維&#xff0c;不同的角度有不同的視覺效果&#xff0c;MATLAB的圖又有有box和沒有box。…

Flutter 異步原理-Zone

前言 Zone 是 Dart 異步模型中的核心機制&#xff0c;主要用于&#xff1a; 隔離異步上下文&#xff0c;形成邏輯上的執行環境。捕獲未處理的異步異常&#xff0c;保證系統穩定。自定義異步任務的調度行為&#xff08;比如微任務、Timer&#xff09;。 什么是 Zone&#xff1…

聊一聊自然語言處理在人工智能領域中的應用

目錄 一、智能交互與對話系統 二、 信息提取與文本分析 三、機器翻譯與跨語言應用 四、內容生成與創作輔助 五、 搜索與推薦系統 六、垂直領域的專業應用 七、關鍵技術支撐 自然語言處理NLP屬于AI的一個子領域&#xff0c;專注于讓機器理解和生成人類語言&#xff0c;比…

Redis的過期設置和策略

Redis設置過期時間主要有以下幾個配置方式 expire key seconds 設置key在多少秒之后過期pexpire key milliseconds 設置key在多少毫秒之后過期expireat key timestamp 設置key在具體某個時間戳&#xff08;timestamp:時間戳 精確到秒&#xff09;過期pexpireat key millisecon…

vite:npm 安裝 pdfjs-dist , PDF.js View 預覽功能示例

pdfjs-dist 是 Mozilla 的 PDF.js 庫的預構建版本&#xff0c;能讓你在項目里展示 PDF 文件。下面為你介紹如何用 npm 安裝 pdfjs-dist 并應用 pdf.js 和 pdf.worker.js。 為了方便&#xff0c;我將使用 vite 搭建一個原生 js 項目。 1.創建項目 npm create vitelatest pdf-v…

【Android】動畫原理解析

一,基礎動畫 基礎動畫,有四種,分別是平移(Translate)、縮放(Scale)、Rorate(旋轉)、Alpha(透明度),對應Android中以下四種。 1,Animation基類 1,基本概念 1,插值器 插值器的作用,是控制動畫過程的參數,可以理解為 時間(t)與動畫進程(d)的函數,動畫僅…