☰
基于Django的短视频推荐系统后端:从协同过滤到向量召回实战
2026/10/10 4:24:40 网站建设 项目流程

1. 从零搭起推荐系统后端:为什么我选了Django而不是其他框架

做短视频推荐系统,很多人第一反应是上Spring Cloud那一套微服务,或者直接拿Python的FastAPI写几个接口完事。但我这次的项目偏偏选了Django,而且是在已经预估到推荐模块计算量不小、并发压力也存在的前提下做的选择。原因不复杂:Django自带的那套全家桶,在一个人或者一个小团队从零开始做完整系统时,能省下来的时间远比想象中多。

先别急着争论框架优劣,我实际对比过。FastAPI的异步性能和轻量确实有优势,但问题在于:短视频推荐系统不只是推荐算法本身,它还需要管理用户体系、视频资源、行为日志、后台审核、数据看板,总要有一个能把这些东西统一管起来的底座。Django的ORM、Admin后台、认证系统、迁移工具,这些开箱即用的能力在这个场景下非常匹配。我一个人从数据库表设计到接口联调,再到后台管理,不需要额外引入太多第三方组件。

另外,Django的ORM在建模用户-视频-行为这对关系上相当顺手。推荐系统里最核心的动作就是记录用户看了什么、给什么点过赞、在哪个视频上停留了很久,这些天然就是多对多的关系模型。用Django的模型继承和ForeignKey直接搭好表结构,后续写推荐逻辑时不必反复写SQL拼接,逻辑顺序要清晰得多。

再谈到项目的资料配套。这套系统最终要交源码、论文和答辩PPT,技术上选Django还有一个隐性好处:它的项目结构本身就是一种可讲解的架构范式。模型、视图、服务、任务、配置分得清清楚楚,写论文时画架构图、描述模块划分非常顺畅,Manager和Service层可以一一对应。如果选了FreeMarker或模板渲染逻辑和业务逻辑混在一起,论文里怎么写清楚模块划分都会麻烦。

提示:实际的系统开发中,框架选型取决于项目规模和团队构成。一个人开发完整系统,Django的电池齐全优势大于性能损耗;如果是大团队高并发项目,当然有更好的选择。我这里说的是"从零到完整上线"的场景。

2. 推荐的本质是匹配问题:数据模型与特征设计

2.1 核心模型:用户、视频、行为日志的建模思路

短视频推荐系统的底层数据结构,绕不开三张核心表:用户表、视频资源表、用户行为表。但我真正踩过坑的地方在于行为日志表的设计——很多初学者把它当成简单的操作记录,放几个字段就完事了,结果后面做特征提取时发现数据根本不够用。

我采用的设计是保留一份完整的行为日志明细表,核心字段包括:用户ID、视频ID、行为类型(曝光、观看、点赞、收藏、分享、不喜欢)、行为时间、观看时长、观看时长占视频总时长比例、行为来源(推荐流、搜索、关注页)。后面几个字段尤其关键。举例来说,光记录"用户看了视频A"没有用,要判断"用户是否真的喜欢视频A",更重要的是他看了多久、看到什么时候划走的、是不是推荐流给的曝光还是用户自己搜出来的。

视频资源表的字段也不能只存标题和地址。我额外做了视频时长、封面图、分类标签(多对多)、上传时间、作者信息、基础热度值、平均完播率缓存字段。这些字段在推荐排序时会直接参与特征计算,后期配合定时任务定期刷新。

用户表除了常规注册信息,我加了一个用户兴趣向量快照字段,用的是JSON格式存储。这个字段看起来冗余,但作用很大:用户在每次登录、每次刷新推荐流时可以直接读取上次计算好的兴趣向量,不必每次临时跑一遍完整的历史行为统计。具体的兴趣向量更新由异步任务完成,避免推荐接口同步算特征。

设计原则补充:在行为日志表上一定要做索引。我最初没加索引,结果行为量到几万条时查询就开始拖慢整体响应,后来在user_id和video_id上加了联合索引,性能问题立刻缓解。

