volist后端数据(SpringBoot + 向量搜索 打造智能短视频推荐系统)

volist后端数据(SpringBoot + 向量搜索 打造智能短视频推荐系统)
SpringBoot + 向量搜索 打造智能短视频推荐系统

一、系统架构设计

智能短视频推荐系统的核心是基于用户兴趣的向量匹配,通过将用户行为、视频内容转化为高维向量,利用向量搜索的高效性实现 “人找内容” 到 “内容找人” 的转变。整体架构分为 5 层:

1. 架构分层

层级

核心组件

功能描述

数据采集层

行为埋点 SDK、视频元数据爬虫、用户画像采集器

采集用户行为(点赞 / 收藏 / 评论 / 完播)、视频元数据(标题 / 标签 / 封面)、用户基础信息

数据处理层

SpringBoot 数据处理服务、Spark/Flink 离线 / 实时计算

行为数据清洗、特征提取、向量生成(用户兴趣向量 / 视频内容向量)

向量存储层

Milvus/Zilliz Cloud(向量数据库)、MySQL(业务数据)

存储高维向量,提供近邻搜索、过滤查询能力;MySQL 存储用户 / 视频基础信息

推荐服务层

SpringBoot 推荐核心服务、负载均衡、缓存(Redis)

接收推荐请求,调用向量搜索,结合业务规则(新鲜度 / 多样性)返回推荐结果

应用层

短视频 APP / 小程序、管理后台

展示推荐结果,收集用户反馈,管理推荐策略

2. 核心流程

  1. 向量生成:视频内容向量:对视频标题、标签、描述做文本嵌入(如 BERT),对封面图做图像嵌入(如 ResNet),融合为视频向量(维度通常 128/256/512 维)。用户兴趣向量:基于用户历史行为(如对视频的互动权重:完播 = 3,点赞 = 2,收藏 = 5),加权平均其互动视频的向量,得到用户兴趣向量。
  2. 推荐匹配:实时推荐:用户打开 APP 时,用用户兴趣向量在向量数据库中搜索 Top-N 相似视频向量。离线推荐:定时计算热门视频、相似视频集群,缓存到 Redis,提升冷启动和推荐多样性。
  3. 结果优化:过滤已观看视频、结合视频新鲜度(发布时间权重)、用户偏好标签过滤,返回最终推荐列表。

二、技术选型

技术领域

选型方案

选型理由

后端框架

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

volist后端数据(SpringBoot + 向量搜索 打造智能短视频推荐系统)

运行

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. 本地部署

  1. 启动 Milvus:通过 Docker 快速部署(推荐单节点模式用于开发测试):
  2. bash
  3. 运行
  4. docker run -d --name milvus -p 19530:19530 -p 9091:9091 milvusdb/milvus:v2.4.3 standalone
  5. 启动 Redis、MySQL:本地或 Docker 部署。
  6. 启动 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),验证向量匹配效果。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

相关阅读

最新文章

热门文章

本栏目文章