一、系统架构设计
智能短视频推荐系统的核心是基于用户兴趣的向量匹配,通过将用户行为、视频内容转化为高维向量,利用向量搜索的高效性实现 “人找内容” 到 “内容找人” 的转变。整体架构分为 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)+ 图像嵌入(可选)
*/
@Component
public 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;
/**
* 用户兴趣向量生成器:基于历史行为加权融合
*/
@Component
public 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向量数据库操作服务
*/
@Service
public 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
@Data
class 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);
}
}
// 统一返回结果
@Data
class 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(给前端返回的数据)
@Data
class 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),验证向量匹配效果。
















- 最新
- 最热
只看作者