跳到主要内容

向量存储

向量存储是 RAG 应用的核心组件,用于存储文档嵌入向量并支持相似度搜索。本章介绍 Spring AI 中的向量存储。

概述

向量存储负责:

  • 存储文档及其嵌入向量
  • 执行相似度搜索
  • 管理文档元数据
┌─────────────────────────────────────────────────────────────┐
│ 向量存储工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 文档 ───> Embedding 模型 ───> 向量 [0.1, 0.2, ...] │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 向量存储 │ │
│ │ (Vector │ │
│ │ Store) │ │
│ └──────┬──────┘ │
│ │ │
│ 查询 ───> Embedding 模型 ───> 向量搜索 │
│ │ │
│ ▼ │
│ 相似文档列表 │
│ │
└─────────────────────────────────────────────────────────────┘

支持的向量数据库

Spring AI 支持多种向量数据库:

数据库特点适用场景
PGVectorPostgreSQL 扩展已有 PG 环境
Redis高性能,支持持久化缓存场景
Milvus分布式,高性能大规模生产
Chroma轻量级,易部署开发测试
Pinecone托管服务无运维需求
Qdrant高性能,Rust 实现高并发场景
Weaviate语义搜索强复杂搜索
Elasticsearch全文+向量混合搜索

基本使用

VectorStore 接口

public interface VectorStore {
// 添加文档
void add(List<Document> documents);

// 删除文档
void delete(List<String> ids);

// 相似度搜索
List<Document> similaritySearch(String query);

List<Document> similaritySearch(SearchRequest request);
}

添加文档

@Service
public class DocumentService {

@Autowired
private VectorStore vectorStore;

public void addDocument(String content, Map<String, Object> metadata) {
Document document = new Document(content, metadata);
vectorStore.add(List.of(document));
}

public void addDocuments(List<String> contents) {
List<Document> documents = contents.stream()
.map(Document::new)
.toList();
vectorStore.add(documents);
}
}

相似度搜索

@GetMapping("/search")
public List<DocumentResult> search(@RequestParam String query,
@RequestParam(defaultValue = "5") int topK) {

SearchRequest request = SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(0.7); // 相似度阈值

List<Document> documents = vectorStore.similaritySearch(request);

return documents.stream()
.map(doc -> new DocumentResult(
doc.getId(),
doc.getContent(),
doc.getMetadata(),
doc.getScore()
))
.toList();
}

record DocumentResult(String id, String content, Map<String, Object> metadata, Double score) {}

数据库配置

PGVector

依赖

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>

配置

spring:
datasource:
url: jdbc:postgresql://localhost:5432/ai_db
username: postgres
password: postgres
ai:
vectorstore:
pgvector:
index-type: HNSW # 索引类型:HNSW, IVFFlat
distance-type: COSINE_DISTANCE # 距离类型
dimensions: 1536 # 向量维度
remove-existing-vector-store-table: false
initialize-schema: true

初始化

-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;

-- 创建表
CREATE TABLE IF NOT EXISTS vector_store (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
content text,
metadata json,
embedding vector(1536)
);

-- 创建索引
CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops);

Redis

依赖

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
</dependency>

配置

spring:
data:
redis:
host: localhost
port: 6379
password:
database: 0
ai:
vectorstore:
redis:
uri: redis://localhost:6379
index: spring-ai-index
prefix: "document:"

Milvus

依赖

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
</dependency>

配置

spring:
ai:
vectorstore:
milvus:
client:
host: localhost
port: 1953
databaseName: default
collectionName: spring_ai_documents
embeddingDimension: 1536
indexType: IVF_FLAT
metricType: COSINE

Chroma

依赖

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-chroma-store-spring-boot-starter</artifactId>
</dependency>

配置

spring:
ai:
vectorstore:
chroma:
client:
host: http://localhost:8000
collectionName: spring_ai_collection
initialize-schema: true

简单内存存储

用于开发和测试:

@Configuration
public class VectorStoreConfig {

@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}

元数据过滤

基本过滤

// 等于
SearchRequest request = SearchRequest.query(query)
.withTopK(5)
.withFilterExpression("category == 'technical'");

// 不等于
SearchRequest request = SearchRequest.query(query)
.withFilterExpression("category != 'deprecated'");

// 数值比较
SearchRequest request = SearchRequest.query(query)
.withFilterExpression("year >= 2023");

复合过滤

// AND 条件
SearchRequest request = SearchRequest.query(query)
.withFilterExpression("category == 'java' && year >= 2023");

