向量数据库
向量数据库是 AI 应用中的核心组件。它和传统数据库不一样:传统数据库做精确匹配,向量数据库做相似度搜索。给一个向量,它能找出最相似的向量,这就是 RAG 的基础。
为什么需要向量数据库?
在 RAG 应用中,向量数据库的作用是:
- 存储文档向量:把文档转换成向量后存起来
- 相似度检索:根据用户问题,找出最相关的文档
- 提供上下文:把检索到的文档作为上下文,发给 AI 模型生成答案
传统数据库只能做精确匹配,比如搜索"MySQL",只能找到包含"MySQL"这个词的文档。但用户可能问"怎么连数据库",虽然没提到 MySQL,但意思是一样的。向量数据库能理解这种语义相似性。
Spring AI 向量数据库 API
Spring AI 提供了统一的 VectorStore 接口,屏蔽了不同向量数据库的实现细节。你可以用同一套代码操作不同的向量数据库。
VectorStoreRetriever 接口
只读接口,只提供检索功能:
@FunctionalInterface
public interface VectorStoreRetriever {
List<Document> similaritySearch(SearchRequest request);
default List<Document> similaritySearch(String query) {
return this.similaritySearch(
SearchRequest.builder().query(query).build()
);
}
}
适合只需要检索、不需要写入的场景,符合最小权限原则。
VectorStore 接口
完整的向量存储接口,支持读写操作:
public interface VectorStore extends DocumentWriter, VectorStoreRetriever {
default String getName() {
return this.getClass().getSimpleName();
}
void add(List<Document> documents);
void delete(List<String> idList);
void delete(Filter.Expression filterExpression);
default void delete(String filterExpression) { ... }
default <T> Optional<T> getNativeClient() {
return Optional.empty();
}
}
基本操作
添加文档
把文档添加到向量数据库:
@Service
public class DocumentService {
private final VectorStore vectorStore;
private final EmbeddingClient embeddingClient;
public DocumentService(VectorStore vectorStore, EmbeddingClient embeddingClient) {
this.vectorStore = vectorStore;
this.embeddingClient = embeddingClient;
}
public void addDocument(String content) {
Document document = new Document(content);
document.getMetadata().put("source", "manual");
document.getMetadata().put("timestamp", Instant.now().toString());
// VectorStore 会自动调用 EmbeddingClient 生成向量
vectorStore.add(List.of(document));
}
public void addDocuments(List<String> contents) {
List<Document> documents = contents.stream()
.map(content -> {
Document doc = new Document(content);
doc.getMetadata().put("source", "batch");
return doc;
})
.collect(Collectors.toList());
vectorStore.add(documents);
}
}
相似度搜索
根据查询文本,找出最相似的文档:
@Service
public class SearchService {
private final VectorStore vectorStore;
public SearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public List<Document> search(String query) {
// 简单搜索
return vectorStore.similaritySearch(query);
}
public List<Document> searchWithOptions(String query) {
// 带选项的搜索
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(5) // 返回最相似的 5 个文档
.similarityThreshold(0.7) // 相似度阈值,低于此值的不返回
.build();
return vectorStore.similaritySearch(request);
}
}
SearchRequest 详解
SearchRequest 是搜索请求的构建器,提供了灵活的配置选项。
基本参数
SearchRequest request = SearchRequest.builder()
.query("Spring AI 如何配置") // 查询文本
.topK(5) // 返回前 K 个结果(默认 4)
.similarityThreshold(0.75) // 相似度阈值(默认 0.0,接受所有)
.build();
相似度阈值
相似度阈值用于过滤结果,只返回相似度高于阈值的文档:
// 只返回相似度 >= 0.8 的文档
SearchRequest request = SearchRequest.builder()
.query("配置 OpenAI")
.similarityThreshold(0.8)
.build();
List<Document> results = vectorStore.similaritySearch(request);
如果所有文档的相似度都低于阈值,返回空列表。
复制搜索请求
基于现有请求创建新请求:
SearchRequest original = SearchRequest.builder()
.query("原始查询")
.topK(10)
.build();
// 基于原请求创建新请求,只修改 topK
SearchRequest modified = SearchRequest.from(original)
.topK(5)
.build();
过滤表达式
向量数据库支持基于元数据的过滤,可以在检索时只返回符合条件的文档。
FilterExpressionBuilder
使用构建器创建过滤表达式:
FilterExpressionBuilder builder = new FilterExpressionBuilder();
// 等于
Expression eqExpr = builder.eq("country", "BG").build();
// 不等于
Expression neExpr = builder.ne("status", "deleted").build();
// 大于
Expression gtExpr = builder.gt("year", 2020).build();
// 大于等于
Expression gteExpr = builder.gte("score", 80).build();
// 小于
Expression ltExpr = builder.lt("price", 100).build();
// 小于等于
Expression lteExpr = builder.lte("age", 65).build();
组合表达式
使用 AND、OR 组合多个条件:
FilterExpressionBuilder b = new FilterExpressionBuilder();
// AND 组合
Expression andExpr = b.and(
b.eq("genre", "drama"),
b.gte("year", 2020)
).build();
// OR 组合
Expression orExpr = b.or(
b.eq("status", "active"),
b.eq("status", "pending")
).build();
// 复杂组合
Expression complex = b.and(
b.in("genre", "drama", "documentary"),
b.not(b.lt("year", 2020))
).build();
IN 和 NOT IN
检查值是否在列表中:
FilterExpressionBuilder b = new FilterExpressionBuilder();
// IN:值在列表中
Expression inExpr = b.in("category", "tech", "science", "ai").build();
// NIN:值不在列表中
Expression ninExpr = b.nin("status", "deleted", "archived").build();
NULL 检查
检查字段是否为 NULL:
FilterExpressionBuilder b = new FilterExpressionBuilder();
// IS NULL
Expression isNull = b.isNull("deletedAt").build();
// IS NOT NULL
Expression isNotNull = b.isNotNull("updatedAt").build();
注意:IS NULL 和 IS NOT NULL 不是所有向量数据库都支持,使用前要确认。
字符串过滤表达式
也可以直接使用字符串形式的过滤表达式:
SearchRequest request = SearchRequest.builder()
.query("Spring AI")
.filterExpression("country == 'BG' AND year >= 2020")
.build();
List<Document> results = vectorStore.similaritySearch(request);
支持的运算符:
比较运算符:
==:等于!=:不等于>:大于>=:大于等于<:小于<=:小于等于
逻辑运算符:
AND/and/&&:与OR/or/||:或NOT/not:非
集合运算符:
IN/in:在列表中NIN/nin:不在列表中
NULL 检查:
IS NULL/is null:为空IS NOT NULL/is not null:不为空
过滤示例
@Service
public class FilteredSearchService {
private final VectorStore vectorStore;
public FilteredSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
// 按国家过滤
public List<Document> searchByCountry(String query, String country) {
SearchRequest request = SearchRequest.builder()
.query(query)
.filterExpression("country == '" + country + "'")
.build();
return vectorStore.similaritySearch(request);
}
// 按年份范围过滤
public List<Document> searchByYearRange(String query, int minYear, int maxYear) {
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression filter = b.and(
b.gte("year", minYear),
b.lte("year", maxYear)
).build();
SearchRequest request = SearchRequest.builder()
.query(query)
.filterExpression(filter)
.build();
return vectorStore.similaritySearch(request);
}
// 多条件过滤
public List<Document> searchWithMultipleFilters(String query) {
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression filter = b.and(
b.in("category", "tech", "ai"),
b.gte("year", 2020),
b.isNotNull("publishedAt")
).build();
SearchRequest request = SearchRequest.builder()
.query(query)
.filterExpression(filter)
.topK(10)
.build();
return vectorStore.similaritySearch(request);
}
}
删除文档
向量数据库提供了多种删除文档的方式。
按 ID 删除
最简单的方式,直接提供文档 ID 列表:
@Service
public class DocumentDeletionService {
private final VectorStore vectorStore;
public DocumentDeletionService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void deleteById(String documentId) {
vectorStore.delete(List.of(documentId));
}
public void deleteByIds(List<String> documentIds) {
vectorStore.delete(documentIds);
}
}
如果某个 ID 不存在,会被忽略,不会报错。
按过滤表达式删除
根据元数据条件删除文档:
public void deleteByFilter() {
// 使用 Filter.Expression
Filter.Expression filter = new Filter.Expression(
Filter.ExpressionType.EQ,
new Filter.Key("country"),
new Filter.Value("Bulgaria")
);
vectorStore.delete(filter);
// 或使用字符串表达式
vectorStore.delete("country == 'Bulgaria'");
}
删除示例
@Service
public class DocumentManagementService {
private final VectorStore vectorStore;
public DocumentManagementService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
// 删除特定国家的文档
public void deleteByCountry(String country) {
try {
vectorStore.delete("country == '" + country + "'");
} catch (Exception e) {
log.error("删除文档失败", e);
}
}
// 删除旧版本的文档
public void deleteOldVersions(String docId, String currentVersion) {
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression filter = b.and(
b.eq("docId", docId),
b.ne("version", currentVersion)
).build();
vectorStore.delete(filter);
}
// 删除过期文档
public void deleteExpiredDocuments() {
LocalDate cutoffDate = LocalDate.now().minusYears(1);
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression filter = b.lt("lastUpdated", cutoffDate.toString()).build();
vectorStore.delete(filter);
}
}
错误处理
删除操作可能失败,要做好错误处理:
public void safeDelete(String filterExpression) {
try {
vectorStore.delete(filterExpression);
} catch (IllegalArgumentException e) {
log.error("过滤表达式无效: {}", filterExpression, e);
} catch (Exception e) {
log.error("删除文档失败", e);
}
}
文档版本管理
常见场景:更新文档时,删除旧版本,添加新版本。
@Service
public class DocumentVersionService {
private final VectorStore vectorStore;
public DocumentVersionService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void updateDocument(String docId, String newContent, String newVersion) {
// 1. 删除旧版本
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression deleteFilter = b.and(
b.eq("docId", docId),
b.ne("version", newVersion)
).build();
vectorStore.delete(deleteFilter);
// 2. 添加新版本
Document newDoc = new Document(newContent);
newDoc.getMetadata().put("docId", docId);
newDoc.getMetadata().put("version", newVersion);
newDoc.getMetadata().put("lastUpdated", Instant.now().toString());
vectorStore.add(List.of(newDoc));
}
// 使用字符串表达式
public void updateDocumentSimple(String docId, String newContent, String newVersion) {
// 删除旧版本
vectorStore.delete("docId == '" + docId + "' AND version != '" + newVersion + "'");
// 添加新版本
Document newDoc = new Document(newContent);
newDoc.getMetadata().put("docId", docId);
newDoc.getMetadata().put("version", newVersion);
vectorStore.add(List.of(newDoc));
}
}
性能考虑
删除性能
- 按 ID 删除:最快,适合精确删除
- 按过滤删除:可能需要扫描索引,性能取决于实现
- 批量删除:大量删除时,考虑分批处理
public void batchDelete(List<String> ids) {
int batchSize = 100;
for (int i = 0; i < ids.size(); i += batchSize) {
List<String> batch = ids.subList(i, Math.min(i + batchSize, ids.size()));
vectorStore.delete(batch);
}
}
搜索性能
- topK 设置:不要设置太大,通常 5-20 就够了
- 相似度阈值:合理设置阈值,减少不必要的计算
- 过滤表达式:尽量使用索引字段过滤
// 好的做法:合理的 topK 和阈值
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(10) // 不要太大
.similarityThreshold(0.7) // 过滤低质量结果
.filterExpression("category == 'tech'") // 使用索引字段
.build();
支持的向量数据库
Spring AI 支持多种向量数据库,包括:
- PGvector:基于 PostgreSQL 的向量扩展
- Milvus:开源向量数据库
- Pinecone:云原生向量数据库
- Qdrant:高性能向量搜索引擎
- Weaviate:开源向量搜索引擎
- Chroma:轻量级向量数据库
- Redis:使用 Redis 作为向量存储
- MongoDB Atlas:MongoDB 的向量搜索
- Elasticsearch:使用 Elasticsearch 的向量功能
- Azure AI Service:Azure 的向量搜索服务
- 等等...
不同数据库的配置方式不同,但 API 使用方式一致。
示例:PGvector 配置
@Configuration
public class PgVectorConfig {
@Bean
public VectorStore vectorStore(
DataSource dataSource,
EmbeddingClient embeddingClient) {
return new PgVectorStore.Builder(dataSource, embeddingClient)
.withDistanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
.withDimensions(1536) // OpenAI embedding 维度
.withRemoveExistingVectorStoreTable(true)
.build();
}
}
示例:Milvus 配置
@Configuration
public class MilvusConfig {
@Bean
public VectorStore vectorStore(
EmbeddingClient embeddingClient) {
return new MilvusVectorStore.Builder()
.withEmbeddingClient(embeddingClient)
.withCollectionName("documents")
.withDimension(1536)
.build();
}
}
最佳实践
1. 合理设置元数据
元数据对过滤和检索很重要:
Document doc = new Document(content);
doc.getMetadata().put("source", "product-manual.pdf");
doc.getMetadata().put("chapter", "第三章");
doc.getMetadata().put("page", 42);
doc.getMetadata().put("category", "tech");
doc.getMetadata().put("year", 2024);
doc.getMetadata().put("version", "1.0");
2. 使用过滤优化检索
结合相似度搜索和元数据过滤:
SearchRequest request = SearchRequest.builder()
.query("配置指南")
.topK(5)
.filterExpression("category == 'tech' AND year >= 2023")
.build();
3. 合理设置相似度阈值
根据实际效果调整阈值:
// 严格模式:只要高质量结果
SearchRequest strict = SearchRequest.builder()
.query(query)
.similarityThreshold(0.85)
.build();
// 宽松模式:接受更多结果
SearchRequest loose = SearchRequest.builder()
.query(query)
.similarityThreshold(0.6)
.build();
4. 批量操作优化
处理大量文档时,使用批量操作:
public void batchAdd(List<Document> documents) {
int batchSize = 100;
for (int i = 0; i < documents.size(); i += batchSize) {
List<Document> batch = documents.subList(
i,
Math.min(i + batchSize, documents.size())
);
vectorStore.add(batch);
}
}
5. 错误处理
所有操作都要做好错误处理:
public List<Document> safeSearch(String query) {
try {
return vectorStore.similaritySearch(query);
} catch (Exception e) {
log.error("搜索失败: {}", query, e);
return Collections.emptyList();
}
}
6. 监控和日志
记录关键操作,方便排查问题:
public void addWithLogging(List<Document> documents) {
log.info("开始添加 {} 个文档", documents.size());
long start = System.currentTimeMillis();
try {
vectorStore.add(documents);
long duration = System.currentTimeMillis() - start;
log.info("文档添加完成,耗时 {} ms", duration);
} catch (Exception e) {
log.error("文档添加失败", e);
throw e;
}
}
常见问题
搜索不到结果
可能原因:
- 相似度阈值设置过高
- 文档没有正确添加
- Embedding 模型不一致
解决方案:
- 降低相似度阈值
- 检查文档是否正确添加
- 确保使用相同的 Embedding 模型
过滤不生效
可能原因:
- 元数据字段名不匹配
- 值类型不匹配
- 向量数据库不支持该过滤操作
解决方案:
- 检查元数据字段名和值
- 确认值类型(字符串、数字等)
- 查看向量数据库的过滤支持情况
性能问题
可能原因:
- topK 设置过大
- 没有使用索引字段过滤
- 向量数据库配置不当
解决方案:
- 合理设置 topK(通常 5-20)
- 使用索引字段进行过滤
- 优化向量数据库配置
总结
向量数据库是 RAG 应用的核心组件,Spring AI 提供了统一的 API,让你可以用同一套代码操作不同的向量数据库。
关键要点:
- 相似度搜索:基于语义相似度,不是关键词匹配
- 元数据过滤:结合相似度搜索和过滤,提高检索精度
- 灵活删除:支持按 ID 和过滤表达式删除
- 性能优化:合理设置参数,使用批量操作
在实际应用中,要根据场景选择 合适的向量数据库,合理设置参数,并做好错误处理和监控。