2.2 特征工程:不是所有行为都值得平等对待

推荐系统的数据模型建好了,接下来才是重头戏:怎么把原始行为日志变成能参与计算的用户特征和视频特征。

我在特征处理上做的一个关键决定是行为加权分桶。不同类型的用户操作代表不同的兴趣强度,直接全部平铺处理会导致推荐结果失真。实际权重我大致设置如下:

行为类型权重说明
曝光0.1系统推了但用户没反应,弱正反馈
完整观看1.0用户看完了,强正反馈
点赞2.0主动表达喜欢
收藏3.0强烈兴趣,有重复消费意图
分享3.5愿意把视频扩散给他人
不喜欢-5.0明确负反馈,需要降权处理

观看时长的处理也比较特殊,不是直接拿秒数用,而是先算完播比(观看时长除以视频总时长),再映射到0到1区间的分数。例如一个3分钟的视频看了1分钟,和30秒的短视频看了1分钟,参照系完全不同。映射函数我用了分段线性映射,并做了饱和截断,长视频看一半比短视频看一半往往更能说明用户对内容有耐心。

用户特征最终汇总成三个维度的向量:兴趣类别分布、活跃时段偏好、内容消费深度。这里的"内容消费深度"是我自己加的维度,用来衡量一个用户倾向于看热门爆款还是长尾小众内容,后续做召回策略选择时会用到。这些特征都定期通过Celery异步任务从行为日志中重新统计,再写回用户画像表。

2.3 冷启动问题的处理

新用户没有历史行为数据,推荐系统最容易陷入"没法猜测喜好"的尴尬。我做了三个层次的冷启动策略:

第一,注册时选兴趣标签。用户注册时选择3-5个感兴趣的分类标签,系统根据标签直接生成初始兴趣向量。这部分向量参与召回但不参与精排,避免初始偏差被过度放大。

第二,热门内容兜底。新用户的推荐流前N条以全局热门为主,穿插少量分类热门。原理上就是利用群体行为缓解个体数据缺失。

第三,快速试探机制。新用户的前几次刷新,系统会刻意混入多个不同类别的内容,通过点击和观看时长反馈迅速修正兴趣向量。这个策略在推荐系统里叫exploration,缺点是短期可能让用户觉得"推得不准",但长期收益显著。

经验:新用户的推荐效果评估不能看单次会话,要看注册后一周内的次日留存和人均观看时长有没有提升。用这两个指标来调试探机制的力度,比主观感觉要靠谱得多。

3. 从协同过滤到向量召回:推荐算法在Django中的落地细节

3.1 两阶段推荐架构:召回与精排

任何推荐系统在线上运行,都要考虑一个现实问题:全量计算用户对所有视频的兴趣分数,在线场景下根本做不到。所以业界通用做法是拆成召回和精排两个阶段。我在这个项目里也是按照这个架构来实现的。

召回阶段的目标是从海量视频库中快速筛出几百个候选视频。这阶段不在乎精度,只在乎速度与覆盖率,策略可以叠加多个:基于用户的协同过滤(找相似用户看过的东西)、基于物品的协同过滤(找相似视频)、热度召回、分类偏好召回、新鲜内容召回等。

精排阶段的目标是对这几百个候选视频精算分数并排序输出。这阶段我用了加权特征融合模型,对视频的多个特征维度(热度、时效性、与用户兴趣的匹配度、行为预测分数)做归一化后线性加权,输出最终排序分数。

召回和精排分开的意义在于:召回可以用更重的算法但离线完成,精排在线上只做轻量计算,两者相互配合既保证效果也保证响应速度。这也是当时答辩时被追问得最多的设计,讲清楚"为什么要拆开"基本就等于把推荐系统的工程思维展示明白了。

3.2 协同过滤算法的Django实现

我在项目里先落地了**基于物品的协同过滤(ItemCF)**作为基础推荐算法,原因很简单:短视频场景里用户量远大于视频量,物品之间的相似度计算和维护成本更低。

