首页 / 资讯中心 / 文章详情

Django实战:搭建个性化图书推荐系统的完整指南

Django实战:搭建个性化图书推荐系统的完整指南 ★ FEATURED ARTICLE
去年我接手一个图书管理的模拟项目领导给的活儿很简单做一个能让用户看到猜你喜欢的推荐系统。当时我第一反应是找个现成的推荐框架结果调研了一圈发现主流方案不是太重就是太偏——要么依赖大数据组件要么需要单独维护一套推荐服务对我们这个体量来说完全是杀鸡用牛刀。最后我干脆用 Django 把整个推荐系统直接写进项目里数据模型、算法逻辑、管理后台、前端展示一套搞定跑通之后才发现这个组合真的很香而且远比想象中省心。这篇文章就完整拆解一下我当时是怎么从零搭起这个 django 个性化图书推荐系统的。会覆盖技术选型原因、数据模型设计、推荐算法落地、推荐引擎的整体流程、源码结构梳理以及上线前后踩过的那些坑。如果你正好在找图书推荐系统的参考实现或者想把推荐功能整合进现有的 Django 项目这篇文章里的思路和代码片段都可以直接抄作业。1. 为什么用Django做图书推荐一个被低估的组合1.1 图书推荐场景到底需要什么很多人一提到推荐系统第一反应就是机器学习、大数据、实时计算但实际上图书推荐这个场景有个非常鲜明的特点数据量不大但业务逻辑不少。一个普通图书管理系统撑死也就几千本书、几百个用户每天产生的行为数据可能就几千条。这点量级任何一家云计算厂商的推荐服务都能轻松承载但你愿意为它单独引入一套外部服务、多维护一个 SDK、再操心数据同步吗对于大部分中小型项目来说代价太高了。图书推荐真正需要的其实是三样东西能存书和用户数据的数据库、能算相似度和推荐结果的算法逻辑、以及一个能让人看见推荐效果的展示页面。Django 恰好三样全占它自带的 ORM 管理数据Python 写算法又天然顺手模板引擎直接渲染推荐结果一个项目里全部搞定不需要再拼凑任何额外组件。还有一点容易被忽略图书推荐不是纯算法问题它是内容管理用户行为推荐计算的结合体。管理员要上架新书、维护分类、处理封面图用户要能注册登录、评分收藏推荐算法要能读取行为数据算出结果。这个流程里脏活累活占了大半Django 的 Admin 后台和成熟生态能消化掉其中绝大部分。1.2 Django能省下哪些事我举个最直观的例子用户系统。推荐系统肯定要区分用户要记录谁给哪本书打了分这就涉及注册、登录、权限控制、用户扩展字段。如果用 Flask 裸写这些基本都得自己来就算用扩展也得认真读文档配置。而 Django 自带完整的认证体系一个User模型加一个LoginRequiredMixin用户相关的问题就解决了七七八八。更别说它的迁移工具改完模型跑一句makemigrations就完事不会出现测试环境跑得好好的生产环境表结构对不上这种尴尬。Django 的 Admin 后台则是另一个大杀器。随便搭出来的后台界面虽然谈不上精美但增删改查、搜索、筛选、分页这些功能全都开箱即用。管理员想要手动调整某本书的推荐权重或者看看某个用户的评分记录根本不需要额外开发管理页面直接进 Admin 就能操作。这意味着我把精力高度集中在推荐算法上而不是无穷无尽的数据管理功能。1.3 什么情况下别选Django说完优点也得说说边界。Django 不是万能的如果你的项目体量真的到了百万级用户、千万级行为数据的程度那 Django 单体应用扛推荐计算就会很吃力。这时候更适合的做法是把推荐拆成独立服务用专门的向量检索工具来做相似度召回Django 只负责提供接口展示结果。另外如果推荐算法非常前沿且复杂比如要跑深度学习模型、要做实时特征管道那 Python 本身没问题但 Django 的请求-响应模型就不适合做流式计算了算法部分最好独立成服务通过消息队列等方式跟主站交互。我当时就给自己定了一条线数据量在十万级以内、推荐结果可以每天离线更新一次、团队没有专门的算法工程师这三个条件同时满足Django 就是最优解。后来项目跑起来也验证了这个判断。2. 数据模型设计图书、用户和行为才是推荐系统的命根子2.1 图书模型别把字段设计死推荐系统的质量上限有一半取决于数据模型设计得好不好。图书模型是整张数据大网的锚点字段太少了没法做基于内容的推荐字段太死了后面扩展又很痛苦。我设计 Book 模型时第一版是这样的# books/models.py from django.db import models class Category(models.Model): name models.CharField(分类名称, max_length50) parent models.ForeignKey( self, on_deletemodels.CASCADE, nullTrue, blankTrue, verbose_name父级分类 ) class Meta: verbose_name 图书分类 verbose_name_plural 图书分类 def __str__(self): return self.name class Book(models.Model): title models.CharField(书名, max_length200) author models.CharField(作者, max_length100) isbn models.CharField(ISBN, max_length20, uniqueTrue) category models.ForeignKey( Category, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name分类 ) publisher models.CharField(出版社, max_length100, blankTrue) pub_date models.DateField(出版日期, nullTrue, blankTrue) cover models.ImageField(封面, upload_tocovers/, blankTrue) description models.TextField(内容简介, blankTrue) tags models.CharField(标签, max_length200, blankTrue) rating_avg models.FloatField(平均评分, default0) rating_count models.PositiveIntegerField(评分人数, default0) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: verbose_name 图书 verbose_name_plural 图书 indexes [ models.Index(fields[category]), models.Index(fields[author]), ] def __str__(self): return self.title这里有几个关键决策值得展开说。第一个是分类用了自关联外键支持一级和二级分类。图书这个品类很特殊比如小说下面还能分科幻小说、推理小说、言情小说自关联可以保留层级关系而不必为每一级单独建表。当然层级太深了查询会麻烦图书场景两级一般就够用了。第二个是rating_avg和rating_count这两个字段。它们看起来冗余实际上非常有用。推荐算法和排序逻辑都要用到平均分和评分人数如果每次都实时聚合计算效率会很难看。这种用空间换时间的冗余字段在推荐系统里是很常见的做法代价仅是需要保证写入评分时同步更新。第三个是tags字段。我用的是逗号分隔的简单字符串而不是单独建一张标签表。如果你追求规范化标签确实应该单独建表但图书的标签通常比较稳定直接存字符串再在算法里做切分开发效率高得多。我就是这么处理的实际效果并不差。2.2 用户模型先说清楚要不要登录图书推荐系统的用户模型设计取决于一个核心问题系统是否需要登录。如果只是给访客推荐热门书那完全不用建用户模型推荐逻辑也会简单很多但这就不叫个性化了。既然标题里写了个性化那登录体系就是必须的否则系统拿什么去记录不同用户的偏好差异呢Django 自带的User模型提供了用户名、密码、邮箱等基础字段但推荐场景通常还需要用户的阅读偏好比如常看哪个分类、对哪类书不感兴趣、希望推荐偏向新书还是经典。这里有两种做法直接修改User模型不推荐耦合性太强最好做一个独立的用户档案模型通过 OneToOne 关联# users/models.py from django.db import models from django.contrib.auth.models import User class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, verbose_name用户) preferred_categories models.ManyToManyField( books.Category, blankTrue, verbose_name偏好分类 ) bio models.CharField(个人简介, max_length300, blankTrue) created_at models.DateTimeField(注册时间, auto_now_addTrue) class Meta: verbose_name 用户档案 verbose_name_plural 用户档案 def __str__(self): return f{self.user.username}的档案这样做的道理很简单认证相关的字段和推荐相关的偏好字段各管各的Django 升级、换认证方案都不影响推荐侧的数据。preferred_categories可以用于冷启动阶段新用户注册时选几个感兴趣的分类系统就能先推荐这些分类下的高分书。2.3 行为模型显式评分与隐式反馈推荐系统最核心的数据是用户行为。行为又分两类显式的比如用户主动给一本书打五分隐式的比如用户查看了某本书的详情页、把书加入收藏、点击了想看按钮。我最初只建了一张评分表后来发现不够。因为很多用户根本没有评分的习惯但他们的浏览记录其实很能说明问题。一个人反复点开某个分类的书籍详情或者多次收藏同一个作者的书这种隐式行为的信号强度不比一次评分低。所以最终我设计了两个行为模型# books/models.py class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) book models.ForeignKey(Book, on_deletemodels.CASCADE, verbose_name图书) score models.PositiveSmallIntegerField(评分, choices[(i, i) for i in range(1, 6)]) created_at models.DateTimeField(评分时间, auto_now_addTrue) class Meta: unique_together (user, book) verbose_name 评分 verbose_name_plural 评分 def __str__(self): return f{self.user.username} - {self.book.title} ({self.score}) class UserBehavior(models.Model): BEHAVIOR_TYPES ( (view, 浏览), (collect, 收藏), (share, 分享), ) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) book models.ForeignKey(Book, on_deletemodels.CASCADE, verbose_name图书) behavior_type models.CharField(行为类型, max_length10, choicesBEHAVIOR_TYPES) created_at models.DateTimeField(行为时间, auto_now_addTrue) class Meta: indexes [ models.Index(fields[user, behavior_type]), models.Index(fields[book]), ] verbose_name 用户行为 verbose_name_plural 用户行为这里有个设计细节Rating表用了unique_together保证一个用户对同一本书只能有一条评分记录这样后面写算法时不用考虑重复评分的情况。UserBehavior表则刻意允许重复因为用户可能今天浏览了这本书明天又看一眼这本身就是有价值的信号。很多时候行为模型的设计决定了推荐算法的上限。评分数据稀疏就靠隐式行为补隐式行为没有就靠冷启动策略硬扛如果连冷启动方案都没做那推荐系统基本就废了。2.4 建索引的顺序和时机模型设计里有一个经常被忽略的点索引。初学者喜欢给所有字段都加索引这反而是坏味道因为索引占空间、拖慢写入。正确的做法是根据查询模式反推索引。推荐系统最常跑的查询有哪些查某本书的所有评分、查某用户的所有评分/行为、查某分类下的热门书、查某作者的所有作品。对应的索引就很清楚了Rating.user、Rating.book、UserBehavior.user、UserBehavior.book、Book.category、Book.author。我在上面代码里已经体现了。还有一个小技巧如果查询经常同时带user和behavior_type条件那就建联合索引。单独在user上建索引再单独在behavior_type上建索引我前面代码里那两行索引定义就覆盖了这类场景。这个决策是在写推荐算法时对照EXPLAIN查询计划再确认的。3. 推荐算法选型与实现协同过滤不是唯一答案3.1 先认清推荐系统的目标推荐算法不是越复杂越好而是越合适越好。开始选型前必须先想明白系统要优化什么指标。对图书推荐来说最有价值的不是预测用户会给某本书打几分而是能不能把用户真正愿意读的书推到面前。前者叫评分预测后者叫 TopN 推荐两者目标差异很大评分预测看重均方根误差TopN 推荐看重的是推荐列表的准确率和覆盖率。我的项目目标就一句话用户在首页能看到一批自己喜欢的、但之前没发现的书。这个目标直接决定了算法选型方向——排序的准确率优先并且要保证多样性不能推荐十本都是同一个系列的。于是我把候选算法范围锁定在协同过滤和基于内容的推荐上深度学习的方案直接排除原因很简单数据量太小模型容易过拟合而且解释成本太高一个小项目没必要。3.2 UserCF与ItemCF两种思路的对比协同过滤分两大类基于用户的UserCF和基于物品的ItemCF。我花了整整两天把两种算法都在项目里实现了对比效果后留下了一个深刻印象。UserCF 的核心逻辑是找到和当前用户兴趣最相似的一群人把他们喜欢的书推荐出来。ItemCF 的核心逻辑则是找到当前用户历史喜欢的书相似的其它书推荐出来。我在项目里实际跑出的对比结论是这样的对比维度UserCFItemCF算法思路人以群分物以类聚适合场景用户少、物品多、兴趣变化快物品少、用户多、兴趣相对稳定实时性较差用户行为变化需要重算相似用户较好用户新行为能较快映射到相似物品可解释性和你兴趣相似的某某也读过这本书因为你读过 A所以推荐相似的 B冷启动表现新用户几乎无法推荐新用户只要有过一次行为就能推荐图书场景有个特点书的总量相对有限而且用户对书的兴趣通常比较稳定。比如我喜欢科幻那不管是三年前还是现在科幻都是我的主要兴趣。这种情况下 ItemCF 的表现通常比 UserCF 更稳定因为物品的相似关系不会随着用户偏好变化而剧烈波动。我在项目里最终采用了 ItemCF 作为主力算法UserCF 保留作为一个可切换的推荐通道用于某些特殊的运营场景比如系里同门在看什么书这种带社交属性的推荐。3.3 基于内容的推荐标签与分类的妙用纯协同过滤有个天然缺陷它无法处理没有评分记录的书也就是冷启动物品。新书上架的第一天是没有用户打分的协同过滤算不出它的相似度它就没有机会被推荐出去。这时候就需要基于内容的推荐来补位。基于内容的推荐逻辑很直接分析一本书的分类、作者、标签等属性计算它跟用户历史上喜欢书籍的相似度然后把相似的书推荐出去。我在项目中实现了一个不那么严谨但非常实用的版本# recommendations/content_based.py from collections import defaultdict from .models import Book, UserBehavior def content_based_score(user, book): 给一本书打分分数越高代表越可能被推荐 score 0 # 1. 分类匹配度用户浏览过的分类中该分类的占比权重 viewed_books Book.objects.filter( id__inUserBehavior.objects.filter( useruser, behavior_typeview ).values(book_id) ) total_views viewed_books.count() if total_views 0 and book.category: same_cat_views viewed_books.filter(categorybook.category).count() score same_cat_views / total_views * 3 # 2. 作者权重用户是否浏览过同作者的书 if book.author and viewed_books.filter(authorbook.author).exists(): score 2 # 3. 标签匹配拆解标签后计算重合度 user_tags set() for b in viewed_books: if b.tags: user_tags.update(t.strip() for t in b.tags.split(,)) book_tags set() if book.tags: book_tags set(t.strip() for t in book.tags.split(,)) if user_tags and book_tags: overlap len(user_tags book_tags) / len(book_tags) score overlap * 2 return score这段代码其实没有用到任何机器学习库纯粹是利用数据模型里已有的属性做加权求和但效果非常直观。你可以看到每一个加分区都有明确的业务含义分类匹配说明用户大概率喜欢这个方向作者匹配说明用户可能追作者的其它作品标签匹配则是在更细粒度上捕捉偏好。基于内容推荐最大的优点就是解决冷启动新书只要有了分类、作者、标签这些元数据就能参与推荐。它的缺点也很明显推荐的多样性较差容易停留在用户已有偏好的范畴内。所以我的做法不是让它单独工作而是把它作为混合推荐里的一个重要分量。3.4 混合推荐与冷启动处理既然单纯协同过滤有冷启动问题单纯内容推荐有多样性不足的问题那答案就是用混合策略把它们融合起来。我在项目里采用的是一个加权的融合公式每个推荐来源都有各自的分数最后按权重汇总排序。具体权重我是这样调的# recommendations/mix.py WEIGHTS { item_cf: 0.5, content_based: 0.3, hot: 0.2, } def get_mixed_recommendations(user, top_n10): 融合三种推荐策略的分数 candidates {} for source, weight in WEIGHTS.items(): if source item_cf: items item_cf_recommend(user, top_n20) elif source content_based: items content_based_recommend(user, top_n20) else: items hot_recommend(top_n20) for book_id, score in items: candidates[book_id] candidates.get(book_id, 0) weight * score # 过滤掉用户已经读过的书 rated_book_ids set(Rating.objects.filter(useruser).values_list(book_id, flatTrue)) viewed_book_ids set(UserBehavior.objects.filter( useruser, behavior_typeview ).values_list(book_id, flatTrue)) already_read rated_book_ids | viewed_book_ids candidates { book_id: score for book_id, score in candidates.items() if book_id not in already_read } sorted_items sorted(candidates.items(), keylambda x: x[1], reverseTrue) return [book_id for book_id, _ in sorted_items[:top_n]]权重0.5、0.3、0.2不是拍脑袋定的我是通过一组人工评估跑出来的。先选了十几位测试用户让他们标记推荐结果满意/不满意然后调权重看哪组参数能获得最高的满意率。最终发现 ItemCF 最能抓用户口味内容推荐负责多样性热门兜底保证冷门用户也不至于空手而归。冷启动用户的情况也考虑到了。如果UserBehavior里一个行为记录都没有推荐算法就直接降级为热门榜推荐按评分人数和平均分排一个 TopN 出来。等用户产生了三条以上浏览或评分记录再切换到混合推荐。这个降级逻辑简单但可靠用户不会一进首页就看到一片空白。4. 从数据到推荐结果完整构建推荐引擎4.1 评分矩阵与相似度计算的工程实现算法理论讲完现在说工程实现。ItemCF 的第一步是构建用户-物品评分矩阵然后计算物品与物品之间的相似度。经典公式是余弦相似度公式但在 Django 里直接用二维数组存矩阵并不明智。实际项目中我用 Python 的字典嵌套来表示稀疏矩阵只有有数据的位置才占内存。对于几千本书几百个用户的场景这种结构已经绰绰有余# recommendations/similarity.py from collections import defaultdict import math def build_user_item_matrix(): 从数据库读取评分构建用户-物品稀疏矩阵 matrix defaultdict(dict) ratings Rating.objects.all().values(user_id, book_id, score) for r in ratings: matrix[r[user_id]][r[book_id]] r[score] return matrix def cosine_similarity(vec_a, vec_b): 计算两个物品向量的余弦相似度 common_users set(vec_a.keys()) set(vec_b.keys()) if not common_users: return 0.0 dot sum(vec_a[u] * vec_b[u] for u in common_users) norm_a math.sqrt(sum(v * v for v in vec_a.values())) norm_b math.sqrt(sum(v * v for v in vec_b.values())) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b) def compute_item_similarities(): 离线计算出所有物品之间的相似度矩阵 matrix build_user_item_matrix() book_ids list({b for user_items in matrix.values() for b in user_items.keys()}) sim_matrix defaultdict(dict) for i in range(len(book_ids)): for j in range(i 1, len(book_ids)): sim cosine_similarity(matrix.get(book_ids[i], {}), matrix.get(book_ids[j], {})) if sim 0.1: sim_matrix[book_ids[i]][book_ids[j]] sim sim_matrix[book_ids[j]][book_ids[i]] sim return sim_matrix这里有个工程优化细节sim 0.1这个阈值不是随便写的。如果不设阈值所有物品对都要存相似度内存和后续计算的开销会白白翻倍而低于 0.1 的相似度基本不会影响推荐结果。设置一个低阈值可以把相似度矩阵剪枝到只保留有意义的边速度和资源占用同时改善。4.2 离线计算与在线召回推荐计算分两条链路离线预计算和在线召回。我的设计相对朴素但很实用。离线环节在每天凌晨跑一次把物品相似度矩阵和每个用户的热门候选结果提前算好结果存到数据库或缓存里。在线环节当用户访问首页时直接读取用户的行为数据从预先算好的相似度矩阵中取出相似书籍再跟其它推荐来源融合排序。离线计算使用 Django 的manage.py自定义命令来执行配合操作系统的定时任务调度非常干净利落# recommendations/management/commands/update_recommendations.py from django.core.management.base import BaseCommand class Command(BaseCommand): help 更新推荐缓存物品相似度矩阵与用户推荐列表 def handle(self, *args, **options): # 计算物品相似度矩阵 sim_matrix compute_item_similarities() save_sim_matrix(sim_matrix) # 为每个活跃用户预生成推荐列表 from django.contrib.auth.models import User active_users User.objects.filter( userbehavior__isnullFalse ).distinct() for user in active_users: rec_ids get_mixed_recommendations(user, top_n50) cache_user_recommendations(user.id, rec_ids) self.stdout.write(self.style.SUCCESS(推荐缓存更新完成))有人可能会问为什么不直接实时计算因为实时计算同一套相似度矩阵会被重复计算很多次同一用户的推荐结果也被反复生成非常浪费。离线预计算把重型计算统一放到低峰期在线请求只做查询和轻量排序性能和成本一下就平衡了。项目数据量再翻十倍这个架构也扛得住。4.3 排序、过滤与多样性控制推荐列表生成后不能直接返回给前端。至少要过三道过滤第一道是过滤已交互物品。用户已经读过、打过分的书再推荐出来等于提醒用户你的历史我都记得但你收藏夹里的新书我不管体验很糟糕。代码里那次求差集操作就是干这个的。第二道是业务规则过滤。比如某本书下架了、未上架的或者因为版权原因不能展示的都需要在推荐前排除。我实现时是在查询集上额外加了is_activeTrue的过滤条件很简单但必须做。第三道是多样性控制。如果 ItemCF 权重太高推荐列表可能全是同一个系列或同一个作者的书。我用的办法是分组限制按分类分组每个分类最多出现 N 本或者按作者分组同一作者的作品在 TopN 里最多占 3 本。这个策略虽然粗暴但很有效用户反馈推荐结果还挺杂的能发现不同类型的好书。4.4 让推荐结果可解释推荐系统有个很反直觉但非常重要的问题推荐结果本身再好如果用户不知道为什么被推荐信任感就会打折。图书推荐尤其如此用户看到一个陌生书名第一反应往往是这是什么为什么推给我。我给推荐接口加了一个推荐理由字段每次返回推荐结果时带上理由模板ItemCF 来源因为你读过《三体》所以推荐《球状闪电》内容推荐来源你常看科幻分类因此推荐这本热门来源近期读者评分较高的一本书实现上并不复杂在推荐结果里多存一个reason字段前端展示时渲染成一段小字就行。但这一个小小的改动让我收到了大量的正面反馈。用户开始愿意点进推荐详情页而不是把它当成一个无脑的广告位。5. 源码导读项目结构、核心模块与关键代码5.1 项目目录到底该怎么拆Django 项目最忌讳的就是把所有模型塞进一个巨大的models.py里。推荐系统天然分为用户、图书、推荐三大块项目结构也应当按这个逻辑拆。我的最终目录是这样的book_recommend/ ├── manage.py ├── config/ # 项目配置 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── users/ # 用户模块 │ ├── models.py │ ├── views.py │ └── urls.py ├── books/ # 图书模块 │ ├── models.py # Category, Book, Rating, UserBehavior │ ├── views.py # 图书列表、详情、评分处理 │ ├── urls.py │ └── admin.py ├── recommendations/ # 推荐模块 │ ├── algorithm.py # 协同过滤与内容推荐算法 │ ├── mix.py # 混合推荐流程 │ ├── content_based.py # 基于内容的推荐 │ ├── similarity.py # 相似度计算 │ └── management/ │ └── commands/ │ └── update_recommendations.py └── templates/ # 前端模板 ├── base.html ├── index.html # 首页推荐列表 └── book_detail.html这个拆分的关键逻辑是recommendations不依赖books的视图层只依赖它的模型层。算法模块可以独立测试甚至可以单独写单元测试而不用启动整个 Web 服务。我在实际开发中感受很深这样的模块边界让调试推荐算法时不用在视图和模型之间来回跳。5.2 核心模块代码走读推荐模块的algorithm.py是整个系统的最核心部分。文件里的item_cf_recommend函数是推荐主力的入口# recommendations/algorithm.py from collections import defaultdict def item_cf_recommend(user, top_n10): 基于物品的协同过滤推荐 # 获取用户评分过的书籍 user_ratings user.rating_set.select_related(book).all() rated_book_ids set(user_ratings.values_list(book_id, flatTrue)) if not rated_book_ids: return [] # 读相似度矩阵 sim_matrix load_sim_matrix() scores defaultdict(float) for book_id in rated_book_ids: for sim_book_id, sim_score in sim_matrix.get(book_id, {}).items(): if sim_book_id not in rated_book_ids: # 加权累加权重为用户对原书的评分 rating_score user_ratings.get(book_idbook_id).score scores[sim_book_id] sim_score * rating_score # 排序取 TopN sorted_scores sorted(scores.items(), keylambda x: x[1], reverseTrue) return [(book_id, score) for book_id, score in sorted_scores[:top_n]]这个实现的逻辑很经典用户喜欢的书是锚点与之相似的物品会被加权推荐权重跟用户对锚点书的评分挂钩。这里user_ratings.get(book_idbook_id).score实际写起来我用了字典预取而不是重复查询避免了一次循环里的 N 次数据库访问细节很重要。load_sim_matrix函数读取的是离线任务缓存到 Redis 或数据库里的相似度矩阵这是离线计算和在线推荐之间的桥梁。正式项目里我建议把相似度矩阵放进 Redis虽然这个矩阵几千本书的规模也就几百 KB但 Redis 的读取速度和简单数据类型处理会比数据库友好得多。5.3 Admin后台给管理员留口子推荐系统的运营不能完全依赖算法自动跑必须给管理员留手动干预的入口。Django Admin 的list_display、search_fields和list_filter让管理操作变得非常顺手# books/admin.py from django.contrib import admin from .models import Book, Category, Rating, UserBehavior admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display (title, author, category, publisher, rating_avg, rating_count) search_fields (title, author, isbn) list_filter (category, publisher) ordering (-rating_count,)运营中的常见需求是某本书评分虚高但不该被推荐或某本书是 ed 推荐位需要顶上去。针对这类需求我给Book模型加了一个is_recommendable布尔字段并在推荐算法的过滤阶段把它考虑进去。只要后台管理员把这个字段勾掉推荐引擎就会自动忽略这本书实现不写代码、不做版本发布、直接干预推荐结果的目标。这个字段后来救了无数次场因为运营数据调整的频率远比想象中高。6. 性能优化与踩坑记录上线前必须知道的几件事6.1 N1查询问题Django 开发里最经典的性能坑就是 N1 查询。在推荐结果页上如果每展示一本书都要额外查一次作者、一次分类、一次评分页面加载时间会指数级上涨。我一开始天真地以为数据量小不用在意结果在本地调试时推荐列表只有十本书但 Django Debug Toolbar 显示的查询数达到了五十多条。排查后发现问题出在模板里循环访问book.author和book.category上。解决办法很标准就是在视图层一次性把关联对象查出来from django.db.models import Prefetch books Book.objects.filter(is_recommendableTrue).select_related( category ).prefetch_related( Prefetch(rating_set, querysetRating.objects.all(), to_attrratings_list) )加了select_related和prefetch_related之后五十多条查询压到五条以内。如果你也在做类似项目我的经验是每次写完一个视图都先看一眼查询数量这个习惯越早养成越好。6.2 相似度计算的性能陷阱相似度计算的最大陷阱是暴力双重循环。物品数量是 5000 时最坏情况下要算 2500 万对相似度即使每对只要几微秒整体也需要几十秒甚至更久。我踩过一次坑当时没有加前面提到的 0.1 阈值也没有先剪枝结果离线任务跑了将近十分钟把开发机的内存吃掉了一大半。后来我做了两个优化第一先做物品的倒排索引。只有被同一用户评分过的两本书之间才有可能计算相似度所以先遍历用户的评分记录把共现过的物品对找出来再去计算。这一步把需要计算的物品对数量从几千万降到几万。第二设置相似度阈值。低于阈值的物品对直接丢弃不进入相似度矩阵。这一步进一步压缩了矩阵规模。优化之后同样的离线任务从十分钟缩短到了十几秒效果显著。6.3 数据稀疏问题评分数据严重稀疏这是图书推荐系统绕不开的现实。几百个用户对几千本书打分矩阵的稠密度通常不到 1%绝大部分物品对没有共同评分用户相似度直接为零。面对稀疏问题我最开始只依赖评分数据推荐效果很差。后来把浏览、收藏等隐式行为也纳入用户-物品矩阵让view行为折算成 0.5 分、collect行为折算成 1.5 分、评分保留实际分数矩阵的稠密度一下子就上去了。这个隐式行为分数化的技巧来自一个很朴素的观察用户可能懒得打分但他们的浏览行为不会骗人。与其跟稀疏评分死磕不如换个思路扩大信号来源。6.4 部署时的几个小坑部署阶段遇到的坑比开发阶段还多这里说几个有代表性的。第一个是DEBUGFalse后静态文件全部丢失。Django 在开发模式下会自动托管静态文件但生产模式不会。解决办法是把whitenoise库加进项目它会自动处理静态文件托管不用额外配 Nginx 规则对小型项目来说是最省心的方案。第二个是 SQLite 在并发写入时会报database is locked。图书推荐系统的写入场景集中在用户评分和行为记录一旦访问量上来 SQLite 就开始出幺蛾子。后来我换成了 PostgreSQL在settings.py里改了数据库配置这个问题就再也没有出现过。第三个是定时离线任务没有接日志。刚开始跑离线任务时某次计算失败了我完全没察觉用户连续两天看到的都是旧推荐列表。后来我在命令里加了两行stdout输出和异常捕获把关键步骤的执行状态追加到日志文件里再配上邮件告警才算踏实。第四个坑是关于源码里的图片路径。Book.cover用的是ImageField如果部署在云服务器上图片存储和访问的路径配置很容易出错。我当时的解决办法是先存到本地/media/目录等数据量大了再迁移到对象存储。这个过渡方案在小体量项目里是完全够用的。还有一个值得单独说的经验不要把推荐结果直接硬编码在模板里。一定要走接口哪怕是用 Django 视图返回 JSON前端再渲染都行。这样以后算法调整、推荐位变化、A/B 测试都更方便不用反复改模板文件。最后再分享一个我自己的体会。图书推荐系统的难点从来不在算法多高深而在数据积累、模型设计和各种工程细节的打磨。如果你没有真实的用户行为数据与其把时间花在调参上不如先把数据模型和管理后台做好让系统先跑起来再慢慢迭代算法。我把这个项目的源码整理好了用的是经典实用的技术栈注释也写得比较详细需要的朋友可以直接拿去参考。调试过程中如果遇到 Django 版本兼容问题或者想换用基于向量的召回方案欢迎在评论区交流我可以基于实际踩坑经验给你更具体的思路。
阅读完成 · 觉得有帮助?
咨询建站