模块化 RAG 大幅提升了检索质量,但底层的 SimpleVectorStore 还是内存存储,应用一重启,所有向量数据就没了。是时候把存储层升级到一个真正的向量数据库,让 RAG 系统具备生产级的持久化能力。
1. 为什么升级
先看看 SimpleVectorStore 的局限:
| 维度 | SimpleVectorStore | 生产级向量数据库 |
|---|---|---|
| 数据持久化 | 内存中,重启丢失 | 磁盘持久化 |
| 保存/恢复 | 需手动调用 save/load | 自动持久化 |
| 并发访问 | 不支持 | 多线程/多进程安全 |
| 数据规模 | 受 JVM 内存限制 | 可存储百万级向量 |
| 过滤查询 | 内存遍历 | 原生索引加速 |
| 索引优化 | 无(暴力搜索) | HNSW / IVF 等近似最近邻索引 |
| 生产部署 | 不推荐 | 生产就绪 |
SimpleVectorStore 的定位很明确:快速原型和教学演示。一旦进入实际开发,就需要一个真正的向量数据库。
2. 选择 Qdrant 还是 PGVector
Spring AI 支持十余种向量数据库,自托管场景下 PGVector 和 Qdrant 是最常被对比的选择。
2.1 PGVector:零成本扩展,但有硬伤
作为 PostgreSQL 的向量扩展,它的核心优势是复用现有基础设施 ,已有 PG 的项目只需启用扩展即可获得向量能力,无需引入新组件。
2.2 Qdrant:专业向量数据库
专为向量检索设计(Rust 编写),优势明确:
| 特性 | PGVector | Qdrant |
|---|---|---|
| 部署 | 依赖 PostgreSQL | 单 Docker 容器 |
| HNSW 维度上限 | 2000 | 65536 |
| 4096 维支持 | ❌ 不支持 | ✅ 原生支持 |
| 元数据过滤 | JSONB 查询 | 原生 payload 索引 |
| 可视化 | 需第三方工具 | 自带 Web Dashboard |
| Spring AI 支持 | 需额外配置 | spring-ai-starter-vector-store-qdrant 开箱即用 |
本专栏选择 Qdrant,原因:
- 完美支持 4096 维 HNSW 索引;
- Web Dashboard 大幅降低学习和调试成本;
3. Docker Compose 启动 Qdrant
在项目根目录创建 docker-compose.yml:
services:
qdrant:
image: qdrant/qdrant:v1.14.0
restart: unless-stopped
ports:
- "6333:6333" # REST API + Web Dashboard
- "6334:6334" # gRPC(Spring AI 使用)
volumes:
- qdrant_data:/qdrant/storage
volumes:
qdrant_data:配置说明:
- qdrant/qdrant:v1.14.0:官方镜像,固定 v1.14.0 以兼容 Spring AI 2.0.0-M3 内置的 Qdrant 客户端(1.13.x),minor 版本差控制在 1 以内
- 6333 端口:REST API 与 Web Dashboard,访问
http://localhost:6333/dashboard查看数据 - 6334 端口:gRPC 接口,Spring AI 连接用
- qdrant_data 卷:持久化存储,容器重建数据不丢失
启动服务:
docker compose up -d验证服务运行:
curl http://localhost:6333/返回 JSON 包含 Qdrant 版本信息说明启动成功。
4. 添加 Qdrant 依赖
在 pom.xml 中添加 Qdrant starter:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
</dependency>这个 starter 会自动配置:
- QdrantClient:基于 gRPC 的客户端,根据配置的 host/port 连接 Qdrant
- QdrantVectorStore Bean:实现
VectorStore接口,替代 SimpleVectorStore - Schema 初始化:根据
EmbeddingModel的输出维度自动创建 collection
5. 配置 Qdrant
更新 src/main/resources/application.yaml,添加 Qdrant 配置:
spring:
application:
name: rag-demo
ai:
ollama:
base-url: http://localhost:11434
init:
pull-model-strategy: when_missing
chat:
model: qwen3.5:9b
embedding:
model: qwen3-embedding:8b
vectorstore:
qdrant:
host: localhost
port: 6334
collection-name: rag_demo
initialize-schema: true
server:
port: 8080新增配置说明:
- host / port:Qdrant 的 gRPC 地址,端口默认是 6334(注意不是 REST 的 6333)
- collection-name:collection 名称,相当于关系数据库的"表",每个 collection 存储一组维度相同的向量
- initialize-schema: true:启动时自动创建 collection。维度从
EmbeddingModel推断(这里是 4096),距离度量默认 Cosine
6. 自动建集合
当 initialize-schema: true 时,QdrantVectorStore 在 Bean 初始化阶段会通过 gRPC 连接 Qdrant,检查指定的 collection 是否存在,不存在则自动创建。创建时会从容器中的 EmbeddingModel Bean 读取向量维度,并使用 Cosine 距离作为相似度度量。
应用启动后,可以通过 Web Dashboard 查看创建的 collection:
open http://localhost:6333/dashboard在 Dashboard 中可以看到名为 rag_demo 的 collection,向量维度 4096,距离度量 Cosine。
7. 更新 AiConfig
src/main/java/com/albertstack/rag/config/AiConfig.java
package com.albertstack.rag.config;
import com.albertstack.rag.service.IngestionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.util.List;
@Slf4j
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
@Bean
public RetrievalAugmentationAdvisor ragAdvisor(
ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
return RetrievalAugmentationAdvisor.builder()
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder).build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5)
.topK(5)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.build();
}
@Bean
public CommandLineRunner ingestDocuments(IngestionService ingestionService) {
return args -> {
List<String> files = List.of(
"docs/spring-ai-intro.txt",
"docs/rag-concepts.txt",
"docs/ollama-guide.txt"
);
for (String file : files) {
var resource = new ClassPathResource(file);
int chunks = ingestionService.ingest(resource);
log.info("已摄入: {} -> {} 个分块", file, chunks);
}
};
}
}相比上一章的版本:
- 删除手动创建的
SimpleVectorStoreBean 及对应 import QdrantVectorStoreAutoConfiguration自动注册QdrantVectorStore,依赖(EmbeddingModel、QdrantClient)由 Spring 容器自动注入IngestionService、RagService、Controller 依赖的都是VectorStore接口,无需改动
8. 测试
运行测试前确保 Qdrant 容器已启动:
docker compose ps8.1 写入与搜索
验证文档写入 Qdrant 后能正常检索。
src/test/java/com/albertstack/rag/QdrantWriteAndSearchTest.java
package com.albertstack.rag;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
class QdrantWriteAndSearchTest {
@Autowired
VectorStore vectorStore;
@BeforeEach
void setUp() {
// 清理上次运行残留的数据,保证测试可重复执行
var filter = new FilterExpressionBuilder();
vectorStore.delete(filter.in("source",
"qdrant-intro.txt", "hnsw-guide.txt", "spring-boot-basics.txt").build());
vectorStore.add(List.of(
new Document(
"Qdrant 是用 Rust 编写的高性能向量数据库,支持高维向量的近似最近邻搜索。",
Map.of("source", "qdrant-intro.txt", "type", "tutorial")),
new Document(
"HNSW 索引是一种高效的近似最近邻搜索算法,查询速度接近常数时间。",
Map.of("source", "hnsw-guide.txt", "type", "reference")),
new Document(
"Spring Boot 的自动配置机制可以根据依赖自动注册相应的 Bean。",
Map.of("source", "spring-boot-basics.txt", "type", "tutorial"))
));
}
@Test
void shouldPersistAndSearchDocuments() {
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("向量数据库搜索")
.topK(2)
.build());
log.info("搜索结果数量: {}", results.size());
results.forEach(doc -> log.info("- [{}] {}", doc.getMetadata().get("source"), doc.getText()));
assertThat(results).isNotEmpty();
assertThat(results).hasSizeLessThanOrEqualTo(2);
}
}8.2 过滤表达式
Qdrant 支持基于 metadata 的过滤查询,Spring AI 的 FilterExpressionBuilder 会把表达式翻译为 Qdrant 原生的 Filter 结构,利用 payload 索引高效执行。
src/test/java/com/albertstack/rag/QdrantFilterTest.java
package com.albertstack.rag;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
class QdrantFilterTest {
@Autowired
VectorStore vectorStore;
@BeforeEach
void setUp() {
// 清理上次运行残留的数据,保证测试可重复执行
var filter = new FilterExpressionBuilder();
vectorStore.delete(filter.in("source",
"spring-ai-intro.txt", "rag-concepts.txt", "qdrant-guide.txt").build());
vectorStore.add(List.of(
new Document(
"Spring AI 提供了统一的 API 来对接各种大模型。",
Map.of("source", "spring-ai-intro.txt", "type", "tutorial", "chapter", 1)),
new Document(
"RAG 通过检索外部知识来增强大模型的回答质量。",
Map.of("source", "rag-concepts.txt", "type", "tutorial", "chapter", 3)),
new Document(
"Qdrant 的 HNSW 索引适用于高维向量的快速检索。",
Map.of("source", "qdrant-guide.txt", "type", "reference", "chapter", 8))
));
}
@Test
void shouldFilterByEquality() {
// 按 type 精确匹配
var filter = new FilterExpressionBuilder();
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("Spring AI")
.topK(5)
.filterExpression(filter.eq("type", "tutorial").build())
.build());
log.info("type=tutorial 结果数: {}", results.size());
results.forEach(doc -> log.info("- [{}] {}", doc.getMetadata().get("source"), doc.getText()));
// 过滤条件生效,只返回 tutorial 类型
results.forEach(doc ->
assertThat(doc.getMetadata().get("type")).isEqualTo("tutorial"));
}
@Test
void shouldFilterByAndCondition() {
// AND 组合:同时满足 type 和 source
var filter = new FilterExpressionBuilder();
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("Spring AI")
.topK(5)
.filterExpression(filter.and(
filter.eq("type", "tutorial"),
filter.eq("source", "spring-ai-intro.txt")
).build())
.build());
log.info("AND 过滤结果数: {}", results.size());
results.forEach(doc -> {
assertThat(doc.getMetadata().get("type")).isEqualTo("tutorial");
assertThat(doc.getMetadata().get("source")).isEqualTo("spring-ai-intro.txt");
});
}
@Test
void shouldFilterByInExpression() {
// IN 查询:source 在指定列表中
var filter = new FilterExpressionBuilder();
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("Spring AI")
.topK(5)
.filterExpression(filter.in("source",
"spring-ai-intro.txt", "rag-concepts.txt").build())
.build());
log.info("IN 过滤结果数: {}", results.size());
results.forEach(doc -> {
String source = (String) doc.getMetadata().get("source");
log.info("- [{}] {}", source, doc.getText());
assertThat(source).isIn("spring-ai-intro.txt", "rag-concepts.txt");
});
}
@Test
void shouldFilterByComparison() {
// 比较运算:chapter >= 3
var filter = new FilterExpressionBuilder();
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("向量搜索")
.topK(5)
.filterExpression(filter.gte("chapter", 3).build())
.build());
log.info("chapter >= 3 结果数: {}", results.size());
results.forEach(doc -> {
// Qdrant 内部把整数 payload 存为 Long,用 Number 接收避免类型不匹配
int chapter = ((Number) doc.getMetadata().get("chapter")).intValue();
log.info("- chapter={}, {}", chapter, doc.getText());
assertThat(chapter).isGreaterThanOrEqualTo(3);
});
}
}FilterExpressionBuilder 是 Spring AI 提供的统一过滤抽象,同样的代码在 Qdrant、PGVector、Milvus 等不同存储上都能工作。Spring AI 内部会把表达式翻译为各数据库的原生查询结构。
9. Web Dashboard
Qdrant 自带的 Web Dashboard 是开发调试的利器。在浏览器打开:
http://localhost:6333/dashboard
在 Dashboard 中可以:
- 查看所有 collection 及其配置(向量维度、距离度量、点数量)
- 浏览每个 collection 中存储的向量点(point),包括 metadata payload
- 用控制台直接执行 Qdrant 的 REST API
- 监控 Qdrant 实例的运行状态
测试运行后,进入 rag_demo collection,可以看到刚才写入的文档点,每个点的 payload 包含 source、type、chapter 等元数据。
10. 本章小结
完成了从 SimpleVectorStore 到 Qdrant 的升级。整个过程只需要换依赖、改配置、删除手动创建的 SimpleVectorStore Bean,业务代码(Service、Controller)完全不用动,这就是面向 VectorStore 接口编程的好处。
| 知识点 | 说明 |
|---|---|
| Qdrant 选型 | 专为向量检索设计,HNSW 默认支持高维向量,避开 PGVector 2000 维上限 |
| Docker Compose | 单容器启动,pin v1.14.0 与 Spring AI 客户端版本兼容 |
| 依赖切换 | 添加 spring-ai-starter-vector-store-qdrant,替换内存存储 |
| 配置更新 | host + port + collection-name + initialize-schema |
| AiConfig 更新 | 删除 SimpleVectorStore Bean,由 auto-configuration 自动注册 QdrantVectorStore |
| 过滤表达式 | FilterExpressionBuilder 支持 eq、and、in、gte 等,跨数据库通用 |
| Web Dashboard | http://localhost:6333/dashboard,可视化 collection 和数据 |
接下来把所有组件整合起来,构建一个结构清晰的 RAG API 服务。