ItemCF的思路并不复杂:如果用户A和用户B都对视频X表现过正反馈,而视频Y也被用户A喜欢过,那么视频Y和视频X之间存在"相似性",可以推荐给用户B。实际实现时,算法分这么几步:

第一步,从行为日志中筛出所有正反馈行为(点赞、收藏、完整观看、分享)。第二步,构建"用户-视频"倒排表,即每个用户对应一份他交互过的视频ID列表。第三步,计算视频两两之间的共现矩阵:视频被同一个用户喜欢过的次数越多,相似度越高。第四步,为每个视频取TopN相似视频,存为离线结果表。

最后一步很关键:候选集表不能每次实时计算。我做法是每天凌晨用Celery定时任务全量跑一遍,把"视频ID -> 相似视频TopN(含相似度分数)"存进一张Redis或数据库表。线上推荐时,找到用户最近正反馈的视频,直接从这个表里捞候选集。

这里面有个细节容易被忽略:计算视频相似度要不要做惩罚?不做惩罚的话,热门视频和所有视频的相似度都会虚高,导致推荐结果趋同。我引入了热门惩罚系数,共现次数除以视频流行度的对数,可以在一定程度上缓解热门物品过度中心化的问题。

# 简化版ItemCF核心计算逻辑 def build_item_similarity(interaction_dict): """interaction_dict: {user_id: [video_id, ...]}""" co_occur_count = {} # (video_a, video_b) -> 共现次数 video_popularity = {} # video_id -> 被交互用户数 for user, videos in interaction_dict.items(): for v in videos: video_popularity[v] = video_popularity.get(v, 0) + 1 for i in range(len(videos)): for j in range(i + 1, len(videos)): a, b = videos[i], videos[j] co_occur_count[(a, b)] = co_occur_count.get((a, b), 0) + 1 co_occur_count[(b, a)] = co_occur_count.get((b, a), 0) + 1 similarity = {} for (a, b), cnt in co_occur_count.items(): # 热门惩罚:cnt / (pop_a * pop_b) 的某个幂次,alpha在0~1之间 alpha = 0.6 denom = (video_popularity[a] ** alpha) * (video_popularity[b] ** alpha) similarity[(a, b)] = cnt / denom return similarity

3.3 向量召回:用Embedding弥补协同过滤的泛化短板

ItemCF的问题在于冷启动和稀疏性。新视频没有任何交互记录,根本无法被相似度计算纳入体系;用户行为稀疏时,候选集也很有限。为了弥补这个短板,我在项目中引入了向量召回方案。

做法是借助内容标签生成视频的Embedding向量。短视频在上传时会打上分类标签,我把标签通过哈希映射生成一个稀疏向量,再通过SVD降维成稠密Embedding。用户的Embedding则由他交互过视频的Embedding加权平均得到。这样算相似度时不再依赖共现矩阵,而是直接算余弦距离。

这一步在工程上难度其实不大,难的是如何把数据转换流程做成常态化任务。我的方案是:标签向量存储在PostgreSQL的JSON字段中,离线脚本定期读取标签并更新视频Embedding表,用户Embedding表则在用户每次交互行为后异步更新。

用向量召回替换掉一部分ItemCF召回,好处是新视频一旦有了标签就能进入推荐池,不需要等待积累行为。实测下来,新视频的曝光率提升了不少。

3.4 排序层的特征加权与分数归一化

精排阶段的本质问题可以概括为:假设召回来的候选集有100个视频,怎么把这100个排出一个让用户满意且平台目标也达成的顺序。

我实现的精排采用多特征加权求和,每个特征先归一化到0-1区间,再乘以权重系数。归一化这一步特别容易踩坑——不同特征的量纲完全不同,不归一化直接加权,数值大的特征会完全支配结果。我用的是最大值最小值归一化,并针对分布不均的特征加了log平滑,避免单个离群值把整个序列压扁。

实际参与精排的特征有这么几个:用户与视频类别匹配度、视频热度分、视频新鲜度分、用户对视频作者的过往偏好、视频完播率预测分(用视频历史完播率近似)。初始权重靠经验设置,上线后用线上数据持续调参。

