跳到主要内容

向量数据库

向量数据库是 AI 应用中的核心组件。它和传统数据库不一样:传统数据库做精确匹配,向量数据库做相似度搜索。给一个向量,它能找出最相似的向量,这就是 RAG 的基础。

为什么需要向量数据库?

在 RAG 应用中,向量数据库的作用是:

  1. 存储文档向量:把文档转换成向量后存起来
  2. 相似度检索:根据用户问题,找出最相关的文档
  3. 提供上下文:把检索到的文档作为上下文,发给 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;
}
}

常见问题

搜索不到结果

可能原因:

  1. 相似度阈值设置过高
  2. 文档没有正确添加
  3. Embedding 模型不一致

解决方案:

  • 降低相似度阈值
  • 检查文档是否正确添加
  • 确保使用相同的 Embedding 模型

过滤不生效

可能原因:

  1. 元数据字段名不匹配
  2. 值类型不匹配
  3. 向量数据库不支持该过滤操作

解决方案:

  • 检查元数据字段名和值
  • 确认值类型(字符串、数字等)
  • 查看向量数据库的过滤支持情况

性能问题

可能原因:

  1. topK 设置过大
  2. 没有使用索引字段过滤
  3. 向量数据库配置不当

解决方案:

  • 合理设置 topK(通常 5-20)
  • 使用索引字段进行过滤
  • 优化向量数据库配置

总结

向量数据库是 RAG 应用的核心组件,Spring AI 提供了统一的 API,让你可以用同一套代码操作不同的向量数据库。

关键要点:

  1. 相似度搜索:基于语义相似度,不是关键词匹配
  2. 元数据过滤:结合相似度搜索和过滤,提高检索精度
  3. 灵活删除:支持按 ID 和过滤表达式删除
  4. 性能优化:合理设置参数,使用批量操作

在实际应用中,要根据场景选择合适的向量数据库,合理设置参数,并做好错误处理和监控。

参考文档:Spring AI Vector Databases