☰
基于Django+协同过滤的电影推荐系统实战解析
2026/10/27 15:24:50 网站建设 项目流程

1. 项目定位与整体设计思路

1.1 这个系统到底要解决什么问题

先说个背景。豆瓣电影推荐系统这个选题,我在课程设计、毕业设计里见的频率非常高,几乎每年都会有人在技术社区问"基于Django的电影推荐系统怎么做"。这个项目之所以经典,是因为它把两个非常核心的东西串在一条线上:一个是Python生态里应用最广的Web框架Django,另一个是推荐系统领域最基础、最常用的协同过滤算法。你把这两样真正打通之后,做出来的不只是个能登录、能看列表的Demo,而是一套完整的数据闭环——用户产生评分行为,后台拿到行为数据,离线任务在后台算相似度,在线接口再根据相似度给用户推电影。

这个系统的目标很明确:在用户面对几千部电影不知道看什么的时候,给出一份个性化的推荐列表。它解决的是信息过载问题。你可能觉得"不就是做个猜你喜欢嘛",但真要把推荐逻辑做得可用,需要考虑的东西远比想象中多。我见过太多人把协同过滤代码跑出来了,但推荐结果要么全是最热门电影,要么全是空白,甚至有的项目推荐接口一调用就超时。这些坑后面我会一个个说清楚。

项目要实现的完整功能链路是这样的:用户注册登录,维护个人基本信息;浏览电影列表查看电影详情;给看过的电影打分;系统根据用户的历史评分记录,用协同过滤算法计算出用户可能感兴趣的电影集合,通过推荐接口输出。对于课程设计或毕设来说,这套功能已经非常完整,既有Web开发的技术含量,又有算法落地的实际场景。

1.2 为什么选Django加协同过滤这套组合

先说Django这边。Django自带的后台管理、ORM、认证系统和迁移机制,能让一个规模不大的项目快速成型。特别是项目里的数据模型需要反复调整,比如今天想加个演员表、明天想改评分的精度,用Django的模型迁移基本不需要手工改数据库,一条migrate命令就够了。对于学生项目来说,这种开发效率是巨大的优势。Django的MTV架构也让代码分层清晰,数据模型放models、业务逻辑放services、页面展示逻辑放views,模块边界一旦清晰了,后面写起来就顺很多。更关键的是Django做接口开发很直接,推荐系统最终是要给前端提供推荐列表的,用Django写REST接口或者直接返回JSON都很方便。

再说协同过滤。推荐系统里其实有很多流派:基于内容的推荐要解析电影的导演、演员、剧情描述,做关键词匹配和特征提取;协同过滤只需要用户对电影的打分数据,不需要理解电影内容本身。后者对数据的依赖更纯粹,也更符合一个课程项目的复杂度定位。你不需要准备大规模的自然语言处理数据,不需要搭建知识图谱,只要有一个用户行为表,算法就能运转。协同过滤里面又分两类:基于用户的和基于物品的。这两者我一个都不会跳过,但就电影推荐这个场景来说,我会重点讲基于物品的协同过滤,也就是ItemCF,后面会解释为什么。

至于为什么不选择深度学习方案比如矩阵分解、图神经网络之类——不是它们不好,而是数据量不够。你手上可能就一万个用户、几千部电影,这点数据量跑深度学习模型,效果很可能还不如一个实现正确的协同过滤。算法不是越复杂越好,"够用、稳定、能解释"才是学生项目和中小型业务场景最需要考虑的。这一点也是我在做完这个项目之后体会最深的。

1.3 系统整体数据流:从评分到推荐的完整链路

可以把整个系统理解成一个流水线,分为离线部分和在线部分。离线部分负责算好所有"准备工作",在线部分负责快速响应请求。

用户在前端给某部电影打了4分,这个行为立即写入Rating表。系统里会定期执行一个离线任务,把所有用户的评分数据拉出来,构建用户-电影评分矩阵,计算电影与电影之间的相似度,把结果缓存到Redis或者一张专门的表里。这个过程不需要实时完成,因为它只跟"当前所有用户的评分数据"相关,数据变化没那么频繁,完全可以一天跑一次甚至一周跑一次。