# 精排打分示例 class RankService: WEIGHTS = { "category_match": 0.35, "hotness": 0.25, "freshness": 0.15, "author_preference": 0.15, "completion_rate": 0.10, } def score(self, features: dict) -> float: normalized = {k: self._normalize(v) for k, v in features.items()} total = 0.0 for key, weight in self.WEIGHTS.items(): total += normalized.get(key, 0) * weight return total

我建议:精排模块的权重不要硬编码在视图函数里,单独拆一个RankService类,后续调参只需要改配置或加一张权重表,不用动业务代码。当时这么做了之后,后期调参省了很多事情。

4. 缓存、异步与队列:高并发场景下的性能优化实践

4.1 Django同步接口的性能瓶颈在哪里

推荐系统的核心接口是"获取推荐流",前端每次下拉刷新都会请求这个接口。如果接口内部要实时跑一遍用户行为查询、视频候选集查询、特征拼接、排序打分,那响应时间一定撑不住。

我用一个形象的说法来描述这个场景:假如用户打开App看到10个视频,背后可能要处理这个用户过去三周的几百条行为记录,从候选表里捞出几百个视频,再逐个算出特征分。这些计算如果放在Django的同步视图里同步执行,用户请求会被迫等几百毫秒甚至几秒,对视频类产品来说是灾难。

所以我在架构设计时做了三件重要的事:本地缓存热数据、异步任务处理重计算、批量接口读取降低数据库压力。

4.2 本地缓存+Redis缓存分层

推荐结果不是每次请求都要实时计算的。用户在短时间内反复刷推荐流,看到的应该是连续刷新的不同内容,但如果几十秒内连续请求,系统完全可以直接返回一份缓存的推荐列表。

我做了两级缓存:第一级是本地进程内缓存,只缓存当前推荐接口最近一次返回结果的JSON字符串,TTL设置30秒;第二级是Redis缓存,为每个用户存放最近三批推荐结果的视频ID列表,TTL设10分钟。

用户每次请求推荐流时,Django视图先读本地缓存,没有则读Redis,Redis也没有才走完整的回归计算链路。关键数据的处理路径大约能减少80%以上的重复计算。实际测试中,推荐流接口的P95响应时间从接近1000ms降到了200ms以内,这个性能提升可以直观感受到。

缓存过期后的重建过程也要注意,我单独封装了一个函数,用Redis的分布式锁保证同一时刻只有一个线程在计算某个用户的推荐结果,防止缓存雪崩时大量请求同时击中后端导致数据库压力飙升。

# 缓存读取与重建的简化逻辑 def get_recommendations(user_id, page): cache_key = f"rec:{user_id}:{page}" result = cache.get(cache_key) if result is not None: return result with redis_lock(f"lock:{cache_key}", timeout=5): result = cache.get(cache_key) # 双重检查 if result is None: result = build_recommendation_list(user_id, page) cache.set(cache_key, result, ex=600) return result

4.3 Celery异步任务与定时推荐预计算

除了缓存,推荐系统里还有一类天然适合异步化的任务:周期性离线计算。比如每日用户兴趣向量更新、每日视频相似度表更新、每日全局热门榜计算,这些任务都不需要用户实时触发,只需要每天定时跑一次。

我引入了Celery消息队列框架,配合Redis作为Broker。定时任务通过Celery Beat调度,核心任务包括:

  • 每天凌晨2点重建视频相似度表
  • 每天凌晨3点更新所有活跃用户的兴趣向量
  • 每30分钟更新一次全局热门视频榜
  • 每10分钟扫描新上传视频并生成标签Embedding

Celery任务跑完后把结果写回数据库或Redis缓存,线上推荐接口只负责读这些已经算好的中间结果。这样做了之后,推荐接口的核心业务逻辑退化为三个步骤:查缓存、读候选表、排序输出,复杂度大大降低。

经验:如果项目部署的内存比较有限,Celery的Worker数量不要开太多。实际经验是4个Worker左右基本能满足中等规模内容平台的吞吐需求,设置过高会导致内存耗尽,引起不必要的OOM问题。

