在成功接入 Spring AI 和 DeepSeek, 实现基本对话功能后,我们开始尝试构建文档知识库。本章将重点介绍 RAG(检索增强生成) 的核心概念、文本向量化与存储的基本流程,并通过搭建 Redis-Stack 环境实现一个简单的文档知识库原型,为后续的智能问答功能打下基础。本文将提供清晰的步骤、代码示例(为方便查看代码,很多部分并没有做拆分、优化、抽象,需要时可自行优化)和环境配置,适合 Java 开发者快速上手。
一、什么是 RAG(检索增强生成)?
1. RAG 的定义
RAG(Retrieval-Augmented Generation)是一种结合 检索 和 生成 的技术,通过从外部知识库检索相关信息,增强大语言模型(LLM)的回答能力。其核心思想是:
- 检索:根据用户的问题,在知识库中找到最相关的文档或片段。
- 生成:将检索到的内容作为上下文,输入大语言模型生成准确、自然的回答。
2. 为什么需要 RAG?
大语言模型虽然强大,但存在以下局限:
- 知识截止:模型的知识受训练数据限制,无法实时获取最新信息。
- 专业性不足:对于企业内部文档或特定领域的知识,模型可能无法提供精准答案。
- 生成幻觉:模型可能生成不准确或虚构的内容。
RAG 通过引入外部知识库,解决了这些问题,确保回答基于真实的文档内容,提高准确性和可信度。
3. RAG 在本项目中的作用
在本项目中,RAG 将用于:
- 存储企业或个人的文档(如 PDF、Word、Excel 等)。
- 将文档向量化并存入向量数据库。
- 根据用户问题检索相关文档,结合 DeepSeek 生成答案。
二、向量化及基本流程
1. 向量化
在 RAG 里,向量化(Embedding) 是最核心的一步。它的作用是把人类语言转换成计算机可以理解和比较的“数字向量”。
可以简单理解为:
- 一句话,就像一句话的“坐标”,被映射到一个高维空间(例如 1536 维)。
- 语义相近的句子,它们的向量坐标就会更靠近。
- 这样,当我们想回答问题时,只要把问题也向量化,再去找“最靠近”的片段,就能检索到相关内容。
比如:
- “我喜欢看电影” →
[0.12, -0.33, ..., 0.87] - “我爱看影片” →
[0.11, -0.31, ..., 0.86] - “今天天气很好” →
[0.75, 0.12, ..., -0.44]
可以看到,前两句话表达的意思接近,所以它们的向量“距离”也很近,而第三句话就差得比较远。
这就是为什么向量化能帮我们解决“语义相似度”的问题,而不仅仅是做关键词匹配。
2. 基本流程
文本向量化是 RAG 的核心步骤,将非结构化文档转化为可检索的向量表示。以下是向量化的基本流程:
-
文本提取
- 文档里通常有各种格式(PDF 中的排版、Word 的表格、Excel 的单元格),直接拿去向量化会失败。
- 我们先把文档中的内容提取成纯文本(plain text),去掉多余的格式,只保留能被模型理解的文字。
- 一份 PDF 简历提取后就变成“姓名:张三,技能:Java、Spring Boot,经历:某某公司…”。
-
文本切片(Chunking)
- 向量模型通常对输入长度有限制(比如最多 8000 个 Token),如果直接把一整本书丢进去,肯定超限。
- 把大文本切分成小段,比如每 200~500 字一片。这样既能保证每段语义完整,又方便后续检索。但是也要注意如果切分的太细就会增加成本而且精度丢失。
- 一本说明书可以被切分为“第一章:安装步骤…”,“第二章:常见问题…”,“第三章:维护方法…”。
-
向量化(Embedding)
- 机器无法直接理解文字的意思,但能理解数字。通过向量化,我们把每个文本片段转换成一个高维数字向量(如 1536 维,就是把这个片段转为了 1536 个数字去代替)。
- 调用模型接口(如 OpenAI、阿里云 DashScope、DeepSeek 等)生成向量。语义相似的文本,它们的向量会比较接近。
- 比如:“我喜欢看电影” 和 “我爱看影片” 会变成两个很接近的向量,而和 “今天天气很好” 的距离就远很多。
-
存储
- 生成的向量和原始文本最好是保存起来,即便存储成本增加,但是在恢复的时候速度会比再向量化一次快,可以做持久化存储,方便后续检索,否则每次都要重新计算,效率太低。
- 把向量 + 文本内容 + 相关元信息(如文档 ID、页码)存到向量数据库。这里我们选择 Redis-Stack,上手快,而且存储也是 json,易于理解,也支持向量检索,后续熟悉之后会替换为 milvus。
- 存储时会像这样保存:
{docId: "001", chunk: "安装步骤…", embedding: [0.12, -0.33, …]}。
-
检索
- 用户提问时,AI 不能凭空知道答案,需要先找到相关资料。
- 把用户的问题也向量化,然后去数据库里找“最相似”的片段(Top-K 结果),再把这些片段交给大模型生成最终答案。
- 比如:用户问“怎么重置密码?”,系统会找到“常见问题”章节中的相关片段,再结合问题给出解答。
在本章中,我们将使用 Redis-Stack 作为向量数据库,完成上述流程的初步实现。
三、Redis-Stack 环境搭建
1. 为什么选择 Redis-Stack?
Redis-Stack 是一个扩展了 Redis 的开源数据库,支持向量搜索、JSON 存储等功能,适合快速原型开发。其优势包括:
- 轻量级:易于部署,适合小型项目或开发测试,只是扩展了redis,所以不需要引入太多。
- 向量搜索:支持高效的向量相似度检索(如余弦相似度)。
- Spring 集成:Spring AI 提供了对 Redis-Stack 的原生支持。
2. 安装 Redis-Stack
方法 1:Docker 部署(推荐)
运行以下命令启动 Redis-Stack 容器:
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack:latest
-
默认端口:6379。
-
验证安装:使用 redis-cli 连接 localhost:6379,运行 PING 命令,应返回 PONG。 连接也可以用redis-insight
3. 配置 Spring AI 与 Redis-Stack
在 pom.xml 中添加 Redis-Stack 的 Spring AI 依赖以及 openAI 依赖,因为 特性t-embedding-v要用阿里的apikey,是兼容openai的。阿里云百炼。处理 pdf 可引入依赖 pdfbox。
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- 显式引入兼容 PDFBox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.29</version>
</dependency>
在 application.yml 中配置 Redis 连接:
spring:
ai:
# OpenAI 兼容模式配置 阿里 文本向量化
openai:
api-key: sk-xxx
base-url: https://dashscope.aliyuncs.com/compatible-mode
embedding:
api-key: sk-xxx
base-url: https://dashscope.aliyuncs.com/compatible-mode
options:
model: text-embedding-v4
dimensions: 1536
encoding-format: float
user: "yanyy"
vectorstore:
redis:
prefix: "deepseek:"
index-name: "deepseek-index"
data:
redis:
host: 127.0.0.1
port: 6378
database: 0
jedis:
pool:
max-idle: 8
max-active: 16
max-wait: 10000
在配置后,spring-ai 会自动生成对应的索引,在 redis-stack 中查看是否生成 deepseek-index 索引 客户端命令为:
FT._LIST
查看索引结构(内容比较多,就不截图了)
FT.INFO deepseek-index
查看索引里的文档数量
FT.SEARCH deepseek-index "*" LIMIT 0 0
四、实现文档知识库雏形
1. 文本提取与切片
为简单起见,我们先以纯文本文件为例,提取内容并进行切片。后续章节将引入 Tika 处理多格式文档。
代码示例:读取文本文件并切片
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@Component
public class DocumentProcessor {
public List<String> processDocument(String filePath) throws IOException {
try (PDDocument document = PDDocument.load(new File(filePath))) {
PDFTextStripper stripper = new PDFTextStripper();
String content = stripper.getText(document);
// 简单切片:按固定长度(500 字符)切分
List<String> chunks = new ArrayList<>();
int chunkSize = 500;
for (int i = 0; i < content.length(); i += chunkSize) {
int end = Math.min(i + chunkSize, content.length());
chunks.add(content.substring(i, end));
}
return chunks;
}
}
}
说明:
- 按 500 字符切片,实际项目中可根据语义或段落调整策略。
- 后续章节将优化切片逻辑,考虑重叠和语义完整性。
2. 向量化与存储
使用阿里云 text-embedding-v4 将文本切片向量化,并存储到 Redis-Stack。这里我们还是自己配置 bean 配置类如下:
// ------------------------------ OpenAI API 配置 ---------------------------
@Value("${spring.ai.openai.base-url}")
private String openAiBaseUrl;
@Value("${spring.ai.openai.chat.options.model}")
private String openAiModelName;
@Value("${spring.ai.openai.api-key}")
private String openAiApiKey;
@Bean(name = "openAiApi")
public OpenAiApi openAiApi() {
return OpenAiApi.builder()
.apiKey(openAiApiKey)
.baseUrl(openAiBaseUrl)
.build();
}
// ------------------------------ textEmbeddingModel 配置 ---------------------------
@Value("${spring.ai.openai.embedding.options.model}")
private String textEmbeddingModelName;
@Value("${spring.ai.openai.embedding.options.dimensions}")
private int textEmbeddingModelDimensions;
@Value("${spring.ai.openai.embedding.options.encoding-format}")
private String textEmbeddingModelEncodingFormat;
@Value("${spring.ai.openai.embedding.options.user}")
private String textEmbeddingModelUser;
/**
* 文本 Embedding 模型
*/
@Bean(name = "textEmbeddingModel")
public OpenAiEmbeddingModel textEmbeddingModel(@Qualifier("openAiApi") OpenAiApi openAiApi) {
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
.model(textEmbeddingModelName)
.dimensions(textEmbeddingModelDimensions)
.encodingFormat(textEmbeddingModelEncodingFormat)
.user(textEmbeddingModelUser)
.build();
return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, options);
}
@Bean
public JedisPooled jedisPooled(RedisConnectionDetails redisConnectionDetails) {
// 包括主机、端口、用户名和密码,这些信息是连接Redis服务器所必需的
return new JedisPooled(redisConnectionDetails.getStandalone().getHost(),
redisConnectionDetails.getStandalone().getPort(),
redisConnectionDetails.getUsername(),
redisConnectionDetails.getPassword());
}
private MyRedisVectorStore createRedisVectorStore(
JedisPooled jedisPooled,
EmbeddingModel embeddingModel,
RedisVectorStoreProperties properties
) {
List<RedisVectorStore.MetadataField> metadataFields = List.of(
RedisVectorStore.MetadataField.text("repositoryId"),
RedisVectorStore.MetadataField.text("documentId"),
RedisVectorStore.MetadataField.text("chunkId"),
RedisVectorStore.MetadataField.text("available"),
RedisVectorStore.MetadataField.text("type")
);
return MyRedisVectorStore.myBuilder(jedisPooled, embeddingModel)
.indexName(properties.getIndexName())
.prefix(properties.getPrefix())
.embeddingFieldName("embedding")
.contentFieldName("content")
.batchingStrategy(new TokenCountBatchingStrategy())
.metadataFields(metadataFields)
.initializeSchema(true)
.build();
}
private MyMilvusVectorStore createMilvusVectorStore(
MilvusServiceClient milvusClient,
EmbeddingModel embeddingModel,
String collectionName,
int embeddingDimension,
String databaseName,
Boolean autoId,
Boolean initializeSchema
) {
return MyMilvusVectorStore.myBuilder(milvusClient, embeddingModel)
.initializeSchema(initializeSchema)
.databaseName(databaseName)
.collectionName(collectionName)
.embeddingDimension(embeddingDimension)
.autoId(autoId)
.build();
}
// ---------------- Text ----------------
@Bean(name = "myTextRedisVectorStore")
public MyRedisVectorStore myTextRedisVectorStore(
JedisPooled jedisPooled,
@Qualifier("textEmbeddingModel") EmbeddingModel embeddingModel,
RedisVectorStoreProperties properties) {
return createRedisVectorStore(jedisPooled, embeddingModel, properties);
}
其中 MyRedisVectorStore 是为了后续扩展 spring-ai 的 RedisVectorStore,rehandle 方法可以直接将持久化后的 embedding 向量恢复到 redis 中。这是为了减少成本,官方的 add 方法会在调用的时候再重新生成一次向量,在实际应用大多时候是增量或者恢复,所以扩展该方法。
public class MyRedisVectorStore extends RedisVectorStore {
private static final String embeddingFieldName = "embedding";
private static final String contentFieldName = "content";
private static final Path2 JSON_SET_PATH = Path2.of("$");
private static final Predicate<Object> RESPONSE_OK = Predicate.isEqual("OK");
private static final Logger logger = LoggerFactory.getLogger(MyRedisVectorStore.class);
private static final String prefix = "deepseek:";
protected MyRedisVectorStore(RedisVectorStore.Builder builder) {
super(builder);
}
/**
* 自定义处理逻辑:补充 metadata 或过滤无效文档
*/
public void rehandle(List<Document> oDocuments) {
List<Document> documents = new ArrayList<>(oDocuments);
try (Pipeline pipeline = this.getJedis().pipelined()) {
for (Document document : documents) {
var fields = new HashMap<String, Object>();
// ---- 处理 embedding ----
Object embeddingObj = document.getMetadata().remove("embedding");
if (embeddingObj == null) {
throw new UsrReqErr("文档 " + document.getId() + " 没有 embedding 数据");
}
byte[] embeddingBlob;
if (embeddingObj instanceof List<?> list) {
logger.info("文档 {} 的 embedding 是 List 类型,长度: {}", document.getId(), list.size());
embeddingBlob = toFloat32Blob(list);
} else if (embeddingObj instanceof float[] arr) {
logger.info("文档 {} 的 embedding 是 float[] 类型,长度: {}", document.getId(), arr.length);
embeddingBlob = toFloat32Blob(arr);
} else if (embeddingObj instanceof double[] arr) {
logger.info("文档 {} 的 embedding 是 double[] 类型,长度: {}", document.getId(), arr.length);
embeddingBlob = toFloat32Blob(arr);
} else {
logger.error("文档 {} 的 embedding 格式不支持: {}", document.getId(), embeddingObj.getClass());
throw new UsrReqErr("文档 " + document.getId() + " 的 embedding 格式不支持: " + embeddingObj.getClass());
}
fields.put(embeddingFieldName, embeddingBlob);
// ---- 写入内容 ----
fields.put(contentFieldName, Optional.ofNullable(document.getText()).orElse(""));
// ---- 写入元数据 ----
fields.putAll(document.getMetadata());
pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
}
List<Object> responses = pipeline.syncAndReturnAll();
Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny();
if (errResponse.isPresent()) {
String message = MessageFormat.format("Could not rehandleMultimodal document: {0}", errResponse.get());
if (logger.isErrorEnabled()) {
logger.error(message);
}
throw new UsrReqErr(message);
}
}
}
/**
* 将 List<?> 转换成 float32 小端字节数组
*/
private byte[] toFloat32Blob(List<?> list) {
ByteBuffer byteBuffer = ByteBuffer.allocate(4 * list.size()).order(ByteOrder.LITTLE_ENDIAN);
for (Object obj : list) {
float v = ((Number) obj).floatValue();
byteBuffer.putFloat(v);
}
return byteBuffer.array();
}
/**
* 将 float[] 转换成 float32 小端字节数组
*/
private byte[] toFloat32Blob(float[] arr) {
ByteBuffer byteBuffer = ByteBuffer.allocate(4 * arr.length).order(ByteOrder.LITTLE_ENDIAN);
for (float v : arr) {
byteBuffer.putFloat(v);
}
return byteBuffer.array();
}
/**
* 将 double[] 转换成 float32 小端字节数组
*/
private byte[] toFloat32Blob(double[] arr) {
ByteBuffer byteBuffer = ByteBuffer.allocate(4 * arr.length).order(ByteOrder.LITTLE_ENDIAN);
for (double v : arr) {
byteBuffer.putFloat((float) v);
}
return byteBuffer.array();
}
private String key(String id) {
return prefix + id;
}
/**
* 自定义 Builder,使用组合方式复用父类 Builder
*/
public static class MyBuilder {
private final RedisVectorStore.Builder parentBuilder;
public MyBuilder(JedisPooled jedis, EmbeddingModel embeddingModel) {
this.parentBuilder = RedisVectorStore.builder(jedis, embeddingModel);
}
public MyBuilder indexName(String indexName) {
this.parentBuilder.indexName(indexName);
return this;
}
public MyBuilder prefix(String prefix) {
this.parentBuilder.prefix(prefix);
return this;
}
public MyBuilder embeddingFieldName(String embeddingFieldName) {
this.parentBuilder.embeddingFieldName(embeddingFieldName);
return this;
}
public MyBuilder contentFieldName(String contentFieldName) {
this.parentBuilder.contentFieldName(contentFieldName);
return this;
}
public MyBuilder initializeSchema(boolean initializeSchema) {
this.parentBuilder.initializeSchema(initializeSchema);
return this;
}
public MyBuilder batchingStrategy(BatchingStrategy batchingStrategy) {
this.parentBuilder.batchingStrategy(batchingStrategy);
return this;
}
public MyBuilder metadataFields(List<RedisVectorStore.MetadataField> metadataFields) {
this.parentBuilder.metadataFields(metadataFields);
return this;
}
public MyRedisVectorStore build() {
return new MyRedisVectorStore(this.parentBuilder);
}
}
// 提供静态工厂方法
public static MyBuilder myBuilder(JedisPooled jedis, EmbeddingModel embeddingModel) {
return new MyBuilder(jedis, embeddingModel);
}
}
代码示例:向量化并存储,因为向量化的时候一次最多接收10个切片,所以在处理切片的时候10个为一组去embedding 然后添加到 redis-stack 中
private final ChatClient chatClient;
private final MyRedisVectorStore myTextRedisVectorStore;
public RagDocumentController(@Qualifier("deepseekV3ClientNoMemory") ChatClient chatClient,
@Qualifier("myTextRedisVectorStore") MyRedisVectorStore myTextRedisVectorStore) {
this.chatClient = chatClient;
this.myTextRedisVectorStore = myTextRedisVectorStore;
}
private static final String TEST_DOCUMENT = "D:\mydownload\测试文档\测试.pdf";
@PostMapping(value = "/chat/document-chunk-embedding", produces = "application/json")
public Result<Integer> testBuiltChunk() {
try {
List<String> chunkList = this.processDocument(TEST_DOCUMENT);
List<org.springframework.ai.document.Document> documentList = new ArrayList<>();
for (String chunk : chunkList) {
org.springframework.ai.document.Document document = new Document(chunk);
documentList.add(document);
}
if (documentList.size() >= 10) {
myTextRedisVectorStore.add(documentList);
documentList.clear();
}
if (!documentList.isEmpty()) {
myTextRedisVectorStore.add(documentList);
}
return Result.success(chunkList.size());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public List<String> processDocument(String filePath) throws IOException {
try (PDDocument document = PDDocument.load(new File(filePath))) {
PDFTextStripper stripper = new PDFTextStripper();
String content = stripper.getText(document);
// 简单切片:按固定长度(500 字符)切分
List<String> chunks = new ArrayList<>();
int chunkSize = 500;
for (int i = 0; i < content.length(); i += chunkSize) {
int end = Math.min(i + chunkSize, content.length());
chunks.add(content.substring(i, end));
}
return chunks;
}
}
说明:
- MyRedisVectorStore:将向量和文本内容存储到 Redis-Stack,并自动创建索引。
- org.springframework.ai.document.Document:Spring AI 提供的文档对象,包含文本和嵌入向量。 运行后可查看,以下为截图
3. 简单检索测试
实现一个简单的检索功能,验证知识库是否正常工作。
spring-ai 已经有通用的 org.springframework.ai.vectorstore.SearchRequest,对于 redis-stack 的简单检索,具体实现可以看源码的 similaritySearch() 方法,实现了对用户输入的内容进行 embedding,然后去检索,还可以添加一些具体的查询条件(在初始化的时候要设置哪些字段带索引,此处只是简单示例,后续会有字段索引,可以实现权限控制之类的),
以下是一个示例
代码示例:检索文档
@PostMapping("/chat/document-search")
public Result<List<String>> search(@RequestParam String query) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(3)
.build();
final List<Document> documents = myTextRedisVectorStore.similaritySearch(request);
List<String> result = new ArrayList<>();
for (Document document : documents) {
result.add(document.getText());
}
return Result.success(result);
}
测试:
- 调用 addDocument 方法,上传一个文本文件(如包含“AI 知识库简介”的文档)。
- 访问 http://localhost:8080/search?query=什么是 AI 知识库,返回最相关的 3 个文本片段。
apifox 调试
此时索引到的内容就可以设置为deepseek 的上下文,他可以根据索引到的内容进行回答,而不是胡编乱造,这样就是一个简易的 rag,后续的优化方向就是以下这几处
五、常见问题与优化
文档切片优化
支持多格式,比如doc,pdf,excel,ppt,md等,大致分为 文本类,表格类,ppt。
-
文本类主要是切片大小,重叠大小。还有对于其中的图片怎么处理,一般是 ocr(本地或者使用阿里云,阿里云最好还是把其他格式转为 pdf 文档,效果比较好),可以获取图片内容,确保文档的内容尽量完善。
-
对于 excel,要考虑有是否为多 sheet,是一行数据一个切片还是多行,每个切片是否保留表头(保留表头虽然成本高一点,但是检索效果更好)。
-
ppt就直接按幻灯片来处理,注意的点还是对于其中图片的处理。
向量化及存储
- 向量化目前来看,在切片大小合适的时候,还是尽量高维度。国内比较成熟的有豆包和阿里可以选用。
- 对于存储,量级不大redis-stack完全够用,如果要考虑后续扩展,业务量上升,还是直接弄 milvus,社区比较完善。
- 目前官方只设置了两个字段 content(文本) embedding(向量), 可以在这里扩展。比如你的层级是知识库(repository) -> 知识库文档(repository_document),那么可以添加字段 repository_id 或者 repository_document_id,给字段建立索引,后续查询时就可以直接用。针对 redis-stack
private MyRedisVectorStore createRedisVectorStore(
JedisPooled jedisPooled,
EmbeddingModel embeddingModel,
RedisVectorStoreProperties properties
) {
List<RedisVectorStore.MetadataField> metadataFields = List.of(
RedisVectorStore.MetadataField.text("repositoryId"),
RedisVectorStore.MetadataField.text("documentId"),
RedisVectorStore.MetadataField.text("chunkId"),
RedisVectorStore.MetadataField.text("available"),
RedisVectorStore.MetadataField.text("type")
);
return MyRedisVectorStore.myBuilder(jedisPooled, embeddingModel)
.indexName(properties.getIndexName())
.prefix(properties.getPrefix())
.embeddingFieldName("embedding")
.contentFieldName("content")
.batchingStrategy(new TokenCountBatchingStrategy())
.metadataFields(metadataFields)
.initializeSchema(true)
.build();
}
该代码前文已有,其中 repositoryId 等已经建立了索引,在向量化时可以设置对应值:
@PostMapping(value = "/chat/document-chunk-embedding", produces = "application/json")
public Result<Integer> testBuiltChunk() {
try {
List<String> chunkList = this.processDocument(TEST_DOCUMENT);
List<org.springframework.ai.document.Document> documentList = new ArrayList<>();
for (String chunk : chunkList) {
org.springframework.ai.document.Document document = new Document(chunk);
document.getMetadata().put("repositoryId", "1");
document.getMetadata().put("documentId", "test-doc-001");
document.getMetadata().put("available", "1");
documentList.add(document);
if (documentList.size() >= 10) {
myTextRedisVectorStore.add(documentList);
documentList.clear();
}
}
if (!documentList.isEmpty()) {
myTextRedisVectorStore.add(documentList);
}
return Result.success(chunkList.size());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
使用为
@PostMapping(value = "/chat/document-search", produces = "application/json")
public Result<List<String>> search(@RequestParam String query) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
String[] repoIdArray = new String[]{"1", "2"};
List<Object> values = List.of(repoIdArray);
org.springframework.ai.vectorstore.filter.Filter.Expression filter = builder.and(
builder.in("repositoryId", values),
builder.in("available", "1"))
.build();
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(3)
.filterExpression(filter)
.build();
final List<Document> documents = myTextRedisVectorStore.similaritySearch(request);
List<String> result = new ArrayList<>();
for (Document document : documents) {
result.add(document.getText());
}
return Result.success(result);
}
添加 filter 就可以,需要注意的是在初始化如果是 String,那么在查询时用 int 就会失效,必须转为 String 再去构造条件。
持久化存储,可以把整个 Document 都存入持久化数据库中,包括 content,embedding,以及你需要的内容,整体就是一个 json。
检索
- Prompt
对于用户输入的内容,可以添加一个 Prompt 优化构造器,扩充完善用户的问题,这里涉及到对于前置对话的存储,即给 chatClient 记忆,官方已实现该功能,后续会给出示例
- 检索
可以将切片后的metadata存入 elasticsearch,使用 bm25 关键词检索,可以和向量检索并行,也可以先关键词检索再向量检索,都可以提高准确率
六、总结与下一步
通过本章,我们搭建了一个文档知识库的雏形,完成了:
- RAG 技术的基本理解。
- Redis-Stack 环境的搭建与配置。
- 文本切片、向量化、存储与检索的初步实现。
虽然当前知识库仅支持简单文本和检索,但已为后续功能奠定了基础。之后将引入 Tika 解析多格式文档(PDF、Word、Excel 等),并处理复杂 PDF 的 OCR 需求,进一步丰富知识库的内容来源。