一、系统架构设计
智能短视频推荐系统的核心是基于用户兴趣的向量匹配,通过将用户行为、视频内容转化为高维向量,利用向量搜索的高效性实现 “人找内容” 到 “内容找人” 的转变。整体架构分为 5 层:
1. 架构分层
层级 | 核心组件 | 功能描述 |
数据采集层 | 行为埋点 SDK、视频元数据爬虫、用户画像采集器 | 采集用户行为(点赞 / 收藏 / 评论 / 完播)、视频元数据(标题 / 标签 / 封面)、用户基础信息 |
数据处理层 | SpringBoot 数据处理服务、Spark/Flink 离线 / 实时计算 | 行为数据清洗、特征提取、向量生成(用户兴趣向量 / 视频内容向量) |
向量存储层 | Milvus/Zilliz Cloud(向量数据库)、MySQL(业务数据) | 存储高维向量,提供近邻搜索、过滤查询能力;MySQL 存储用户 / 视频基础信息 |
推荐服务层 | SpringBoot 推荐核心服务、负载均衡、缓存(Redis) | 接收推荐请求,调用向量搜索,结合业务规则(新鲜度 / 多样性)返回推荐结果 |
应用层 | 短视频 APP / 小程序、管理后台 | 展示推荐结果,收集用户反馈,管理推荐策略 |
2. 核心流程
- 向量生成:视频内容向量:对视频标题、标签、描述做文本嵌入(如 BERT),对封面图做图像嵌入(如 ResNet),融合为视频向量(维度通常 128/256/512 维)。用户兴趣向量:基于用户历史行为(如对视频的互动权重:完播 = 3,点赞 = 2,收藏 = 5),加权平均其互动视频的向量,得到用户兴趣向量。
- 推荐匹配:实时推荐:用户打开 APP 时,用用户兴趣向量在向量数据库中搜索 Top-N 相似视频向量。离线推荐:定时计算热门视频、相似视频集群,缓存到 Redis,提升冷启动和推荐多样性。
- 结果优化:过滤已观看视频、结合视频新鲜度(发布时间权重)、用户偏好标签过滤,返回最终推荐列表。
二、技术选型
技术领域 | 选型方案 | 选型理由 |
后端框架 | SpringBoot 3.x + Spring Cloud(可选,微服务扩展) | 快速开发、生态完善,支持高并发,易于集成第三方组件 |
向量数据库 | Milvus 2.x(开源本地部署)/ Zilliz Cloud(托管服务) | 支持高维向量近邻搜索(ANN),毫秒级响应,支持过滤条件(如视频分类、时长) |
嵌入模型(Embedding) | Sentence-BERT(文本)、ResNet-50(图像)、Hugging Face Transformers | 轻量级、效果好,支持中文文本嵌入,可本地化部署或调用 API |
缓存 | Redis 7.x | 缓存热门视频、用户兴趣向量、已观看视频列表,降低数据库压力 |
数据计算 | Spark(离线向量生成)、Flink(实时行为处理) | 处理海量用户行为数据,高效生成向量 |
数据库 | MySQL 8.x(业务数据)、MongoDB(可选,存储视频元数据 / 行为日志) | MySQL 存储结构化数据,MongoDB 适合非结构化 / 半结构化数据 |
API 文档 | SpringDoc OpenAPI 3(Swagger) | 自动生成 API 文档,方便前后端联调 |
三、核心模块实现
1. 环境准备(Maven 依赖)
核心依赖包括 SpringBoot、Milvus 客户端、Embedding 模型、Redis 等:
<!-- SpringBoot核心 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- 向量数据库Milvus --><dependency> <groupId>io.milvus</groupId> <artifactId>milvus-sdk-java</artifactId> <version>2.4.3</version></dependency><!-- Embedding模型(Sentence-BERT) --><dependency> <groupId>com.hankcs.hanlp</groupId> <artifactId>hanlp</artifactId> <version>portable-1.8.4</version></dependency><dependency> <groupId>net.sf.trove4j</groupId> <artifactId>trove4j</artifactId> <version>3.0.3</version></dependency><!-- 或使用Hugging Face Transformers(需Java 11+) --><dependency> <groupId>ai.djl.huggingface</groupId> <artifactId>tokenizers</artifactId> <version>0.23.0</version></dependency><!-- 工具类 --><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson2</artifactId> <version>2.0.41</version></dependency><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional></dependency>2. 向量生成模块(核心)
2.1 视频内容向量生成
通过 Sentence-BERT 将视频文本信息(标题 + 标签 + 描述)转化为向量,结合图像向量(可选)融合:
java
运行
import com.hankcs.hanlp.HanLP;import org.springframework.stereotype.Component;import java.util.List;import java.util.stream.Collectors;/** * 视频向量生成器:文本嵌入(Sentence-BERT)+ 图像嵌入(可选) */@Componentpublic class VideoEmbeddingGenerator { // 加载Sentence-BERT模型(本地化部署或调用远程API) private final SentenceBertModel sentenceBertModel = new SentenceBertModel("path/to/model"); /** * 生成视频向量(文本+图像融合) * @param video 视频元数据 * @return 512维向量 */ public float[] generate(VideoDTO video) { // 1. 文本预处理:分词、过滤停用词 String text = video.getTitle() + " " + String.join(" ", video.getTags()) + " " + video.getDescription(); List<String> keywords = HanLP.extractKeyword(text, 20); // 提取Top20关键词 String processedText = String.join(" ", keywords); // 2. 文本嵌入(512维) float[] textEmbedding = sentenceBertModel.encode(processedText); // 3. 图像嵌入(可选:封面图转化为向量,如ResNet-50输出2048维,降维到512维) float[] imageEmbedding = new ImageEmbeddingGenerator().generate(video.getCoverUrl()); // 4. 向量融合(加权平均:文本0.7,图像0.3) float[] finalEmbedding = new float[512]; for (int i = 0; i < 512; i++) { finalEmbedding[i] = 0.7f * textEmbedding[i] + 0.3f * imageEmbedding[i]; } return finalEmbedding; }}// 简化的Sentence-BERT模型封装(实际需加载预训练模型,如bert-base-chinese)class SentenceBertModel { private final String modelPath; public SentenceBertModel(String modelPath) { this.modelPath = modelPath; // 初始化模型(如使用DJL加载PyTorch预训练模型) } public float[] encode(String text) { // 模型推理:文本 -> 向量(示例返回随机向量,实际替换为真实模型输出) float[] vector = new float[512]; for (int i = 0; i < 512; i++) { vector[i] = (float) Math.random(); } return vector; }}2.2 用户兴趣向量生成
基于用户历史互动行为(点赞 / 收藏 / 完播)加权计算兴趣向量:
java

运行
import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.List;import java.util.Map;/** * 用户兴趣向量生成器:基于历史行为加权融合 */@Componentpublic class UserInterestGenerator { // 行为权重配置:完播>收藏>点赞>评论>浏览 private static final Map<String, Float> BEHAVIOR_WEIGHT = Map.of( "finish_watch", 3.0f, "collect", 2.5f, "like", 2.0f, "comment", 1.5f, "view", 1.0f ); @Resource private VideoEmbeddingGenerator videoEmbeddingGenerator; @Resource private MilvusService milvusService; @Resource private StringRedisTemplate redisTemplate; /** * 生成用户兴趣向量 * @param userId 用户ID * @return 512维兴趣向量 */ public float[] generate(String userId) { // 1. 从Redis获取用户最近30天互动行为(videoId -> behaviorType) String behaviorKey = "user:behavior:" + userId; Map<Object, Object> behaviorMap = redisTemplate.opsForHash().entries(behaviorKey); if (behaviorMap.isEmpty()) { // 冷启动:返回热门视频平均向量 return getHotVideoAvgEmbedding(); } // 2. 加权计算兴趣向量 float[] interestVector = new float[512]; float totalWeight = 0.0f; for (Map.Entry<Object, Object> entry : behaviorMap.entrySet()) { String videoId = (String) entry.getKey(); String behavior = (String) entry.getValue(); Float weight = BEHAVIOR_WEIGHT.getOrDefault(behavior, 1.0f); // 3. 从Milvus获取视频向量 float[] videoVector = milvusService.getVideoVector(videoId); if (videoVector == null) continue; // 4. 加权累加 for (int i = 0; i < 512; i++) { interestVector[i] += videoVector[i] * weight; } totalWeight += weight; } // 5. 归一化 if (totalWeight > 0) { for (int i = 0; i < 512; i++) { interestVector[i] /= totalWeight; } } return interestVector; } /** * 冷启动策略:热门视频平均向量 */ private float[] getHotVideoAvgEmbedding() { List<String> hotVideoIds = redisTemplate.opsForList().range("video:hot", 0, 99); // Top100热门视频 float[] avgVector = new float[512]; for (String videoId : hotVideoIds) { float[] videoVector = milvusService.getVideoVector(videoId); if (videoVector == null) continue; for (int i = 0; i < 512; i++) { avgVector[i] += videoVector[i]; } } // 归一化 for (int i = 0; i < 512; i++) { avgVector[i] /= hotVideoIds.size(); } return avgVector; }}3. 向量数据库操作(Milvus)
封装 Milvus 的向量存储、查询、删除操作:
import io.milvus.client.MilvusClient;import io.milvus.client.MilvusServiceClient;import io.milvus.param.ConnectParam;import io.milvus.param.IndexType;import io.milvus.param.MetricType;import io.milvus.param.collection.CreateCollectionParam;import io.milvus.param.dml.SearchParam;import io.milvus.response.SearchResultsWrapper;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;import java.util.List;/** * Milvus向量数据库操作服务 */@Servicepublic class MilvusService { @Value("${milvus.host:localhost}") private String host; @Value("${milvus.port:19530}") private int port; @Value("${milvus.collection:video_vector}") private String collectionName; private MilvusClient milvusClient; // 初始化Milvus客户端 @PostConstruct public void init() { ConnectParam connectParam = ConnectParam.newBuilder() .withHost(host) .withPort(port) .build(); milvusClient = new MilvusServiceClient(connectParam); // 检查集合是否存在,不存在则创建(向量维度512,索引类型IVF_FLAT,适合中小规模数据) createCollectionIfNotExists(); } // 创建视频向量集合 private void createCollectionIfNotExists() { boolean exists = milvusClient.hasCollection(collectionName); if (!exists) { CreateCollectionParam createParam = CreateCollectionParam.newBuilder() .withCollectionName(collectionName) .addFieldType("video_id", FieldType.VARCHAR, 64, true) // 主键:视频ID .addFieldType("vector", FieldType.FLOAT_VECTOR, 512) // 向量字段 .addFieldType("category", FieldType.INT32) // 视频分类(用于过滤) .addFieldType("publish_time", FieldType.INT64) // 发布时间(用于新鲜度排序) .withIndexType(IndexType.IVF_FLAT) // 索引类型:IVF_FLAT(平衡速度和精度) .withMetricType(MetricType.COSINE) // 距离度量:余弦相似度(适合向量匹配) .withParams("{\"nlist\":1024}") // IVF_FLAT参数:聚类数量 .build(); milvusClient.createCollection(createParam); } } /** * 插入视频向量 */ public void insertVideoVector(VideoVectorDTO vectorDTO) { milvusClient.insert(collectionName, List.of(vectorDTO.getVideoId()), List.of(vectorDTO.getVector()), List.of(vectorDTO.getCategory()), List.of(vectorDTO.getPublishTime()) ); } /** * 获取视频向量 */ public float[] getVideoVector(String videoId) { SearchResultsWrapper results = milvusClient.query( collectionName, "video_id = ?", List.of(videoId), List.of("vector") ); return results.getFieldData("vector", Float[].class).stream() .findFirst() .map(floats -> floats.stream().mapToFloat(Float::floatValue).toArray()) .orElse(null); } /** * 向量搜索:根据用户兴趣向量找相似视频 * @param userVector 用户兴趣向量 * @param topN 返回数量 * @param category 分类过滤(可选) * @return 相似视频ID列表(按相似度+新鲜度排序) */ public List<String> searchSimilarVideos(float[] userVector, int topN, Integer category) { // 构建搜索条件:余弦相似度,可选分类过滤 SearchParam.Builder searchBuilder = SearchParam.newBuilder() .withCollectionName(collectionName) .withVector(userVector) .withTopK(topN) .withMetricType(MetricType.COSINE) .withParams("{\"nprobe\":10}") // 搜索时探查的聚类数量(越大越准,速度越慢) .addOutputField("video_id", "publish_time"); // 分类过滤(如用户偏好美食类视频) if (category != null) { searchBuilder.withFilter("category = " + category); } // 执行搜索 SearchResultsWrapper results = milvusClient.search(searchBuilder.build()); // 结果处理:按相似度降序 + 发布时间降序排序 return results.getResults().stream() .sorted((a, b) -> { int simCompare = Float.compare(b.getScore(), a.getScore()); // 相似度优先 if (simCompare != 0) return simCompare; // 相似度相同,取更新时间较新的 return Long.compare( (Long) b.getFieldValue("publish_time"), (Long) a.getFieldValue("publish_time") ); }) .map(result -> (String) result.getFieldValue("video_id")) .toList(); }}// 视频向量DTO@Dataclass VideoVectorDTO { private String videoId; private float[] vector; private Integer category; private Long publishTime; // 时间戳(毫秒)}4. 推荐核心服务
整合向量生成、向量搜索、业务规则,提供推荐 API:
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import java.util.List;/** * 推荐核心API */@RestController@RequestMapping("/api/recommend")public class RecommendController { @Resource private UserInterestGenerator userInterestGenerator; @Resource private MilvusService milvusService; @Resource private VideoService videoService; @Resource private StringRedisTemplate redisTemplate; /** * 获取个性化推荐视频 * @param userId 用户ID * @param topN 推荐数量(默认20) * @param category 分类过滤(可选,如1=美食,2=游戏) * @return 视频列表(含标题、封面、播放地址等) */ @GetMapping("/personalized") public Result<List<VideoVO>> getPersonalizedRecommend( @RequestParam String userId, @RequestParam(defaultValue = "20") int topN, @RequestParam(required = false) Integer category) { // 1. 生成用户兴趣向量 float[] userInterestVector = userInterestGenerator.generate(userId); // 2. 从Redis获取用户已观看视频ID,用于过滤 String watchedKey = "user:watched:" + userId; List<String> watchedVideoIds = redisTemplate.opsForList().range(watchedKey, 0, -1); // 3. 向量搜索:找相似视频(过滤已观看) List<String> similarVideoIds = milvusService.searchSimilarVideos(userInterestVector, topN * 2, category); List<String> recommendVideoIds = similarVideoIds.stream() .filter(videoId -> !watchedVideoIds.contains(videoId)) .limit(topN) .toList(); // 4. 补充视频元数据(从MySQL/MongoDB查询) List<VideoVO> videoVOList = videoService.getVideoByIds(recommendVideoIds); return Result.success(videoVOList); } /** * 冷启动推荐:热门视频 */ @GetMapping("/hot") public Result<List<VideoVO>> getHotRecommend(@RequestParam(defaultValue = "20") int topN) { List<String> hotVideoIds = redisTemplate.opsForList().range("video:hot", 0, topN - 1); List<VideoVO> videoVOList = videoService.getVideoByIds(hotVideoIds); return Result.success(videoVOList); }}// 统一返回结果@Dataclass Result<T> { private int code = 200; private String msg = "success"; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setData(data); return result; }}// 视频VO(给前端返回的数据)@Dataclass VideoVO { private String videoId; private String title; private String coverUrl; private String playUrl; private List<String> tags; private Integer category; private Long publishTime; private Long playCount;}5. 冷启动与优化策略
5.1 冷启动处理
- 新用户:推荐热门视频(按播放量 / 点赞量排序)+ 全分类视频(探索用户兴趣)。
- 新视频:将新视频向量与各分类的热门向量匹配,推荐给该分类的潜在用户;同时加入 “新视频推荐池”,给所有用户少量曝光。
5.2 推荐多样性优化
- 向量搜索时限制同一分类视频占比不超过 30%。
- 定期更新用户兴趣向量(如每小时),避免长期推荐同类内容。
- 引入 “探索因子”:推荐列表中混入 10%-20% 的非相似但高热度视频,拓宽用户兴趣。
5.3 性能优化
- Redis 缓存:缓存用户兴趣向量、热门视频列表、已观看视频 ID,减少向量生成和数据库查询开销。
- Milvus 索引优化:大规模数据(千万级)时,改用 HNSW 索引(牺牲部分内存,提升搜索速度)。
- 异步处理:视频向量生成、用户兴趣向量更新改为异步任务(如使用 Spring @Async),避免阻塞推荐流程。
四、部署与扩展
1. 本地部署
- 启动 Milvus:通过 Docker 快速部署(推荐单节点模式用于开发测试):
- bash
- 运行
- docker run -d --name milvus -p 19530:19530 -p 9091:9091 milvusdb/milvus:v2.4.3 standalone
- 启动 Redis、MySQL:本地或 Docker 部署。
- 启动 SpringBoot 应用:配置application.yml中的数据库连接信息。
2. 生产环境扩展
- 微服务拆分:将推荐服务、向量生成服务、数据采集服务拆分为独立微服务,通过 Spring Cloud 注册中心(Nacos/Eureka)实现服务发现。
- 向量数据库集群:Milvus 集群部署(含 Proxy、DataNode、IndexNode、QueryNode),支持水平扩展,应对高并发查询。
- 嵌入模型优化:将 Embedding 模型部署为独立服务(如用 FastAPI 封装 Python 模型),SpringBoot 通过 HTTP 调用,避免 Java 端加载模型占用过多内存。
- 监控告警:集成 Prometheus + Grafana 监控系统吞吐量、响应时间、向量搜索准确率;通过 ELK 收集日志。
五、关键指标与效果评估
1. 核心指标
- 推荐准确率:点击转化率(CTR)、完播率、点赞 / 收藏率(目标:高于随机推荐 30%+)。
- 系统性能:推荐接口响应时间(目标:P99 < 200ms)、QPS(目标:支持 10 万 + 并发)。
- 用户体验:用户留存率(7 日留存率提升 15%+)、内容多样性(用户观看视频分类数提升 20%+)。
2. 效果评估方法
- A/B 测试:将用户分为实验组(向量推荐)和对照组(随机推荐 / 热门推荐),对比核心指标。
- 离线评估:使用历史行为数据计算召回率(Recall@K)、精确率(Precision@K),验证向量匹配效果。