在线部分则是用户访问推荐页面时,后端拿到当前用户已经评过分的电影ID列表,去缓存里查这些电影各自的相似电影,做加权汇总、滤掉已经看过的、排序,最后返回TopN推荐结果。这个链路必须得快,用户点一下页面不可能等几秒钟。

这个设计的核心思路就是"离线算、在线查"。如果你每次请求都现场跑一遍全网相似度计算,数据量一大马上就会爆炸。这也是很多初学者最容易踩的坑——算法代码在Jupyter里跑通了,就直接搬到View里每次请求都跑一遍,最后接口响应时间几秒钟起步。正确做法是一定要把计算量大的部分放到离线任务里,在线只做轻量查询。

2. 协同过滤算法核心原理拆解

2.1 基于用户的协同过滤:找到和你有共同口味的人

基于用户的协同过滤(UserCF)是最早被提出来的协同过滤实现方式,它的直觉非常朴素:如果你跟某个人对好几部电影的评分都很接近,那你们俩口味大概率相似,他喜欢的你大概率也会喜欢。

具体操作分三步。第一步,拿到目标用户A和所有其他用户对电影的评分记录。第二步,计算A与其他每个用户之间的"相似度",这个相似度不是性格测试那种,而是看评分曲线的吻合程度,评分越接近,相似度越高。第三步,把所有相似用户的评分按相似度加权汇总,排除A已经看过的电影,生成推荐列表。

我说一个生活化的类比。你去一家餐厅吃饭,看到隔壁桌那两个人点的好几道菜都跟你平时喜欢的一样,你觉得他们口味跟你很投缘,于是问他们"还有什么菜推荐"。UserCF干的就是这件事——自动找到"口味投缘"的人,参考他们的选择来给你推荐。

UserCF有一个明显的局限性,它是在"用户"这个维度上做计算的。当系统的用户数量远大于电影数量时,计算所有用户两两之间的相似度,开销会非常大。而在一个电影网站里,用户量通常是电影的很多倍,这意味着UserCF的相似度矩阵会非常庞大。更麻烦的是,用户兴趣会漂移,这个月喜欢科幻,下个月喜欢文艺片,UserCF对这种变化反应比较迟钝。

2.2 电影场景我为什么更推荐ItemCF

基于物品的协同过滤(ItemCF)的基本逻辑是:给你推荐"和你已经喜欢的电影相似"的其他电影。注意这里的"相似"不是指题材或类型上的相似,而是"经常被同一批用户评分"的相似。给你看《星际穿越》评了高分,那系统找找还有哪些电影是被喜欢《星际穿越》的这批用户也打了高分的,大概率就是你的菜。

这个逻辑在电影场景下有一个巨大的优势:电影的总量是相对稳定的,而且新电影增加的速度远赶不上用户增加的速度。所以电影之间的相似度矩阵规模是可控的,你可以离线把所有电影两两相似度算好存起来。用户兴趣再怎么漂移也没关系,只要他现在喜欢某部电影,系统就能立刻找到和这部电影相似的候选集。

在工业界,亚马逊的"买了A商品的用户也买了B商品"就是典型的ItemCF思路。在豆瓣电影和各类视频网站上,ItemCF的表现通常也要好于UserCF,因为电影这种物品的"用户评分共现"特征非常明显。所以我的项目里推荐算法的核心是基于物品的协同过滤,UserCF作为对照组和补充方案,分析两者在不同场景下的表现差异。

2.3 相似度计算与评分预测的代码化实现

相似度计算是整个算法的地基。最常用的指标是余弦相似度。把每个电影看作一个向量,向量的每一维是一个用户的评分。两部电影越相似,它们对应向量的夹角就越小,余弦值越接近1。

