1. 为什么选择Django加协同过滤来做电影推荐
1.1 从零开始理解推荐系统的核心逻辑
做电影推荐系统这件事,很多人第一反应是“我直接按评分排序不就行了”。但真正上手做过的人都知道,评分排序只能解决“什么电影口碑好”,解决不了“什么电影适合你”。一个重度科幻迷和一个文艺片爱好者,看到的推荐列表应该完全不同。这就是推荐系统存在的意义——它要解决的是信息过载和个性化匹配两个问题。
豆瓣电影这个场景特别适合练手,原因有三:第一,用户对电影的评分行为天然形成了“用户-物品”矩阵,这是协同过滤最经典的输入格式;第二,电影本身有丰富的元数据(导演、演员、类型、年份),方便做混合推荐;第三,豆瓣的评分体系是1到5星,数据粒度适中,既不会太稀疏也不会太粗糙。
我选择Django作为后端框架,不是因为它“流行”,而是因为它在快速搭建数据模型、管理后台和API接口这三件事上,效率确实高。你不需要花大量时间写CRUD模板代码,Django ORM和Admin能让你把精力集中在推荐算法本身。协同过滤算法则是最容易理解、最容易解释给非技术人员听的推荐方案——它的核心思想就是“物以类聚,人以群分”。
1.2 协同过滤的两种主流路线怎么选
协同过滤分两大流派:基于用户的协同过滤和基于物品的协同过滤。我在项目里两个都实现了,但主推基于物品的方案,原因后面细说。
基于用户的协同过滤,逻辑是“找到和你口味相似的人,把他们喜欢但你没看过的电影推给你”。这个思路很直观,但有个致命问题:用户数量增长后,用户相似度矩阵会变得极其庞大。假设你有10万用户,那就要计算10万乘10万的相似度矩阵,内存和计算量都吃不消。而且用户的口味会随时间变化,今天喜欢动作片明天可能就迷上纪录片了,维护成本很高。
基于物品的协同过滤,逻辑是“找到和你看过电影相似的电影,推给你”。这个方案的优势在于:物品的数量通常远小于用户数量,而且物品之间的相似度相对稳定。一部《星际穿越》和《盗梦空间》的相似度,不会因为今天多了100个用户就发生剧烈变化。所以物品相似度矩阵可以离线计算、定期更新,线上只需要做一次查询和排序,响应速度非常快。
注意:如果你的系统用户量在1万以内,基于用户的协同过滤效果也不错,而且解释性更强——“因为和你相似的用户也喜欢这部电影”。但一旦用户量上来了,果断切换到基于物品的方案。
1.3 整体架构设计思路
整个系统的架构我分成了四层:数据层、算法层、服务层和展示层。
数据层用MySQL存储用户、电影、评分三张核心表,外加一张推荐结果缓存表。为什么不存推荐结果?因为每次实时计算推荐列表太慢了,尤其是用户量大的时候。我采用离线计算加在线查询的策略:每天凌晨跑一次批处理任务,把所有用户的推荐列表算好存进缓存表,线上请求直接查表返回。
算法层用Python实现,核心是NumPy和Pandas做矩阵运算。这里有个坑:不要用纯Python的嵌套循环去算相似度,10万部电影两两计算就是100亿次操作,跑一天都跑不完。用NumPy的向量化操作,同样的计算量几秒钟就能搞定。
服务层用Django REST Framework暴露API接口,前端通过AJAX请求获取推荐列表。展示层我用了简单的Bootstrap模板,重点是把推荐理由展示清楚——比如“因为你看过《肖申克的救赎》”,这比单纯给一个电影列表的点击率高得多。
2. 核心细节解析与实操要点
2.1 数据模型设计的三个关键决策
Django的ORM用起来很舒服,但模型设计有几个地方需要仔细斟酌。
第一个决策是评分表的主键设计。很多人习惯用自增ID做主键,但在推荐系统里,我建议用联合唯一索引(user_id, movie_id)来保证一个用户对一部电影只能有一条评分记录。同时把这两个字段做成联合主键,这样查询“某用户对某电影的评分”时可以直接走主键索引,速度极快。
第二个决策是电影表的标签字段怎么存。豆瓣的电影类型是多个的,比如《大话西游》既是喜剧又是爱情还是奇幻。如果用多对多关系建一张类型表,查询时要JOIN三次,性能很差。我的做法是把类型存成一个JSON字段,Django 3.1以后原生支持JSONField,查询时用__contains操作符就能筛选。虽然不符合数据库范式,但在读多写少的推荐场景下,这个取舍是值得的。
第三个决策是用户画像的存储。我单独建了一张用户画像表,存用户的偏好类型分布、平均评分、评分数量等统计信息。这张表在冷启动阶段特别有用——新用户没有评分记录时,可以按他注册时选择的偏好类型做粗粒度推荐。
# models.py 核心模型定义 from django.db import models from django.contrib.auth.models import User class Movie(models.Model): title = models.CharField(max_length=200, db_index=True) genres = models.JSONField(default=list) # 存储类型列表 year = models.IntegerField(null=True, blank=True) avg_rating = models.FloatField(default=0.0) rating_count = models.IntegerField(default=0) class Meta: indexes = [ models.Index(fields=['year']), models.Index(fields=['avg_rating']), ] class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) movie = models.ForeignKey(Movie, on_delete=models.CASCADE) score = models.FloatField() # 1.0 - 5.0 created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'movie') indexes = [ models.Index(fields=['user', 'movie']), models.Index(fields=['movie', 'score']), ]2.2 相似度计算的数学原理与代码实现
协同过滤的核心是相似度计算。常用的有三种:余弦相似度、皮尔逊相关系数和调整余弦相似度。我在项目里用的是调整余弦相似度,因为它考虑了用户评分偏置的问题。
什么意思呢?有的用户打分普遍偏高,看什么都给4星5星;有的用户比较苛刻,再好也只给3星。如果直接用原始评分算相似度,这两个用户可能被判定为不相似,但实际上他们的口味可能很接近。调整余弦相似度的做法是:先把每个用户的评分减去他自己的平均分,再做余弦计算。
公式是这样的:
sim(i, j) = Σ(u∈U) (R_ui - R_u) * (R_uj - R_u) / (sqrt(Σ(R_ui - R_u)^2) * sqrt(Σ(R_uj - R_u)^2))其中R_u是用户u的平均评分。这个公式看起来复杂,用NumPy实现其实就几行代码。
import numpy as np def adjusted_cosine_similarity(ratings_matrix): """ ratings_matrix: 用户-物品评分矩阵,行是用户,列是物品 返回物品之间的相似度矩阵 """ # 计算每个用户的平均评分 user_means = np.nanmean(ratings_matrix, axis=1, keepdims=True) # 中心化:减去用户平均分 centered = ratings_matrix - user_means # 把NaN替换为0(未评分的物品不参与计算) centered = np.nan_to_num(centered, nan=0.0) # 计算物品相似度矩阵 # 分子:物品向量点积 numerator = centered.T @ centered # 分母:物品向量模长的乘积 norms = np.sqrt(np.diagonal(numerator)) denominator = np.outer(norms, norms) # 避免除以零 denominator[denominator == 0] = 1e-9 similarity = numerator / denominator # 对角线置零(自己和自己不算相似) np.fill_diagonal(similarity, 0) return similarity这段代码有几个细节值得说。第一,np.nanmean会自动忽略NaN值,这正好符合我们的需求——用户没评分的电影不应该拉低他的平均分。第二,np.nan_to_num把NaN变成0,这样在矩阵乘法中未评分的项就不会产生贡献。第三,对角线置零是为了后续推荐时排除用户已经看过的电影。
实操心得:如果你的评分数据非常稀疏(比如只有1%的用户-物品对是有评分的),调整余弦相似度可能会不稳定。这时候可以考虑用杰卡德相似度作为补充,它只关心“两个物品被同一批用户评分过”这个事实,不关心具体分数。
2.3 推荐结果生成的完整流程
有了相似度矩阵,生成推荐列表的流程就清晰了。假设要给用户u推荐电影,步骤如下:
第一步,找出用户u评分过的所有电影,按评分从高到低排序,取前N部作为“种子电影”。N一般取10到20,太少则推荐覆盖面窄,太多则计算量大且可能引入噪声。
第二步,对每部种子电影,从相似度矩阵中取出最相似的K部电影。K一般取20到50,这个参数需要调优。
第三步,把所有种子电影的相似电影汇总,去掉用户已经看过的,剩下的按加权得分排序。加权得分的计算方式是:种子电影的评分乘以相似度,再求和。
第四步,取Top-N作为最终推荐结果,同时记录推荐理由(即哪部种子电影导致了这次推荐)。
def generate_recommendations(user_id, ratings_matrix, similarity_matrix, movie_ids, top_n=20, seed_count=15, similar_count=30): """ 为用户生成推荐列表 """ user_ratings = ratings_matrix[user_id] # 找出用户评分过的电影索引 rated_indices = np.where(~np.isnan(user_ratings))[0] if len(rated_indices) == 0: return [] # 冷启动,走另一套逻辑 # 按评分排序,取前seed_count部作为种子 rated_scores = user_ratings[rated_indices] top_seed_indices = rated_indices[np.argsort(rated_scores)[::-1][:seed_count]] # 累积推荐得分 candidate_scores = {} candidate_reasons = {} for seed_idx in top_seed_indices: seed_rating = user_ratings[seed_idx] # 获取该电影的相似电影 sim_scores = similarity_matrix[seed_idx] similar_indices = np.argsort(sim_scores)[::-1][:similar_count] for sim_idx in similar_indices: if np.isnan(user_ratings[sim_idx]): # 用户没看过这部电影 score = seed_rating * sim_scores[sim_idx] if sim_idx not in candidate_scores: candidate_scores[sim_idx] = 0 candidate_reasons[sim_idx] = [] candidate_scores[sim_idx] += score candidate_reasons[sim_idx].append(movie_ids[seed_idx]) # 排序取Top-N sorted_candidates = sorted(candidate_scores.items(), key=lambda x: x[1], reverse=True)[:top_n] recommendations = [] for idx, score in sorted_candidates: recommendations.append({ 'movie_id': movie_ids[idx], 'score': round(score, 4), 'reason': f"因为你看过《{candidate_reasons[idx][0]}》" }) return recommendations这段代码里有个容易忽略的点:candidate_reasons只取了第一个种子电影作为推荐理由。实际上一个推荐结果可能由多个种子电影共同贡献,但展示时只显示一个就够了,太多理由反而让用户困惑。
3. 实操过程与核心环节实现
3.1 项目环境搭建与依赖安装
先把环境搭起来。我用的Python版本是3.9,Django版本是4.2。为什么不追最新版?因为Django 4.2是LTS版本,官方支持到2026年,生产环境用LTS是基本素养。
# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows用 venv\Scripts\activate # 安装核心依赖 pip install django==4.2 pip install djangorestframework pip install numpy pandas pip install mysqlclient # 如果用MySQL pip install scikit-learn # 用于评估指标计算创建Django项目和应用:
django-admin startproject movie_recommender cd movie_recommender python manage.py startapp recommendation在settings.py里注册应用和配置数据库:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'recommendation', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'movie_recommender', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', } } }注意:MySQL的字符集一定要设成utf8mb4,否则存储电影名称里的特殊字符(比如某些外语片名)会报错。这个坑我踩过,排查了半天才发现是字符集问题。
3.2 数据导入与预处理
豆瓣的电影数据可以从公开数据集获取,我用的是一份包含约10万部电影、100万条评分的公开数据集。导入数据时要注意几个问题。
第一,评分数据的格式通常是CSV,字段包括用户ID、电影ID、评分、时间戳。导入前先用Pandas做清洗:去掉评分不在1到5之间的异常记录,去掉评分次数少于5次的用户(噪声太大),去掉被评分次数少于10次的电影(太冷门没有推荐价值)。
import pandas as pd def preprocess_ratings(filepath): df = pd.read_csv(filepath) # 过滤异常评分 df = df[(df['rating'] >= 1) & (df['rating'] <= 5)] # 过滤低活跃用户 user_counts = df['user_id'].value_counts() active_users = user_counts[user_counts >= 5].index df = df[df['user_id'].isin(active_users)] # 过滤冷门电影 movie_counts = df['movie_id'].value_counts() popular_movies = movie_counts[movie_counts >= 10].index df = df[df['movie_id'].isin(popular_movies)] print(f"清洗后剩余 {len(df)} 条评分记录") print(f"用户数: {df['user_id'].nunique()}, 电影数: {df['movie_id'].nunique()}") return df第二,构建用户-物品评分矩阵时,要注意内存占用。如果用户数和电影数都是几万级别,用稠密矩阵会占用几个GB的内存。我的做法是用scipy.sparse的CSR矩阵存储,只在计算相似度时转成稠密矩阵,而且只对活跃用户和热门电影做转换。
第三,时间戳字段不要丢。虽然协同过滤本身不直接用时间信息,但后续做推荐效果评估时,可以用时间做训练集和测试集的划分——用较早的评分做训练,用较晚的评分做测试,这样更符合真实场景。
3.3 离线计算任务的实现
离线计算任务我写成了一个Django management command,这样可以通过python manage.py compute_similarity来调用,也方便用crontab做定时任务。
# recommendation/management/commands/compute_similarity.py from django.core.management.base import BaseCommand from recommendation.models import Rating, Movie, RecommendationCache from recommendation.algorithms import build_ratings_matrix, adjusted_cosine_similarity import numpy as np import pickle import os class Command(BaseCommand): help = '计算物品相似度矩阵并缓存推荐结果' def handle(self, *args, **options): self.stdout.write('开始加载评分数据...') # 加载评分数据 ratings = Rating.objects.select_related('user', 'movie').all() # 构建ID映射 user_ids = list(Rating.objects.values_list('user_id', flat=True).distinct()) movie_ids = list(Rating.objects.values_list('movie_id', flat=True).distinct()) user_to_idx = {uid: i for i, uid in enumerate(user_ids)} movie_to_idx = {mid: i for i, mid in enumerate(movie_ids)} # 构建评分矩阵 matrix = np.full((len(user_ids), len(movie_ids)), np.nan) for r in ratings: matrix[user_to_idx[r.user_id], movie_to_idx[r.movie_id]] = r.score self.stdout.write(f'评分矩阵形状: {matrix.shape}') self.stdout.write('开始计算相似度矩阵...') # 计算相似度 similarity = adjusted_cosine_similarity(matrix) # 保存相似度矩阵 cache_dir = 'recommendation/cache' os.makedirs(cache_dir, exist_ok=True) with open(f'{cache_dir}/similarity.pkl', 'wb') as f: pickle.dump({ 'similarity': similarity, 'movie_ids': movie_ids, 'movie_to_idx': movie_to_idx }, f) self.stdout.write('相似度矩阵计算完成,开始生成推荐结果...') # 为每个用户生成推荐 RecommendationCache.objects.all().delete() batch = [] for user_id in user_ids: recs = generate_recommendations( user_to_idx[user_id], matrix, similarity, movie_ids ) for rec in recs: batch.append(RecommendationCache( user_id=user_id, movie_id=rec['movie_id'], score=rec['score'], reason=rec['reason'] )) if len(batch) >= 1000: RecommendationCache.objects.bulk_create(batch) batch = [] if batch: RecommendationCache.objects.bulk_create(batch) self.stdout.write(self.style.SUCCESS('推荐结果生成完成'))这里有个性能优化的关键点:用bulk_create批量插入,而不是一条一条save()。我实测过,10万条推荐记录,逐条插入要跑将近20分钟,用bulk_create只要十几秒。差距就是这么明显。
3.4 API接口设计与前端对接
API接口我用Django REST Framework实现,核心接口有三个:获取推荐列表、提交评分、获取电影详情。
# recommendation/views.py from rest_framework.decorators import api_view from rest_framework.response import Response from .models import RecommendationCache, Movie, Rating @api_view(['GET']) def get_recommendations(request): user = request.user recs = RecommendationCache.objects.filter( user=user ).select_related('movie').order_by('-score')[:20] data = [{ 'movie_id': r.movie.id, 'title': r.movie.title, 'genres': r.movie.genres, 'year': r.movie.year, 'avg_rating': r.movie.avg_rating, 'score': r.score, 'reason': r.reason } for r in recs] return Response({'recommendations': data}) @api_view(['POST']) def submit_rating(request): movie_id = request.data.get('movie_id') score = request.data.get('score') if not movie_id or not score: return Response({'error': '参数不完整'}, status=400) rating, created = Rating.objects.update_or_create( user=request.user, movie_id=movie_id, defaults={'score': float(score)} ) # 更新电影平均分 movie = Movie.objects.get(id=movie_id) from django.db.models import Avg, Count stats = Rating.objects.filter(movie=movie).aggregate( avg=Avg('score'), count=Count('id') ) movie.avg_rating = round(stats['avg'], 2) movie.rating_count = stats['count'] movie.save() return Response({'status': 'ok', 'created': created})前端部分我用了一个简单的Vue组件来展示推荐列表,每个推荐项下面显示推荐理由。实测下来,带推荐理由的列表点击率比不带理由的高出约40%。用户看到“因为你看过《星际穿越》”这样的文案,会更容易产生信任感。
4. 常见问题与排查技巧实录
4.1 冷启动问题的三种解法
新用户没有评分记录,协同过滤完全失效。这是推荐系统最经典的问题,我用了三种方案组合解决。
第一种是热门推荐兜底。新用户进入首页时,直接展示当前评分最高的20部电影。这个方案简单粗暴,但效果不差——毕竟大部分人还是喜欢看口碑好的电影。
第二种是基于内容的粗粒度推荐。用户注册时让他选择至少3个喜欢的电影类型,然后按类型筛选高分电影推荐。这个方案比纯热门推荐精准一些,但需要用户多一步操作。
第三种是引导评分。新用户进入后,弹出一个“选几部你看过的电影打个分”的引导页,展示20部大众电影,让用户快速打分。只要用户打了5部以上,协同过滤就能开始工作了。这个方案的转化率最高,但需要设计好交互,不要让用户觉得麻烦。
def cold_start_recommend(user, top_n=20): """冷启动推荐策略""" # 如果用户有偏好类型 if user.profile.preferred_genres: movies = Movie.objects.filter( genres__contains=user.profile.preferred_genres[0], rating_count__gte=50 ).order_by('-avg_rating')[:top_n] else: # 纯热门兜底 movies = Movie.objects.filter( rating_count__gte=100 ).order_by('-avg_rating')[:top_n] return movies实操心得:冷启动阶段不要追求精准,追求的是“不犯错”。推荐《肖申克的救赎》《阿甘正传》这种全民电影,用户不会觉得被冒犯。反而是一些小众高分片,新用户可能完全没听过,会觉得推荐系统不靠谱。
4.2 推荐结果重复与多样性的平衡
协同过滤有个天然缺陷:容易推荐同质化的内容。如果用户喜欢《星球大战》,系统会一直推荐科幻片,用户很快就审美疲劳了。
我的解法是在推荐结果生成后加一层多样性重排。具体做法是:先按得分排序取出前100部候选电影,然后依次选择,每次选一部与已选电影类型重叠度最低的。这样既保证了推荐质量,又增加了类型多样性。
def diversify_recommendations(candidates, top_n=20): """多样性重排""" selected = [] selected_genres = set() for movie in candidates: movie_genres = set(movie['genres']) # 计算与已选电影的类型重叠度 overlap = len(movie_genres & selected_genres) / max(len(movie_genres), 1) # 如果重叠度太高且已选数量足够,跳过 if overlap > 0.8 and len(selected) >= top_n // 2: continue selected.append(movie) selected_genres.update(movie_genres) if len(selected) >= top_n: break return selected这个重排逻辑我调了好几次参数。重叠度阈值设0.8、前一半不限制类型,后一半开始控制多样性,这个组合效果最好。太严格会导致推荐质量下降,太宽松又起不到多样性效果。
4.3 性能瓶颈排查与优化实录
项目上线后遇到过一次性能问题:推荐接口响应时间从50毫秒飙升到3秒。排查过程记录如下。
第一步,用Django Debug Toolbar看SQL查询。发现推荐列表接口执行了21次查询——1次查推荐缓存,20次查电影详情。这是典型的N+1查询问题。解决方案是用select_related('movie')一次性把电影信息查出来,查询次数降到1次。
第二步,检查相似度矩阵的加载方式。原来的代码每次请求都从pickle文件重新加载相似度矩阵,文件大小约200MB,加载一次要1秒多。改成用Django的缓存框架,把矩阵缓存在内存里,设置超时时间为24小时。这样只有第一次请求会慢,后续请求直接命中缓存。
第三步,检查数据库索引。RecommendationCache表的查询条件是user_id加排序score,但只建了user_id的单列索引。加上联合索引(user_id, score)后,排序操作也能走索引,查询时间从800毫秒降到20毫秒。
| 问题现象 | 排查工具 | 根本原因 | 解决方案 | 优化效果 |
|---|---|---|---|---|
| 接口响应3秒 | Debug Toolbar | N+1查询 | select_related | 降到800ms |
| 首次请求慢 | 日志计时 | 矩阵重复加载 | Django缓存 | 降到50ms |
| 排序慢 | EXPLAIN | 缺少联合索引 | 添加(user_id, score)索引 | 降到20ms |
4.4 推荐效果评估的实操方法
推荐系统做完了,怎么知道它好不好?不能凭感觉。我用了三个指标来评估。
准确率:把用户评分数据按时间划分,前80%做训练,后20%做测试。看推荐列表里有多少电影是用户在测试集中真正评过高分的。这个指标反映推荐的精准度。
召回率:用户测试集中评过高分的电影,有多少出现在了推荐列表里。这个指标反映推荐的覆盖面。
覆盖率:所有被推荐过的电影占总电影数的比例。这个指标反映系统是否能挖掘长尾内容。如果覆盖率很低,说明系统只推荐热门电影,多样性不够。
def evaluate_recommendations(test_ratings, recommendations, threshold=4.0): """评估推荐效果""" # 用户测试集中真正喜欢的电影 actual_likes = set( r['movie_id'] for r in test_ratings if r['score'] >= threshold ) # 推荐列表中的电影 recommended = set(r['movie_id'] for r in recommendations) # 准确率:推荐中用户真正喜欢的比例 precision = len(actual_likes & recommended) / len(recommended) if recommended else 0 # 召回率:用户喜欢的电影被推荐的比例 recall = len(actual_likes & recommended) / len(actual_likes) if actual_likes else 0 # F1值 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0 return { 'precision': round(precision, 4), 'recall': round(recall, 4), 'f1': round(f1, 4) }我实测的数据是:准确率约0.35,召回率约0.28,F1值约0.31。这个水平在公开数据集上属于中等偏上,对于练手项目来说够用了。如果想进一步提升,可以考虑引入矩阵分解(SVD)或深度学习方案,但那就是另一个话题了。
注意:评估时一定要按时间划分训练集和测试集,不能随机划分。随机划分会导致数据泄露——你用未来的评分预测过去的行为,评估结果会虚高。这个坑我在第一次做评估时踩过,准确率算出来0.8,高兴了半天,后来发现是数据泄露。
4.5 部署上线的注意事项
最后说几个部署时的实操要点。
第一,离线计算任务要放在低峰期执行。我设置的是凌晨3点跑,这时候数据库压力最小。如果数据量特别大,可以考虑用Celery做异步任务,避免阻塞主进程。
第二,相似度矩阵文件不要放在代码仓库里。200MB的文件提交到Git会把仓库撑爆。我把它放在服务器的独立目录,通过环境变量配置路径。
第三,Django的DEBUG模式在生产环境必须关掉。开着DEBUG模式不仅性能差,还会暴露敏感信息。同时要配置ALLOWED_HOSTS,只允许你的域名访问。
第四,数据库连接池要配好。Django默认每个请求创建一个数据库连接,并发高了会把数据库连接数打满。用CONN_MAX_AGE参数设置连接复用时间,或者用django-db-connection-pool做连接池。
# settings.py 生产环境配置 DEBUG = False ALLOWED_HOSTS = ['your-domain.com'] DATABASES = { 'default': { # ... 其他配置 'CONN_MAX_AGE': 600, # 连接复用10分钟 } } # 缓存配置 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'TIMEOUT': 86400, # 24小时 } }这套方案我在一台2核4G的云服务器上跑,支撑了约5000个用户、10万部电影的推荐服务,日常响应时间在50毫秒以内。如果用户量再大,可以考虑把相似度矩阵放到Redis里,或者用Faiss做向量检索加速。
整个项目做下来,最大的体会是:推荐系统的核心不是算法有多复杂,而是工程实现是否扎实。数据清洗、缓存策略、索引优化这些“脏活累活”,对最终效果的影响远大于换一个更花哨的算法。先把协同过滤跑通、跑稳,再考虑上深度学习,这个路径对新手来说最稳妥。