Embedding(文本嵌入):把自然语言文字 → 固定长度浮点数字向量
检索底层原理【RAG】

redis-stack 镜像包含了 Redis 主服务 + RedisInsight + 向量检索模块(RediSearch),非常适合快速搭建支持向量存储的 RAG 环境
# 1. 先创建宿主机目录(可选,但推荐)
mkdir -p /home/redis-data
# 2. 启动容器(把 -v 改成 Linux 路径)
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 -v /home/redis-data:/data redis/redis-stack:latest




1、pom
<properties>
<java.version>17</java.version>
<spring-ai.version>2.0.0-M7</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- Redis 向量库-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
<!--Redis 客户端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- Tika 通用文档读取器 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<!-- Markdown 文档读取器 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
<!-- 问答顾问器 advisor-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2、application.yml
2.1
${OPENAI_API_KEY}

2.2
ollama docker容器
宿主机:192.168.91.164

容器ollama启动模型qwen2.5:0.5b

访问宿主机模型
http://192.168.91.164:11434/

spring:
autoconfigure:
exclude: #禁用 Ollama Embedding(如果你用 OpenAI),不然会冲突
- org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration
application:
name: dashscope18088
data:
redis:
url: redis://192.168.91.165:6379
client-type: jedis
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
chat:
model: qwen3.6-plus
embedding:
options:
model: text-embedding-v4
dimensions: 2048 # 输出向量维度2048
# chat:
# model: qwen3.6-plus
# 可选:超时/重试配置
vectorstore:
redis:
initialize-schema: true #是否在启动时创建/初始化向量索引等结构
index-name: sf2026_ai_vector_index # RediSearch 里的索引名
prefix: sf2026 #存向量文档用的Redis键前级
ollama:
base-url: http://192.168.91.164:11434
chat:
model: qwen2.5:0.5b
# options:
# temperature: 0.7
# max-tokens: 2000
server:
port: 18088
logging:
level:
org.springframework.ai: debug
3、config
import com.sb.dashscope18088.advisor.MySimpleLoggerAdvisor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiConfiguration {
@Bean
public ChatMemory chatMemory(){
return MessageWindowChatMemory.builder()
.maxMessages(10) // 设置消息窗口大小为10
.chatMemoryRepository(new InMemoryChatMemoryRepository()) // 内存存储
.build();
}
@Bean
public ChatClient chatClient(OpenAiChatModel model,ChatMemory chatMemory){
return ChatClient
.builder(model) // 创建ChatClient对象,以及设置模型model
//.defaultAdvisors(new MySimpleLoggerAdvisor())//添加日志拦截器
.defaultAdvisors(
new SimpleLoggerAdvisor(),//添加一个日志拦截器(内置日志拦截器,)
MessageChatMemoryAdvisor.builder(chatMemory).build()//添加一个聊天记录截器
)
.build(); // 构建ChatClient对象
}
@Bean
public ChatClient chatClient2(OllamaChatModel model){
return ChatClient
.builder(model) // 创建ChatClient对象,以及设置模型model
.defaultAdvisors(new MySimpleLoggerAdvisor())//添加日志拦截器
.build(); // 构建ChatClient实例
}
@Bean
public ChatClient chatClient3(OpenAiChatModel model){
// System Prompt 工程:清晰定义 AI 的角色、任务、约束和输出格式
//【系统提示词】
String systemPrompt = """
你是一个资深的 Java 技术顾问。
禁止回答任何非技术类问题,例如天气或娱乐八卦。
代码示例必须符合 Java 17+ 规范。
回答需要符合以下格式:首先一句话概括问题的核心,然后提供代码示例,最后补充注意事项。
如果自己不确定,可以说"关于这个问题,我目前没有确切的信息",禁止编造内容。
""";
return ChatClient
.builder(model) // 创建ChatClient对象,以及设置模型model
.defaultSystem(systemPrompt)
//.defaultAdvisors(new MySimpleLoggerAdvisor())//添加日志拦截器
.defaultAdvisors(new SimpleLoggerAdvisor())//内置日志拦截器
.build(); // 构建ChatClient对象
}
}
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 文本分块配置类
*/
@Configuration
public class TextSplitterConfiguration {
/**
* 令牌文本切分器
*/
@Bean
public TokenTextSplitter tokenTextSplitter() {
return TokenTextSplitter.builder()
.withChunkSize(50) // 单个文本块的最大 token 数
.withMaxNumChunks(1000) // 最大可生成的块数量。防止对超长文本无限分割,达到此上限后即使还有剩余文本也停止生产新块。
.build();
}
}
4、pojo
import java.util.List;
//使用 Java 16+ 的 Record 特性,编译器会自动生成构造器、equals/hashCode等方法
public record TopicBook(
String topic, // 主题
List<String> books // 推荐书籍
){}
// Java16+ Record 不可变实体,用于书籍评论结构化接收
public record BookReview(
String reviewerName, // 评价人
int rating, // 评分
String comment // 评价内容
){}
5、tool

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author Administrator
*/
@Component
public class DateTimeTools {
@Tool(description = "获取用户在指定时区的当前日期和时间,用于回答需要实时时间的问题")
public String getCurrentTime(){
// 获取用户的时区偏好设置
var zoneId = LocaleContextHolder.getTimeZone().toZoneId();
var now = LocalDateTime.now().atZone(zoneId);
// 格式化为 yyyy-MM-dd HH:mm:ss
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(now);
}
@Tool(description = "设置闹钟,调用此工具可在指定时间触发提醒。时间参数必须是 ISO-8601 格式," +
"例如: 2026-05-03 15:30:00")
public void setAlarm(@ToolParam(description = "闹钟的触发时间,标准格式: yyyy-MM-dd HH:mm:ss")
String alarmTime) {
System.out.println("⏰ 闹钟已设置,将在 " + alarmTime + " 提醒用户。");
// 可扩展:持久化入库、定时任务、消息推送等业务逻辑
}
}
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.List;
/**
* 多格式文档解析工具类
* 支持: PDF、DOC(DOCX)、TXT/TEXT、MD
*/
@Component
public class DocumentParseUtil {
public List<Document> parse(String filePath) {
File file = new File(filePath);
String suffix = filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase();
Resource resource = new FileSystemResource(file);
DocumentReader reader = switch (suffix) {
case "pdf", "doc", "docx", "txt", "text" -> new TikaDocumentReader(resource);
case "md", "markdown" -> new MarkdownDocumentReader(file.toURI().toString());
default -> throw new IllegalArgumentException("不支持的文件格式: " + suffix);
};
return reader.get();
}
}
6、controller
import com.sb.dashscope18088.tool.DocumentParseUtil;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.IOException;
import java.util.List;
@RestController
public class MyRagController {
@Autowired
private VectorStore vectorStore; // 向量存储
@Autowired
private TokenTextSplitter tokenTextSplitter; // 文本分块器
@Autowired
private DocumentParseUtil documentParseUtil; // 文档解析工具
@Autowired
private ChatClient chatClient;
/**
* 内容添加到vectorStore
* @return
*/
@RequestMapping("/addRagDocs")
public String addDocs() throws IOException {
// 读取 resources/upload 目录下的文件
// addChunkedDocuments("upload/Java学习.pdf", "Java学习.pdf");
addChunkedDocuments("www.sf2026.com网站简介.txt", "www.sf2026.com网站简介.txt");
// addChunkedDocuments("upload/java1234简介.docx", "java1234简介.docx");
// addChunkedDocuments("upload/小锋老师简介.md", "小锋老师简介.md");
return "addRagDocs OK";
}
/**
* 查询vectorStore 内容
* @return
*/
@RequestMapping("/queryRag")
public String queryRag() {
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.builder()
.query("介绍下sf2026")
.topK(3)
.build()
);
System.out.println("【Redis vector queryRag】"+docs);
return "【Redis vector queryRag】"+docs;
}
/**
* 查询vectorStore 内容
* 问答顾问器
* @return
*/
@RequestMapping("/qaadvisorRag")
public String qaadvisorRag(){
// 1. 构建 RAG 顾问器
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(3) // 搜索结果数量
.similarityThreshold(0.7) // 相似度阈值 0-1 值越大要求越严格 过滤低相关性的结果,提高 RAG 回答质量
.build())
.build();
// 2. 调用 ChatClient,自动完成 RAG 流程
String result = chatClient.prompt()
.advisors(qaAdvisor) // 注入 RAG 顾问器
.advisors(a->a.param(ChatMemory.CONVERSATION_ID,"0001"))
.user("介绍下sf2026") // 用户问题
.call()
.content();
System.out.println("【Redis vector qaadvisor】"+result);
return "【Redis vector qaadvisor】"+result;
}
/**
* 解析后按 token 切分再入库,避免单条 Document 超过嵌入模型输入上限
*/
private void addChunkedDocuments(String filePath, String label) throws IOException {
// 1. 从 resources 目录读取文件
ClassPathResource resource = new ClassPathResource("upload/" + filePath);
// 2. 获取文件对象(开发环境 IDE 下有效)
File file = resource.getFile();
List<Document> raw = documentParseUtil.parse(file.getAbsolutePath());
List<Document> chunks = tokenTextSplitter.apply(raw);
vectorStore.add(chunks);
System.out.println(label + ", 向量存储添加成功!(共 " + chunks.size() + " 块)");
}
}
7、访问
7.1

内容添加到vectorStore
http://localhost:18088/addRagDocs


7.2
查询vectorStore内容
http://localhost:18088/queryRag

7.3
查询vectorStore内容
问答顾问器
http://localhost:18088/qaadvisorRag


浙公网安备 33010602011771号