公式很简单:cos(A, B) = A·B / (|A| × |B|)。落在代码里,不用自己手写这个数学公式,直接用sklearn的cosine_similarity就能算。但这里有一个重要的优化细节:直接用原始评分算余弦相似度,会遇到一个问题——有些用户习惯打高分,有些用户习惯打低分,这会导致评分尺度不统一。更稳的做法是先对每行做去均值处理,也就是皮尔逊相关系数的思路,这样计算出来的相似度更准确。我在项目里用的是"去均值后的余弦相似度",效果比直接算余弦好不少。

相似度算完之后,推荐得分的计算逻辑是:对你没看过的候选电影,累加"你对已看过的电影的评分 × 候选电影与已看过的电影的相似度"。这个累加值就是候选电影的推荐分数。也可以再除以相似度总和做归一化,防止热度高的电影通过共现次数堆分数。

2.4 冷启动问题的应对策略

冷启动是推荐系统里的老难题。新用户一条评分记录都没有,系统怎么知道他想看什么?新电影一部评分都没有,系统怎么把它推给别人?

我的处理方式是分层的。对于新用户,不做协同过滤,直接返回全站热门的电影列表,或者按注册时选择的兴趣标签做粗粒度筛选。这是最简单也最实用的兜底方案。对于新电影,协同过滤矩阵里对应的行是全零,算不出有效的相似度。这里我在系统里加了一个"类型相似兜底"逻辑:新电影上线后先按类型标签匹配同类型的电影名单,如果类型库里其他电影有用户评分,就按评分排序推给用户。等新电影有了一定数量的评分记录之后,它才会进入协同过滤的计算池。这套组合策略的代码会在第四章里展开。

3. Django后端核心设计与数据建模

3.1 三张核心表:用户、电影、评分的建模细节

用Django做数据建模非常舒服,ORM直接映射数据库表结构。这个项目的核心模型就三张表,User、Movie、Rating,外加一张用于缓存推荐结果的RecommendationCache表。代码结构如下:

from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): username = models.CharField(max_length=50, unique=True) avatar = models.URLField(blank=True) preference_tags = models.CharField(max_length=100, blank=True, help_text="注册时选择的兴趣标签") class Meta: db_table = "user" class Movie(models.Model): title = models.CharField(max_length=200, verbose_name="电影名") original_title = models.CharField(max_length=200, blank=True) year = models.IntegerField(null=True, blank=True, verbose_name="上映年份") genres = models.CharField(max_length=100, verbose_name="电影类型") cover_url = models.URLField(blank=True, verbose_name="封面图") description = models.TextField(blank=True, verbose_name="简介") avg_rating = models.FloatField(default=0, verbose_name="平均评分") rating_count = models.IntegerField(default=0, verbose_name="评分人数") class Meta: db_table = "movie" indexes = [models.Index(fields=["genres"]), models.Index(fields=["year"])] class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="ratings") movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name="ratings") score = models.FloatField(verbose_name="评分") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "rating" unique_together = ("user", "movie") class RecommendationCache(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="rec_cache") items = models.JSONField(default=list, verbose_name="推荐电影ID列表") updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "recommendation_cache"

几个设计上的考虑值得展开说一下。

Rating表里的unique_together是关键。它保证了同一个用户对同一部电影只能打一次分,不会出现两条重复评分。这个约束在数据库层面防住了重复数据,比在业务逻辑里到处写if判断要可靠得多。

Movie表的genres字段我用了逗号分隔的字符串,比如"剧情,悬疑,犯罪"。你可能觉得这样做不够范式化,应该拆一张电影-类型关联表。诚实地讲,如果要做复杂的类型筛选和统计,拆表是更好的;但在这个项目里,类型只是用来做冷启动兜底和列表筛选,逗号分隔完全够用,还省掉了一次联表查询。这就是典型的学生项目取舍。

RecommendationCache表的作用是把推荐结果持久化。离线任务算好推荐列表后写入这张表,在线接口直接读取,不再重复计算。加这张表还有一层好处:如果Redis挂了,推荐接口不至于完全不能用,至少能从数据库兜底读取上一次的推荐结果。

