「从零搭建」用 SpringBoot + 向量搜索打造智能短视频推荐系统!
在短视频内容爆炸的时代,如何让用户在海量视频中快速看到“自己想看的内容”,成为推荐系统的核心问题。 传统基于规则或协同过滤的推荐方式,已无法满足实时性与语义理解需求。
于是,向量搜索(Vector Search) 结合深度语义向量嵌入(Embedding)成为主流解决方案。 本文将通过 Spring Boot + Milvus/PGVector + OpenAI Embedding + Thymeleaf + Bootstrap,构建一个可落地的短视频语义推荐系统,实现以下目标:
- 视频元数据存储与语义向量嵌入;
- 用户输入搜索语句时,自动生成向量并进行相似度检索;
- 实时返回语义最相近的短视频内容;
- 前端动态展示推荐结果。
项目结构设计
springboot-vector-recommend/
├── src/
│ ├── main/
│ │ ├── java/com/icoderoad/recommend/
│ │ │ ├── controller/
│ │ │ │ └── VideoController.java
│ │ │ ├── service/
│ │ │ │ └── VideoService.java
│ │ │ ├── model/
│ │ │ │ └── Video.java
│ │ │ └── util/
│ │ │ └── EmbeddingUtil.java
│ │ ├── resources/
│ │ │ ├── templates/
│ │ │ │ └── recommend.html
│ │ │ └── application.yml
├── pom.xml
依赖配置(pom.xml)
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.icoderoad</groupId>
<artifactId>springboot-vector-recommend</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- Spring Boot 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- PostgreSQL + pgvector 支持 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- OpenAI Embedding 工具 -->
<dependency>
<groupId>com.theokanning.openai-gpt3-java</groupId>
<artifactId>client</artifactId>
<version>0.17.1</version>
</dependency>
<!-- Fastjson 解析工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.34</version>
</dependency>
</dependencies>
</project>
application.yml 配置
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/video_recommend
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
openai:
api-key: sk-xxxxxx # 替换为你自己的OpenAI密钥
数据库表结构(pgvector)
执行 SQL:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE video (
id SERIAL PRIMARY KEY,
title VARCHAR(255),
description TEXT,
url VARCHAR(255),
embedding vector(1536) -- 存储 OpenAI Embedding 向量
);
Embedding 工具类
package com.icoderoad.recommend.util;
import com.theokanning.openai.embedding.EmbeddingRequest;
import com.theokanning.openai.embedding.EmbeddingResult;
import com.theokanning.openai.service.OpenAiService;
import java.util.List;
public class EmbeddingUtil {
private static final String MODEL = "text-embedding-3-small";
private static final OpenAiService service = new OpenAiService(System.getenv("OPENAI_API_KEY"));
// 生成文本向量
public static List<Float> getEmbedding(String text) {
EmbeddingRequest request = EmbeddingRequest.builder()
.input(List.of(text))
.model(MODEL)
.build();
EmbeddingResult result = service.createEmbeddings(request);
return result.getData().get(0).getEmbedding();
}
}
后端推荐服务逻辑
package com.icoderoad.recommend.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.icoderoad.recommend.model.Video;
import com.icoderoad.recommend.util.EmbeddingUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class VideoService {
@Resource
private VideoMapper videoMapper;
/**
* 基于语义搜索的相似视频推荐
*/
public List<Video> recommendByText(String query) {
List<Float> embedding = EmbeddingUtil.getEmbedding(query);
String vectorString = embedding.toString().replace("[", "(").replace("]", ")");
// 使用 PGVector 的相似度查询(<-> 表示余弦距离)
String sql = "SELECT * FROM video ORDER BY embedding <-> '" + vectorString + "' LIMIT 10";
return videoMapper.selectBySql(sql);
}
}
Controller 层接口
package com.icoderoad.recommend.controller;
import com.icoderoad.recommend.model.Video;
import com.icoderoad.recommend.service.VideoService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
public class VideoController {
private final VideoService videoService;
public VideoController(VideoService videoService) {
this.videoService = videoService;
}
@GetMapping("/")
public String index() {
return "recommend";
}
@PostMapping("/recommend")
public String recommend(@RequestParam("query") String query, Model model) {
List<Video> results = videoService.recommendByText(query);
model.addAttribute("query", query);
model.addAttribute("videos", results);
return "recommend";
}
}
前端展示部分
(Thymeleaf + Bootstrap 实现推荐结果列表页)
文件:
src/main/resources/templates/recommend.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
<meta charset="UTF-8">
<title>短视频推荐系统</title>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<style>
body {
background-color: #f8f9fa;
}
.video-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.video-card:hover {
transform: scale(1.02);
}
.video-title {
font-weight: 600;
color: #333;
}
.video-desc {
color: #666;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container py-5">
<div class="text-center mb-4">
<h2 class="fw-bold">短视频智能推荐系统</h2>
<p class="text-muted">输入关键词,看看你会喜欢哪些视频</p>
</div>
<!-- 搜索框 -->
<form method="post" th:action="@{/recommend}" class="d-flex justify-content-center mb-5">
<input type="text" name="query" class="form-control w-50 me-2" placeholder="输入你的兴趣,如 '旅行' 或 '美食'"
th:value="${query}">
<button class="btn btn-primary px-4" type="submit">推荐一下</button>
</form>
<!-- 推荐结果列表 -->
<div class="row" th:if="${videos != null}">
<div th:each="v : ${videos}" class="col-md-4 mb-4">
<div class="card video-card">
<iframe th:src="${v.url}" class="card-img-top" height="200" allowfullscreen></iframe>
<div class="card-body">
<h5 class="video-title" th:text="${v.title}"></h5>
<p class="video-desc" th:text="${v.description}"></p>
</div>
</div>
</div>
</div>
<div th:if="${videos == null}" class="text-center text-muted mt-5">
<p>输入关键词后将显示推荐结果</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
运行效果
- 启动 Spring Boot 服务:
- mvn spring-boot:run
- 访问:http://localhost:8080
- 在输入框中输入关键词(如“音乐”、“健身”或“宠物”),点击推荐按钮;
- 页面将动态展示语义上相似的视频列表,并通过 Bootstrap 卡片美观呈现。
总结
通过本实战,我们完成了一个从 语义理解 → 向量检索 → 实时推荐 → 前端展示 的完整闭环系统。 它不仅适用于短视频推荐场景,也可轻松扩展至:
- 新闻/文章语义检索
- 音乐情绪推荐
- 知识问答匹配
接下来你可以继续优化:
- 将向量存储从 PostgreSQL 升级为 Milvus / Qdrant;
- 结合 ChatGPT Re-Ranker 提升结果精度;
- 利用 Redis 缓存向量查询结果 提高响应速度。
相关文章
- MyBatis如何实现分页查询?_mybatis collection分页查询
- 通过Mybatis Plus实现代码生成器,常见接口实现讲解
- MyBatis-Plus 日常使用指南_mybatis-plus用法
- 聊聊:Mybatis-Plus 新增获取自增列id,这一次帮你总结好
- MyBatis-Plus码之重器 lambda 表达式使用指南,开发效率瞬间提升80%
- Spring Boot整合MybatisPlus和Druid
- mybatis 代码生成插件free-idea-mybatis、mybatisX
- mybatis-plus 团队新作 mybatis-mate 轻松搞定企业级数据处理
- Maven 依赖范围(scope) 和 可选依赖(optional)
- Trace Sql:打通全链路日志最后一里路