4.4 数据库层面的索引与读写分离思考

即使有了缓存和异步,传统关系型数据库依然是系统的底座,索引设计没有做好,缓存失效的那一瞬间就是灾难。

我在行为日志表上的索引设计是这样的:联合索引(user_id, created_time)、联合索引(video_id, behavior_type)、单独索引(created_time)。视频表上加了联合索引(category_id, status, created_time)。这些索引的建立依据完全来自查询模式——先确认线上SQL的WHERE条件组合,再创建对应索引,而不是盲目把所有字段都加索引。

读多写少的场景下,后期也可以考虑读写分离,把行为日志写入操作分到从库,推荐接口读取走主库。项目规模不大时这个配置不是必须的,但预留了这样的方向。

数据库层面还有一个隐藏的坑:行为日志表的数据量增长很快,如果不做分区或者定期归档,一年后查询性能会明显恶化。我的做法是每个月月初手动归档上个月数据到历史表,线上只保留近90天的热数据,更早的数据离线分析用。

5. 反馈闭环:采集用户行为数据并持续修正推荐结果

5.1 曝光与点击日志的准确埋点

推荐系统上线只是第一步,真正的价值在于根据线上反馈持续修正算法。而反馈数据从哪里来?从用户实际的使用行为中来。这就要求埋点必须准确。

我定义了一套前端上报协议:前端在推荐流视频卡片进入视野至少1秒后,上报"曝光"事件;用户点击播放时上报"点击"事件;播放结束后上报"完播"事件并携带观看时长。每个事件都包含用户ID、视频ID、请求批次ID、位置序号等字段。批次ID很重要,它能把"用户看到了第几个位置的视频"和"用户是否点击了该视频"关联起来,是后续计算推荐位点击率的基础。

埋点上报统一走一个Django API接口,先放入内存缓冲队列,批量写入数据库而不是每条日志一次写操作。这样可以有效降低写压力,减少对推荐接口的干扰。

5.2 离线指标计算与推荐效果评估

推荐系统上线后,我一直关注三个核心指标:推荐位点击率(CTR)、人均观看时长、次日留存率。这三个指标分别反映推荐系统"能不能吸引用户点"、"点进来能不能留住用户"、"用户愿不愿意再来"。

我写了一个离线统计脚本,每天晚上把当日曝光与点击日志聚合,算出整体CTR以及各个推荐位置的CTR差异。从位置维度的数据里能看出不少门道,比如第3到第5位的视频点击率通常最高,第1位反而不是;刷到第10个视频以后用户点击意愿明显下降,说明用户已经到了疲劳区,下一批内容需要调整策略。

人均观看时长则需要与视频播放记录关联,统计用户在推荐流中实际观看的总时长除以活跃用户数。这个指标与推荐质量直接相关:如果推荐的都是用户不感兴趣的内容,观看时长一定上不去。

5.3 在线A/B实验:不要拍脑袋改算法

修改推荐策略时最忌讳的是"拍脑袋上线"。我一开始调试时也犯过这个错误,把协同过滤的召回数量从50改到100,直接全量上线,结果第二天数据波动,分不清是正常波动还是策略带来的变化。

后来我落地了最简单的A/B实验机制:在用户表上加一个实验分组字段,推荐接口根据用户所在分组走不同的策略版本,离线对比两个版本的CTR和人均观看时长。这样就能比较客观地评估策略变化的实际影响。

举一个实际例子:把"不喜欢"按钮的负反馈权重从-5.0调整到-8.0后,实验组的推荐位CTR没有明显下降,但用户取关率下降了,说明更激进的负反馈处理能抑制用户对推荐流的反感。

建议:任何推荐策略改动,灰度上线是底线。先放5%的用户流量跑一天,对比核心指标后再逐步放量,能有效避免策略失误影响全部用户。

6. 部署上线前的自检清单与避坑经验

6.1 数据一致性问题:推荐列表不能出现已下架视频

视频资源会存在审核下架、作者删除等情况,但缓存和候选表可能仍然保留了这些视频的ID。用户刷新推荐流时如果刷到了不存在或者无法播放的内容,体验影响会很明显。