3.2 算法逻辑封装成独立服务层

很多参考代码喜欢把算法直接写在View里面,我强烈不建议这么干。推荐算法的计算逻辑和HTTP请求处理逻辑完全没有关系,混在一起既不好调试也不好扩展。我在项目里建了一个独立的recommender app,把所有算法逻辑放在services.py里,View只负责取参数、调用服务、返回结果。

services.py里封装了三个核心函数:构建评分矩阵、计算电影相似度、生成用户推荐列表。评分矩阵的构建直接查Rating表然后转成pandas的DataFrame。这里有个小技巧:直接查库拿全部评分数据可能很快(几千条),也可能很慢(几十万条),所以要用values_list拿到纯字段值,避免ORM逐条实例化对象。实际测试下来性能差距是很明显的。

生成用户推荐列表的时候还封装了一个可选的"排除已看"参数。这个函数既给在线接口用,也给离线批量预计算用,一套逻辑两边复用。函数签名设计得清楚一点,后面扩展会省很多事。

3.3 推荐接口设计与JSON结构

接口设计我走的是最实用的路线。所有接口统一在recommender/views.py里,用Django自带的View类实现,不引入额外框架。前端或者测试工具只需要请求一个URL就能拿到JSON。

推荐接口的完整逻辑是这样:拿到当前登录用户的user_id,先查RecommendationCache,有缓存且没过期就直接返回JSON;否则调services里的冷启动逻辑,返回热门榜。返回的数据结构里除了电影的基本字段,还会带上推荐依据——给用户推荐了哪几部电影,是基于哪些已看过的电影推荐出来的,相似度是多少。这一步对毕设答辩特别有用,老师一问"推荐结果怎么来的",你直接打开接口看推荐依据列表,说服力很强。

关于API设计,有几个要点:接口一律返回JSON,不掺HTML;异常情况返回明确的错误码和错误信息;推荐列表的每一部电影都带完整信息,前端不再二次请求详情;列表顺序严格按照推荐分数从高到低排。

3.4 数据导入与同步的工程化处理

数据是推荐系统的原料。我用的实验数据集是MovieLens公开数据集,里面包含电影信息、用户评分等字段,格式非常规整,是推荐系统方向最经典的数据集之一,很多论文和公开课的实验数据都来自这里。

导入流程我写成了一个Django management command,用python manage.py load_movielens这样的方式执行。命令里做的事情包括:读取movies.csv解析电影信息,逐条写入Movie表;读取ratings.csv批量写入Rating表。批量写入用bulk_create,一次性提交几千条数据,比逐条save快一个数量级。这里有一个实际体验值得记录:第一次我直接逐条遍历保存,五千条评分数据卡了差不多半分钟,改成bulk_create之后体感是瞬间完成的。数据导入看起来是体力活,但如果处理得不好,后面重构模型的成本会非常高。

4. 实操过程与核心环节完全复盘

4.1 从零创建项目和App

我把完整的实操流程从头到尾走一遍,方便你直接照着做。项目名称我用douban_movie。第一步是初始化项目:

# 创建虚拟环境(强烈建议,别装在全局环境) python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate # 安装依赖 pip install django pandas scikit-learn redis # 创建项目与核心App django-admin startproject douban_movie cd douban_movie python manage.py startapp users python manage.py startapp movies python manage.py startapp recommender

创建完app之后,记得在douban_movie/settings.py的INSTALLED_APPS里把这几个app加上,同时配置好数据库连接和Redis连接。数据库我用的是SQLite,因为项目数据量不大,SQLite完全扛得住,而且零配置,最适合学习和实验。如果你的数据量到了几百万条,再换MySQL或PostgreSQL不迟。

Redis在这里的职责是缓存相似度矩阵和推荐结果。在settings.py里配置一下连接池,然后写一个简单的redis_client模块,所有地方统一从这里拿连接。