// OR 条件
SearchRequest request = SearchRequest.query(query)
.withFilterExpression("category == 'java' || category == 'spring'");

// IN 条件
SearchRequest request = SearchRequest.query(query)
.withFilterExpression("category in ['java', 'spring', 'kotlin']");

// 模糊匹配
SearchRequest request = SearchRequest.query(query)
.withFilterExpression("title like '%Spring%'");

使用 FilterExpressionBuilder

FilterExpressionBuilder b = new FilterExpressionBuilder();

// 构建复杂表达式
Filter.Expression expression = b.and(
b.eq("category", "technical"),
b.gte("year", 2023)
).build();

SearchRequest request = SearchRequest.query(query)
.withFilterExpression(expression);

自定义 Embedding 模型

配置 OpenAI Embedding

spring:
ai:
openai:
embedding:
options:
model: text-embedding-3-small

使用 Ollama 本地模型

spring:
ai:
ollama:
embedding:
model: nomic-embed-text
options:
dimension: 768

自定义 Embedding 模型

@Service
public class CustomEmbeddingService {

@Autowired
private EmbeddingModel embeddingModel;

public float[] getEmbedding(String text) {
EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text));
return response.getResult().getOutput();
}

public List<float[]> getEmbeddings(List<String> texts) {
EmbeddingResponse response = embeddingModel.embedForResponse(texts);
return response.getResults().stream()
.map(result -> result.getOutput())
.toList();
}
}

批量操作

批量添加文档

@Service
public class BulkDocumentService {

@Autowired
private VectorStore vectorStore;

@Autowired
private TokenTextSplitter textSplitter;

public void bulkImport(List<Document> documents, int batchSize) {
// 分批处理
Lists.partition(documents, batchSize).forEach(batch -> {
vectorStore.add(batch);
});
}

public void importFromFile(Path filePath) throws IOException {
String content = Files.readString(filePath);

Document document = new Document(content, Map.of(
"filename", filePath.getFileName().toString(),
"path", filePath.toString()
));

// 分割大文档
List<Document> chunks = textSplitter.apply(List.of(document));

vectorStore.add(chunks);
}
}

删除文档

@Service
public class DocumentCleanupService {

@Autowired
private VectorStore vectorStore;

public void deleteByIds(List<String> ids) {
vectorStore.delete(ids);
}

public void deleteByMetadata(String key, String value) {
// 某些向量库支持按元数据删除
// 具体实现取决于使用的向量库
}
}

监控和调试

查询向量库状态

@GetMapping("/stats")
public Map<String, Object> getStats() {
// 注意:具体实现取决于使用的向量库
return Map.of(
"totalDocuments", "N/A", // 需要根据具体实现
"lastUpdate", LocalDateTime.now()
);
}

调试嵌入向量

@GetMapping("/debug/embedding")
public Map<String, Object> debugEmbedding(@RequestParam String text) {
EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text));
float[] embedding = response.getResult().getOutput();

return Map.of(
"text", text,
"dimension", embedding.length,
"embedding", Arrays.toString(Arrays.copyOf(embedding, 10)) + "..."
);
}

最佳实践

1. 选择合适的维度

// 高维度:更精确,但存储和计算成本高
// 低维度:更快,但可能损失精度

// OpenAI text-embedding-3-small: 1536
// OpenAI text-embedding-3-large: 3072
// Ollama nomic-embed-text: 768

2. 合理设置相似度阈值

// 根据业务场景调整
double threshold = switch (useCase) {
case "precise" -> 0.9; // 精确匹配
case "balanced" -> 0.7; // 平衡
case "broad" -> 0.5; // 宽泛搜索
default -> 0.7;
};

SearchRequest request = SearchRequest.query(query)
.withSimilarityThreshold(threshold);

3. 添加丰富的元数据

Document document = new Document(content, Map.of(
"title", "Spring AI 入门教程",
"category", "tutorial",
"tags", List.of("java", "spring", "ai"),
"author", "张三",
"createdAt", LocalDateTime.now().toString(),
"language", "zh-CN",
"source", "official-docs",
"version", "1.0.0"
));

小结

本章我们学习了:

  1. 向量存储概念:存储和检索嵌入向量
  2. 支持的数据库:PGVector、Redis、Milvus 等
  3. 基本操作:添加、删除、搜索文档
  4. 元数据过滤:精确筛选搜索结果
  5. 批量操作:高效处理大量文档

练习

  1. 配置一个 PGVector 向量存储
  2. 实现文档的批量导入功能
  3. 创建带元数据过滤的搜索接口

参考资源