我在部署前做了一轮数据清洗,核心思路是:每天定时任务扫描线上推荐候选集状态,把已下架视频从各张候选表和缓存中移除。同时推荐接口读取最终视频详情时,做一次状态过滤,保证输出的每条视频都是可播放状态。

这个看似不起眼的问题,实际影响很直接。第一次上线时因为没有数据处理环节,测试阶段就出现了"黑屏视频混在推荐流里"的问题,后来专门补了这道过滤。

6.2 部署架构与配置参数

整个系统部署在单台服务器上,架构是:Nginx作为前端静态资源服务和反向代理,Gunicorn作为Django应用服务器,Celery Worker处理异步任务,Redis承载缓存与消息队列,PostgreSQL作为主存储。

Gunicorn的Worker数量我设置为CPU核心数的2倍加1。这个配置在大部分场景下算经验值,既充分利用多核,又不至于因为Worker过多导致上下文切换开销过大。再看Celery的并发数,我设置为4,配合内存上限配置,能稳定运行较长周期。

提示:Django项目的SECRET_KEY、数据库密码等敏感信息不要硬编码在settings.py中,我习惯放到环境变量文件里,部署时单独加载。这样做既避免泄露,又方便不同环境切换配置。

6.3 常见故障及应对方式

这里整理几个部署和测试阶段最常踩的问题,按照从高到低的发生频率列出:

问题根因应对方式
Redis连接池耗尽并发请求量超过默认连接池大小在Django Redis配置中设置连接池上限,并开启连接复用
Celery任务堆积任务生产速度大于Worker消费速度拆分任务优先级,高优任务单独队列,降低冗余任务频率
数据库连接数达到上限Gunicorn Worker数乘以每个Worker的数据库连接数过高使用连接池管理数据库连接,设置最大连接数
推荐接口偶发超时Redis缓存失效瞬间触发了全量重算调整缓存TTL,优化重建链路的查询计划

6.4 论文与答辩准备中的心得

这篇项目的论文写作和答辩思路,我有一条主线:需求分析->系统设计->算法设计->工程实现->效果评估。这个逻辑不只是为了论文结构,本身也是做项目的完整思路。

论文中一定要有算法对比实验,这是答辩的加分项。我对比了只用热门推荐的baseline和加入协同过滤+向量召回后的效果,明确数据显示推荐策略的加入能让CTR和人均观看时长明显提升。实验过程也能反过来验证系统中的算法实现是真实有效的,而不是摆设。

答辩时最容易遇到的追问是:"你的推荐算法相比业界最新模型有什么不足?"这种问题不需要辩解,更值得做的是诚实承认当前方案在特征表达能力和模型复杂度上的局限,同时说明下一步的改进方向——比如引入深度神经网络模型替换线性加权排序。这样的回答体现的是思考深度,而不是一味推卸。

7. 写在最后的几点提醒

整个项目从零搭建到完成,我最深的体会是:推荐系统的工程难度不在算法推导,而在于把算法"安稳地"放进业务系统里跑起来。数据清洗、特征存储、缓存更新、异步调度、效果评估,这些工程细节拼在一起,才构成了一个真正可用的推荐产品。

如果你打算复刻这个项目,我给三个优先级的建议:

  • 数据库建模时重视行为日志表的扩展性,给它预留字段。后续每次调整算法都可能会需要新的特征来源。
  • 缓存和异步任务要在项目一开始就设计好,不要等接口压测不过了才回头补。返工的成本远高于一开始就做好规划的成本。
  • 给自己留出至少两周的测试和调优时间,推荐效果和接口性能都不是一天能调出来的。

最后再分享一个小技巧:把推荐系统里的每个算法模块都设计成可以独立开关的插件模式。比如召回阶段可以只开热度召回,精排阶段可以只算热度分。这样排查线上问题时,能通过逐一关闭模块快速定位是数据问题还是算法问题。我在项目debug时靠这个习惯节省了大量时间。

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

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

立即咨询