4.2 离线计算电影相似度矩阵

这是整个系统里算法含量最高的部分。我按实践中的步骤逐步说清楚。

第一步,从Rating表把所有评分数据拉出来,转成pandas DataFrame:

import pandas as pd from movies.models import Movie, Rating from users.models import User def build_rating_matrix(): # 用 values_list 取值,避免 ORM 实例化开销 ratings = list( Rating.objects.values_list("user_id", "movie_id", "score") ) df = pd.DataFrame(ratings, columns=["user_id", "movie_id", "score"]) # 转成用户-电影评分矩阵 matrix = df.pivot_table( index="user_id", columns="movie_id", values="score" ).fillna(0) return matrix

这里是第一个关键点。评分矩阵的默认值是0,而0在数学上的含义是"没有评分",但0同时也是评分体系里的一个值。如果我们直接拿这个矩阵算余弦相似度,所有没评过分的位置都被当成"打了0分",这会让相似度计算严重失真。解决办法有两种:一是用掩码计算,只统计有评分的位置;二是对矩阵做去均值处理,把每行用户自己的评分均值减掉,然后用sklearn的cosine_similarity直接算。去均值之后,没打分的0依然存在,但由于减去了用户均分,0已经不再是绝对的0分意义,而是"与该用户平均水平的偏差",这样处理之后相似度计算的结果要合理得多。

第二步,计算电影与电影的相似度矩阵:

from sklearn.metrics.pairwise import cosine_similarity # 转置矩阵,让每一行代表一部电影在所有用户上的评分 movie_matrix = matrix.T # 对每个用户行做去均值处理,消除评分尺度差异 centered = movie_matrix.apply(lambda row: row - row[row > 0].mean(), axis=1).fillna(0) sim_matrix = cosine_similarity(centered)

这里我用scikit-learn的cosine_similarity,比手写双重循环快太多了。在几千部电影、几万条评分的规模下,这个计算在几秒内完成。如果数据量更大,可以改成按块计算或者用更高效的内存布局。

第三步,把结果序列化之后存进Redis:

import json movie_ids = list(movie_matrix.index) # 保存电影ID列表和相似度矩阵 cache_key = "movie_id_list" redis_client.set(cache_key, json.dumps(movie_ids)) # 相似度矩阵存成按电影的邻接表 for i, movie_id in enumerate(movie_ids): sims = sim_matrix[i] # 按相似度从高到低排序,取前50个作为候选 top_indices = sims.argsort()[::-1][1:51] neighbors = [] for idx in top_indices: if sims[idx] > 0: neighbors.append({ "movie_id": movie_ids[idx], "sim": round(float(sims[idx]), 4) }) redis_client.set( f"movie_sim:{movie_id}", json.dumps(neighbors), ex=86400 # 缓存一天 )

这个"取每个电影最相似的Top50"的做法非常关键。如果不做截断,每个电影都要存储所有其他电影的相似度,矩阵是稠密的,几千部电影就意味着几百万条记录,存储和查询压力都很大。截断之后,每个电影只需要存50个最相似邻接项,系统存储量和查询速度都大幅优化。推荐效果并不会因此受太大影响,因为相似度很低的那些电影对最终排序的贡献本来就可以忽略。

第四步,把整个离线计算流程做成一个management command,方便定期执行:

python manage.py compute_similarity

这个命令内部就是把上面几步串起来执行。我把这个命令挂到了crontab里,每天凌晨2点跑一次。这里的工程化价值在于:算法和数据是分离的,算法代码只负责计算,调度交给系统工具,不需要在Python里写复杂的定时任务逻辑。

4.3 在线推荐接口的实现

离线相似度算好之后,在线推荐就非常轻量了。核心逻辑是:查用户最近评过分的电影,去Redis里找这些电影的相似电影,累加得分,排序,返回。

import json from django.http import JsonResponse from django.views import View from .services import get_recommendations from movies.models import Movie class RecommendView(View): def get(self, request): user = request.user if not user.is_authenticated: return JsonResponse({"code": 401, "msg": "请先登录"}) # 获取用户的最近评分记录(评分时间倒序,取最近20条) recent_ratings = user.ratings.select_related("movie").order_by("-created_at")[:20] rated_movie_ids = {r.movie_id for r in recent_ratings} if not rated_movie_ids: # 没有评分记录,走冷启动:返回热门榜 hot_movies = Movie.objects.order_by("-rating_count")[:20] return JsonResponse({"code": 0, "data": [ {"id": m.id, "title": m.title, "genres": m.genres, "avg_rating": m.avg_rating, "reason": "近期热门"} for m in hot_movies ]}) # 获取推荐结果,算法逻辑全部在 service 里 recommendations = get_recommendations(user.id, list(rated_movie_ids)) return JsonResponse({"code": 0, "data": recommendations})

get_recommendations函数的具体实现是:遍历最近的评分记录,对每条记录去Redis取相似电影列表,按相似度乘以用户评分累加得到得分。同时对得分做归一化处理,避免评分次数多的用户天然分数堆得更高。最终返回的推荐项会附带"推荐依据",也就是基于哪部电影推荐的、相似度多少。

这里我讲一个具体的参数调整经验。最近评分记录取多少条最合适?我试过5条、10条、20条、50条。结果发现取太少,推荐结果过于依赖单部电影,多样性差;取太多,会把用户很久以前的老兴趣也纳入进来,反而干扰当前兴趣。最终取20条是平衡点,既覆盖了用户近期的主要兴趣点,又不会让时间太久的评分干扰结果。

4.4 接口实测与性能数据

项目做完之后,我对推荐接口做了性能实测。测试条件:MovieLens数据集约1000部电影、6000个用户、10万条评分记录,本地开发环境,SQLite加Redis。实测结果:

场景耗时
热门榜冷启动接口42ms
有缓存推荐接口65ms
无缓存,重新计算推荐350ms
离线计算相似度矩阵约4秒

从这个数据可以明显看出,加了Redis缓存之后接口性能提升显著。无缓存情况下虽然350ms对于本地实验也能接受,但如果是生产环境,同时几百个请求打过来,每次350ms的CPU计算会让服务很快扛不住。缓存设计不是锦上添花,而是推荐系统架构里必不可少的一环。

5. 常见问题与排查技巧实录

5.1 相似度矩阵全是0,推荐结果空白

这是我在第一次实现时踩过的坑,而且我相信很多人都会踩。排查出来的原因其实很简单:电影相似度矩阵按余弦相似度计算,如果两个电影之间没有任何共同用户,也就是它们没有出现在任何同一个用户的评分列表里,余弦相似度就是0。数据稀疏度越高,这个情况越普遍。

解决办法有几个层次。第一层,相似度计算前做去均值处理,这能缓解一部分尺度不一致的问题,但解决不了共现为空的问题。第二层,计算TopK邻居时,不要只取相似度大于0的,可以把阈值放宽到"即使为0,只要出现在候选列表里也纳入",但这样意义不大。第三层,换相似度计算方式,比如用皮尔逊相关系数或者杰卡德相似系数,在一定场景下效果更好。第四层,也是最实用的,就是我在前面提到的类型兜底推荐——当协同过滤的候选电影为空时,按用户已看过的电影类型做二次筛选,保证前端永远有东西可展示。

我最终的方案是协同过滤为主、类型兜底为辅的混合推荐策略。这个方案不追求纯算法上的完美,但保证了产品体验的稳健。

5.2 推荐结果全是热门电影,个性化不足

另一个高频问题:推荐列表里出现的总是《肖申克的救赎》《霸王别姬》这种高分热门电影。这其实是流行度偏差在作祟。热门电影用户评分数量大,共现次数多,天然有更高的推荐分数,冷门电影即使特征匹配度更高也排不上号。

解决办法是给推荐分数加入一个惩罚项。我在实践中用的最简单有效的办法是:在归一化时对所有候选电影乘以一个"热度惩罚系数",评分人数越少的电影,系数越接近1;评分人数越多的电影,系数越小。这样能明显提升推荐列表的多样性。你也可以去看一些更系统的方案,比如TF-IDF思想里的IDF项,但学生项目里一个简单的惩罚系数已经够用了。

5.3 Django ORM查询引发的性能问题

项目里有几处高频查询隐藏着N+1问题。最典型的是推荐接口在组装返回数据时,每条推荐电影都要查一次Movie表,看似没几个查询,实际可能触发几十次SQL。我第一次做性能测试时就被这个坑拖慢了。

解决方式很简单,用select_related和prefetch_related解决:

# 优化前:每条推荐都要单独查询电影 # 优化后:一次查出所有推荐电影信息 recommendations = Movie.objects.filter(id__in=candidate_ids).prefetch_related("rating_set")

另外一个容易忽略的点是delete操作。在Django里执行Model.objects.filter(...).delete()时,如果模型存在关联的外键或者信号处理器,删除操作可能会比你预期的慢很多。我在清空旧的推荐缓存表时遇到过这个问题,后来改成在Redis里按过期时间自动清理,数据库只保留最新一份结果,避免频繁大表删除操作。

5.4 缓存过期与数据一致性

推荐结果缓存在Redis里,过期时间设置多久合适?设太短,离线任务还没跑完缓存就失效了,接口又会打回原形变慢;设太长,用户刚打了一部电影的新评分,推荐列表却迟迟不更新,体验不好。我用的方案是分类型处理:相似度矩阵的缓存时间设一天,推荐列表的缓存时间设两小时。用户产生新的评分行为时,主动删除该用户的推荐缓存,下次请求时重新生成。这算是一种简单的缓存失效策略,能有效缓解"评完分推荐一点不变"的尴尬。

还有一个一致性相关的细节:离线相似度任务执行期间,Redis里的相似度数据可能正在被写入,此时有并发请求过来可能读到半更新状态。由于我们用的数据结构是整矩阵序列化和键值覆盖写的,不会有明显的脏数据问题,但如果你的实现是按条目逐步写入的,那就要考虑用版本号或者双缓冲的思路来保证一致性。我在这个阶段没有遇到严重问题,但多线程读写共享数据时确实值得注意。

5.5 推荐接口返回字段协商

前端往往希望一次拿到足够展示的数据,但你又不想把所有字段都暴露出去。这里我踩过一个小坑:一开始接口直接返回了Movie的model转字典,里面包含了description大段文本、cover_url等字段,传输体积大且前端不一定需要。后来我改成在service层显式组装返回字段,只保留id、title、year、genres、avg_rating、reason这六个字段,接口体积直接缩小了七八成。

关于接口的健壮性还有一个点。当用户已经把所有近期可见的电影都评过分了,推荐接口会出现候选列表为空的情况。我在接口里加了一个判断:如果最终候选为空,降级返回热门榜并附上一个"探索新片"的提示。这个降级策略虽然朴素,但保证了接口在任何输入下都不会报错,对于课程设计和答辩演示来说非常加分。


我做完这个项目最大的感受是:推荐系统听起来高大上,但落地时的瓶颈往往不在算法本身,而在数据处理的工程细节。协同过滤总共就几行核心代码,花一天就能写通,真正花时间的反而是数据清洗、缓存设计、接口优化、异常分支处理这些看起来不起眼的部分。这也是为什么我建议你用Django来做——它的ORM和自带能力能帮你把工程侧的复杂度兜住,让你把精力集中在真正有价值的算法和数据设计上。如果你也想做类似的项目,我建议你先把MovieLens数据集跑通全流程,再换成自己的数据源,你会明显感觉到一套工程逻辑是可以复用的。做完了协同过滤,还可以继续往矩阵分解、冷启动优化、混合推荐这些方向一点点加码,每一次扩展都会让你对推荐系统的理解更深一层。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询