Spring AI 技术细节:VectorStore 多库统一抽象

Spring AI 技术细节:VectorStore 多库统一抽象

前置知识

  • 了解 Spring 框架的 Bean 管理和依赖注入机制
  • 知道向量数据库的基本操作:存储(add)、删除(delete)、相似度检索(similaritySearch)
  • 熟悉策略模式(Strategy Pattern)的设计思想
  • (扩展)了解 EmbeddingModel 的工作机制和向量化过程

核心概念

核心问题

市面上有多种向量数据库(Qdrant、Milvus、PGVector、Redis、Neo4j),每种都有独特的客户端 API。Spring AI 如何用统一的接口屏蔽底层差异,让业务代码无需修改就能切换不同存储后端?

生活类比

Type-C 统一接口:不同品牌的手机(各种向量数据库)都可以用同一根 Type-C 充电线(VectorStore 接口)充电,充电器内部自动适配不同协议。开发者只需面向统一接口编程,底层更换对用户无感。

核心思想

通过策略模式定义 VectorStore 统一接口,各向量库提供 Adapter 实现类;业务代码依赖接口而非实现,通过 Spring Bean 注入切换底层存储,实现开闭原则(OCP)和数据库可移植性。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

一、VectorStore 接口设计

Spring AI 通过 VectorStore 接口统一了向量数据库的操作,让开发者可以用相同的 API 操作不同的向量存储后端(Qdrant、Milvus、PGVector、Redis、Neo4j 等)。这种策略模式(Strategy Pattern)使得切换向量数据库时业务代码无需任何修改,只需调整配置中的 Bean 定义即可。

text 复制代码
┌───────────────────────────────────────────────────────────────────────────┐
│                           业务代码层                                      │
│                                                                          │
│  @Service                                                                │
│  public class SearchService {                                            │
│      private final VectorStore vectorStore;  // 依赖接口,而非具体实现   │
│                                                                          │
│      public List<Document> search(String q) {                            │
│          return vectorStore.similaritySearch(q);   // 多态调用          │
│      }                                                                   │
│  }                                                                       │
└────────────────────────────┬─────────────────────────────────────────────┘
                             │ VectorStore 接口
                             ▼
┌───────────────────────────────────────────────────────────────────────────┐
│                    VectorStore (统一抽象接口)                             │
│                                                                          │
│  + add(List<Document>): void                                             │
│  + delete(List<String>): void                                            │
│  + similaritySearch(SearchRequest): List<Document>                       │
│  + default similaritySearch(String): List<Document>                      │
└──────┬──────────────┬──────────────┬──────────────┬──────────────────────┘
       │              │              │              │
       ▼              ▼              ▼              ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ QdrantVS    │ │ MilvusVS    │ │ PGVector    │ │ Neo4jVS     │
│ (gRPC/HTTP) │ │ (REST/gRPC) │ │ (JDBC)      │ │ (Bolt)      │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘

接口设计的演进与版本兼容

Spring AI 的 VectorStore 接口并非一成不变,随着版本迭代也在不断丰富。早期的版本只有 adddeletesimilaritySearch 三个核心方法,后续版本增加了:

java 复制代码
// 1.7.0+ 版本新增的批量操作和配置能力
public interface VectorStore {

    // ... 核心方法 ...

    /**
     * 批量写入,支持并行处理
     * @since 1.7.0
     */
    default void batchAdd(List<Document> documents, 
                          BatchOptions options) {
        // 默认实现:简单循环
        documents.forEach(this::add);
    }

    /**
     * 获取存储后端的状态和统计信息
     * @since 1.8.0
     */
    default StoreStats getStats() {
        return StoreStats.UNKNOWN;
    }

    /**
     * 执行原生查询(绕过统一抽象,用于特殊场景)
     */
    default <T> T executeNativeQuery(Object nativeQuery, 
                                      Class<T> responseType) {
        throw new UnsupportedOperationException(
            "Native query not supported by this implementation");
    }
}

设计权衡 :接口的扩展遵循"最小可用原则"------只暴露所有后端都支持的能力。对于某特定数据库的独有特性(如 Qdrant 的 Payload 索引、Milvus 的 Partition Key),可通过 executeNativeQuery 绕过抽象层直接调用,或通过 metadata 字段传递额外参数。


二、Document 模型与 VectorStore 接口

Spring AI 的 VectorStore 围绕 Document 模型设计,文档包含内容、元数据和可选的预计算 Embedding:

java 复制代码
/**
 * Spring AI Document 模型(完整版)
 */
@Getter
@Builder
public class Document {
    private String id;
    private String content;
    private Map<String, Object> metadata;
    private float[] embedding; // 可选预计算的嵌入向量

    // 扩展字段
    private Float score;       // 相似度分数(检索结果中填充)
    private String collection; // 所属集合(多租户/多命名空间支持)
}

Document 的深层设计考量

1. Embedding 字段的语义与使用模式

embedding 字段是 float[] 类型,允许两种使用模式:

  • 自动计算模式 :调用 add() 时,VectorStore 内部通过注入的 EmbeddingModel 自动生成向量,此时 embedding 字段可为 null
  • 预计算模式 :客户端提前计算好向量,传入已填充 embedding 的 Document。这在以下场景非常有用:
    • 使用外部专用的 Embedding 服务(如通过 HTTP 调用独立的模型服务)
    • 需要对同一个文本使用不同 Embedding 模型生成多个版本
    • 缓存 Embedding 结果以节省计算资源
java 复制代码
// 预计算模式示例
@Service
public class CachedEmbeddingService {
    private final EmbeddingModel model;
    private final Cache<String, float[]> embeddingCache;

    public Document createWithCachedEmbedding(String text) {
        float[] cached = embeddingCache.get(text, () -> model.embed(text));
        return Document.builder()
            .content(text)
            .embedding(cached)  // 使用缓存的向量
            .metadata(Map.of("cached", true))
            .build();
    }
}
2. Metadata 的类型系统与序列化

元数据支持任意 Map<String, Object>,但不同后端对数据类型的支持有差异:

后端 支持的类型 特殊说明
PGVector JSONB 支持任意 JSON 结构,可索引嵌套字段
Redis 原生类型 + Tag 支持 String、Numeric、Geo、Tag
Qdrant Payload JSON 支持任意 JSON,可配置索引策略
Milvus Schema 预定义 需提前定义字段类型,不支持任意 Map

跨后端兼容性建议

  • 元数据中仅使用 String、Number、Boolean、Instant(自动转时间戳)
  • 避免嵌套过深的 JSON 结构
  • 如需复杂查询,确保对应的字段在后端已建立索引
java 复制代码
// 元数据类型安全的构建器
public class MetadataBuilder {
    private final Map<String, Object> metadata = new LinkedHashMap<>();
    
    public MetadataBuilder text(String key, String value) {
        metadata.put(key, value);
        return this;
    }
    
    public MetadataBuilder number(String key, Number value) {
        metadata.put(key, value);
        return this;
    }
    
    public MetadataBuilder tag(String key, String... values) {
        // 特定后端支持 Tag 类型(如 Redis)
        metadata.put(key, String.join(",", values));
        return this;
    }
    
    public MetadataBuilder timestamp(String key, Instant time) {
        metadata.put(key, time.toEpochMilli());
        return this;
    }
}

SearchRequest 的扩展参数

java 复制代码
/**
 * 完整版 SearchRequest 参数
 */
@Builder
public class SearchRequest {
    private String query;               // 查询文本
    private float[] queryVector;        // 预计算查询向量(与 query 二选一)
    private int topK = 4;               // 返回数量
    private double similarityThreshold; // 相似度阈值
    private String filterExpression;    // 元数据过滤表达式

    // 扩展参数
    private int offset = 0;             // 分页偏移(用于滚动检索)
    private List<String> includeFields; // 仅返回指定字段(减少数据传输)
    private List<String> excludeFields; // 排除指定字段
    private String sortBy;              // 排序字段(非相似度排序)
    private String consistencyLevel;    // 一致性级别(强一致/最终一致)
}

三、多存储后端配置

通过配置切换不同的向量存储实现,零业务代码修改:

java 复制代码
@Configuration
public class VectorStoreConfig {

    /**
     * PGVector 实现:基于 PostgreSQL 的 pgvector 扩展
     * 适合已有 PG 数据库的团队
     */
    @Bean
    @ConditionalOnProperty(name = "spring.ai.vectorstore.type",
                           havingValue = "pgvector")
    public VectorStore pgVectorStore(JdbcTemplate jdbcTemplate,
                                      EmbeddingModel embeddingModel) {
        return PgVectorStore.builder(jdbcTemplate, embeddingModel)
            .tableName("vector_documents")
            .dimensions(1536)
            .initializeSchema(true)        // 自动建表
            .schemaValidation(true)        // 启动时校验 schema
            .maxDocumentBatchSize(10000)
            .build();
    }
}

各后端实现深度对比

1. PGVector 实现细节
java 复制代码
// PGVector 的核心 SQL 生成逻辑(简化)
public class PgVectorStore implements VectorStore {
    
    private final JdbcTemplate jdbcTemplate;
    private final String tableName;
    private final int dimensions;
    
    @Override
    public void add(List<Document> documents) {
        // 1. 调用 EmbeddingModel 生成向量
        List<Object[]> batchArgs = documents.stream()
            .map(doc -> {
                float[] embedding = doc.getEmbedding();
                if (embedding == null) {
                    embedding = embeddingModel.embed(doc.getContent());
                }
                return new Object[]{
                    doc.getId(),
                    doc.getContent(),
                    PGvector.fromArray(embedding),  // pgvector 专用类型
                    Jsonb.fromMap(doc.getMetadata())
                };
            })
            .collect(Collectors.toList());
        
        // 2. 批量 UPSERT(使用 ON CONFLICT 处理重复)
        String sql = """
            INSERT INTO %s (id, content, embedding, metadata)
            VALUES (?, ?, ?, ?)
            ON CONFLICT (id) DO UPDATE SET
                content = EXCLUDED.content,
                embedding = EXCLUDED.embedding,
                metadata = EXCLUDED.metadata
            """.formatted(tableName);
        
        jdbcTemplate.batchUpdate(sql, batchArgs);
    }
    
    @Override
    public List<Document> similaritySearch(SearchRequest request) {
        float[] queryVector = resolveQueryVector(request);
        
        // 使用 pgvector 的 <=> 操作符计算余弦距离
        String sql = """
            SELECT id, content, metadata, 1 - (embedding <=> ?) AS score
            FROM %s
            WHERE metadata @> ?::jsonb  -- 元数据过滤
            ORDER BY score DESC
            LIMIT ?
            """.formatted(tableName);
        
        return jdbcTemplate.query(sql, 
            new Object[]{ queryVector, filterJson, request.getTopK() },
            this::mapRowToDocument
        );
    }
}

PGVector 的高级特性

  • 支持 IVFFlat 和 HNSW 两种索引,适用于不同数据量和精度要求
  • 可结合 PostgreSQL 的 RLS(行级安全)实现细粒度权限控制
  • 支持向量维度动态变化(但建议固定)

生产级配置

yaml 复制代码
spring:
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW                     # 或 IVFFlat
        index-lists: 100                     # IVFFlat 的列表数
        index-ops: vector_cosine_ops         # 索引操作类
        hnsw-m: 16                           # HNSW 每层最大连接数
        hnsw-ef-construction: 200            # HNSW 构建时动态列表大小
        similarity-metric: COSINE            # COSINE / EUCLIDEAN / DOT_PRODUCT
2. Redis 实现细节
java 复制代码
// Redis 向量存储的特殊处理
public class RedisVectorStore implements VectorStore {
    
    private final RedisClient redisClient;
    private final String indexName;
    private final String prefix;
    
    @Override
    public void add(List<Document> documents) {
        // Redis 使用 RedisHash 存储每个文档
        List<RedisCommand> commands = documents.stream()
            .map(doc -> {
                Map<String, Object> fields = new HashMap<>();
                fields.put("content", doc.getContent());
                fields.put("embedding", floatArrayToByteArray(doc.getEmbedding()));
                fields.put("metadata", JsonUtils.toJson(doc.getMetadata()));
                fields.put("_id", doc.getId());
                
                // 使用 HSET 存储
                return RedisCommand.hset(prefix + doc.getId(), fields);
            })
            .collect(Collectors.toList());
        
        redisClient.send(commands);
    }
    
    @Override
    public List<Document> similaritySearch(SearchRequest request) {
        // 使用 Redis FT.SEARCH 命令
        String query = String.format(
            "(%s)=>[KNN %d @embedding $vec AS score]",
            buildFilterExpression(request.getFilterExpression()),
            request.getTopK()
        );
        
        // 执行 FT.SEARCH
        return redisClient.ftSearch(indexName, query, 
            Map.of("params", Map.of("vec", queryVector)),
            Map.of("dialect", 2)
        );
    }
}

Redis 独有特性

  • 支持向量和元数据的实时更新,延迟极低(亚毫秒级)
  • 通过 Redis Stack 的 JSON 模块支持复杂元数据结构
  • 支持向量检索与全文检索(RediSearch)的混合查询
3. Qdrant 实现细节
java 复制代码
public class QdrantVectorStore implements VectorStore {
    
    private final QdrantGrpcClient grpcClient;
    private final String collectionName;
    
    @Override
    public void add(List<Document> documents) {
        UpsertPointsRequest.Builder builder = UpsertPointsRequest.newBuilder()
            .setCollectionName(collectionName);
        
        documents.forEach(doc -> {
            PointStruct point = PointStruct.newBuilder()
                .setId(PointId.newBuilder()
                    .setUuid(doc.getId()).build())
                .setVector(floatArrayToQdrantVector(doc.getEmbedding()))
                .putAllPayload(metadataToPayload(doc.getMetadata()))
                .build();
            builder.addPoints(point);
        });
        
        grpcClient.upsert(builder.build());
    }
}

Qdrant 高级特性

  • 支持多向量(Multi-Vector)和稀疏向量(Sparse Vector)
  • 支持向量量化和压缩(Scalar Quantization、Binary Quantization)
  • 支持分片(Sharding)和副本(Replication)的水平扩展

配置切换的最佳实践

使用配置中心动态切换
java 复制代码
@Configuration
public class DynamicVectorStoreConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public VectorStore vectorStore(
            ApplicationContext context,
            VectorStoreProperties properties) {
        
        // 从配置中心(如 Apollo / Nacos)动态读取
        String storeType = properties.getType();
        
        return switch (storeType) {
            case "pgvector" -> context.getBean("pgVectorStore", VectorStore.class);
            case "redis" -> context.getBean("redisVectorStore", VectorStore.class);
            case "qdrant" -> context.getBean("qdrantVectorStore", VectorStore.class);
            default -> throw new IllegalArgumentException(
                "Unsupported vector store type: " + storeType);
        };
    }
}
多数据源并存(读写分离/灰度发布)
java 复制代码
@Configuration
public class MultiVectorStoreConfig {
    
    @Bean
    @Primary  // 主写入
    public VectorStore primaryVectorStore() {
        return pgVectorStore();
    }
    
    @Bean
    @Qualifier("readonly")
    public VectorStore readonlyVectorStore() {
        // 从库只读副本,用于大查询场景
        return pgVectorStoreReadReplica();
    }
    
    @Bean
    @Qualifier("cache")
    public VectorStore cacheVectorStore() {
        // Redis 作为缓存层,TTL 较短
        return redisVectorStore();
    }
}

@Service
public class HybridSearchService {
    
    private final VectorStore primaryStore;
    private final VectorStore cacheStore;
    
    public List<Document> searchWithCache(String query) {
        // 先查缓存(Redis),miss 后查主库(PGVector)
        List<Document> cached = cacheStore.similaritySearch(query);
        if (!cached.isEmpty()) {
            return cached;
        }
        
        List<Document> results = primaryStore.similaritySearch(query);
        // 异步写入缓存
        CompletableFuture.runAsync(() -> cacheStore.add(results));
        return results;
    }
}

四、VectorStore 的过滤检索

基于元数据的过滤检索是生产环境的核心需求,Spring AI 提供类 SQL 的过滤表达式:

java 复制代码
@Service
public class FilteredVectorSearchService {

    private final VectorStore vectorStore;

    /**
     * 元数据过滤检索示例
     * 仅检索特定租户和分类下的文档
     */
    public List<Document> searchWithFilter(String query,
                                            String tenantId,
                                            String category) {
        String filterExpr = String.format(
            "tenant_id == '%s' && category == '%s'",
            tenantId, category
        );

        SearchRequest request = SearchRequest.builder()
            .query(query)
            .topK(10)
            .similarityThreshold(0.65)
            .filterExpression(filterExpr)
            .build();

        return vectorStore.similaritySearch(request);
    }
}

过滤表达式的语法规范与跨后端兼容性

Spring AI 定义了一套统一的过滤表达式语法,底层适配器会将其转换为各后端原生的查询语言:

表达式语法 含义 PGVector 转换 Redis 转换 Qdrant 转换
field == 'value' 等值比较 metadata @> '{"field":"value"}' @field:{value} field:value
field != 'value' 不等比较 NOT (metadata @> '{"field":"value"}') -@field:{value} -field:value
num > 100 大于 (metadata->>'num')::numeric > 100 @num:[(100 inf] num > 100
field IN ['a','b'] 包含 metadata ? 'field' AND metadata->>'field' IN ('a','b') `@field:{a b}`
field CONTAINS 'keyword' 字符串包含 metadata->>'field' LIKE '%keyword%' @field:(keyword) field: keyword*
`(A && B) C` 逻辑组合 对应 SQL AND/OR

注意filterExpression 的具体语法与每个后端的支持程度有关。Spring AI 提供了一套通用语法,但某些高级特性(如正则表达式、地理距离查询)可能只在特定后端支持。

复杂过滤表达式的构建
java 复制代码
// 使用构建器模式构造复杂过滤条件
@Service
public class AdvancedFilterService {
    
    public SearchRequest buildComplexFilter() {
        // 条件:租户为 'tenant-123' 且 (分类为 'tech' 或 'product') 且 创建时间在最近7天内
        String filter = FilterExpression.builder()
            .and("tenant_id == 'tenant-123'")
            .and(
                FilterExpression.builder()
                    .or("category == 'tech'")
                    .or("category == 'product'")
                    .build()
            )
            .and("created_at >= " + Instant.now().minus(7, ChronoUnit.DAYS).toEpochMilli())
            .build()
            .toString();
        
        return SearchRequest.builder()
            .query("AI 技术趋势")
            .topK(20)
            .filterExpression(filter)
            .build();
    }
}

检索结果的后处理与重排序

java 复制代码
@Service
public class RerankingSearchService {
    
    private final VectorStore vectorStore;
    private final CrossEncoderModel reranker;  // 交叉编码器用于重排序
    
    public List<Document> searchWithRerank(String query, int topK) {
        // 1. 先召回 2倍数量的候选(保留排序空间)
        List<Document> candidates = vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(query)
                .topK(topK * 2)
                .similarityThreshold(0.6)
                .build()
        );
        
        // 2. 使用 Cross-Encoder 重新评分(更准确但更慢)
        List<ScoredDocument> reranked = reranker.rerank(query, candidates);
        
        // 3. 截断并返回
        return reranked.stream()
            .limit(topK)
            .map(ScoredDocument::getDocument)
            .collect(Collectors.toList());
    }
}

分页与滚动检索

对于大规模数据集,单次检索可能返回海量结果,需要分页支持:

java 复制代码
@Service
public class PaginatedSearchService {
    
    /**
     * 使用 offset/limit 分页
     */
    public SearchResult<Document> searchPage(String query, int page, int size) {
        SearchRequest request = SearchRequest.builder()
            .query(query)
            .topK(size)
            .offset(page * size)  // 注意:offset 可能导致性能问题
            .build();
        
        List<Document> results = vectorStore.similaritySearch(request);
        long total = countTotalMatches(query);  // 需要额外统计
        
        return new SearchResult<>(results, total, page, size);
    }
    
    /**
     * 使用游标滚动(更适合大数据量导出)
     */
    public List<Document> scrollSearch(String query, String cursor, int batchSize) {
        SearchRequest request = SearchRequest.builder()
            .query(query)
            .topK(batchSize)
            .filterExpression("score > " + cursor)  // 基于上一页最后一条的分数
            .build();
        
        return vectorStore.similaritySearch(request);
    }
}

五、写入与删除操作管理

VectorStore 的文档生命周期管理实践:

java 复制代码
@Service
public class DocumentManagementService {

    private final VectorStore vectorStore;
    private final EmbeddingModel embeddingModel;
    private final DocumentTransformer transformer;  // 文档预处理
    
    public void ingestDocuments(List<Document> documents) {
        // 1. 文档预处理:清洗、分块、富化
        List<Document> processed = transformer.process(documents);
        
        // 2. 批量写入
        vectorStore.add(processed);
    }
}

写入性能优化

1. 批量大小调优

各后端推荐的批量大小:

后端 推荐批量大小 原因
PGVector 500 - 2000 受 JDBC batch 限制,过大导致内存溢出
Redis 1000 - 5000 管道(Pipeline)模式可高效处理
Qdrant 100 - 500 gRPC 单次请求有大小限制,需平衡网络开销
Milvus 1000 - 10000 原生支持大批量导入
java 复制代码
@Service
public class BatchIngestionService {
    
    private final int BATCH_SIZE = 1000;
    
    public void ingestLargeDataset(List<Document> allDocs) {
        // 分批处理,避免单次请求过大
        int total = allDocs.size();
        for (int i = 0; i < total; i += BATCH_SIZE) {
            int end = Math.min(i + BATCH_SIZE, total);
            List<Document> batch = allDocs.subList(i, end);
            
            // 异步提交,控制并发数
            CompletableFuture.runAsync(() -> {
                vectorStore.add(batch);
                log.info("Processed batch {}-{}", i, end);
            });
        }
    }
}
2. 并行写入与事务边界
java 复制代码
@Service
public class ParallelIngestionService {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(4);
    
    public void parallelIngest(List<Document> documents) {
        // 按租户分片,并行写入
        Map<String, List<Document>> shards = documents.stream()
            .collect(Collectors.groupingBy(
                doc -> doc.getMetadata().get("tenant_id").toString()
            ));
        
        List<CompletableFuture<Void>> futures = shards.entrySet().stream()
            .map(entry -> CompletableFuture.runAsync(
                () -> vectorStore.add(entry.getValue()),
                executor
            ))
            .collect(Collectors.toList());
        
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
            .join();  // 等待所有分片完成
    }
}
3. 写入失败的重试策略
java 复制代码
@Service
public class ResilientIngestionService {
    
    private final VectorStore vectorStore;
    private final RetryTemplate retryTemplate;
    
    public void ingestWithRetry(List<Document> documents) {
        retryTemplate.execute(context -> {
            try {
                vectorStore.add(documents);
                return null;
            } catch (DataAccessException e) {
                // 记录失败的文档ID,便于后续补偿
                log.warn("Batch write failed, retry count: {}", 
                    context.getRetryCount());
                throw e;
            }
        });
    }
}

删除策略与数据生命周期管理

1. 软删除模式
java 复制代码
@Service
public class SoftDeleteService {
    
    public void softDelete(String id) {
        // 不真正删除,而是标记为已删除
        Document doc = Document.builder()
            .id(id)
            .metadata(Map.of("deleted", true, "deleted_at", Instant.now()))
            .build();
        vectorStore.add(List.of(doc));  // 覆盖更新
    }
    
    public List<Document> searchActive(String query) {
        // 检索时过滤掉已删除的文档
        return vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(query)
                .filterExpression("deleted == false")
                .build()
        );
    }
}
2. 基于时间窗口的数据过期
java 复制代码
@Component
public class DataRetentionScheduler {
    
    private final VectorStore vectorStore;
    private final Duration retentionPeriod = Duration.ofDays(90);
    
    @Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨3点执行
    public void purgeExpiredData() {
        Instant cutoff = Instant.now().minus(retentionPeriod);
        
        // 先检索所有过期的文档ID
        List<Document> expired = vectorStore.similaritySearch(
            SearchRequest.builder()
                .filterExpression("created_at < " + cutoff.toEpochMilli())
                .topK(10000)  // 每次最多删除1万条
                .build()
        );
        
        if (!expired.isEmpty()) {
            List<String> ids = expired.stream()
                .map(Document::getId)
                .collect(Collectors.toList());
            vectorStore.delete(ids);
            log.info("Purged {} expired documents", ids.size());
        }
    }
}

变更数据捕获(CDC)与向量同步

java 复制代码
@Service
public class VectorSyncService {
    
    private final VectorStore vectorStore;
    private final ApplicationEventPublisher eventPublisher;
    
    @EventListener
    public void onDocumentUpdated(DocumentUpdatedEvent event) {
        // 当业务数据库中的文档更新时,自动同步到向量库
        Document doc = convertToDocument(event.getSource());
        vectorStore.add(List.of(doc));
    }
    
    @EventListener
    public void onDocumentDeleted(DocumentDeletedEvent event) {
        vectorStore.delete(List.of(event.getId()));
    }
}

六、异常处理与监控

扩展:向量存储操作的标准异常层次

java 复制代码
// Spring AI 定义的异常层次
public class VectorStoreException extends RuntimeException {
    private final String storeType;
    private final String operation;
    private final String errorCode;
}

// 具体子类
public class VectorStoreConnectionException extends VectorStoreException {}
public class VectorStoreIndexException extends VectorStoreException {}
public class VectorStoreValidationException extends VectorStoreException {}
public class VectorStoreTimeoutException extends VectorStoreException {}

操作拦截与监控指标

java 复制代码
@Component
@Aspect
public class VectorStoreMonitor {
    
    private final MeterRegistry meterRegistry;
    
    @Around("execution(* org.springframework.ai.vectorstore.VectorStore.*(..))")
    public Object monitorVectorStore(ProceedingJoinPoint joinPoint) throws Throwable {
        String method = joinPoint.getSignature().getName();
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            Object result = joinPoint.proceed();
            meterRegistry.counter("vectorstore.operations", 
                "method", method, "status", "success").increment();
            return result;
        } catch (Exception e) {
            meterRegistry.counter("vectorstore.operations",
                "method", method, "status", "error",
                "error_type", e.getClass().getSimpleName()).increment();
            throw e;
        } finally {
            sample.stop(Timer.builder("vectorstore.duration")
                .tag("method", method)
                .register(meterRegistry));
        }
    }
}

健康检查

java 复制代码
@Component
public class VectorStoreHealthIndicator implements HealthIndicator {
    
    private final VectorStore vectorStore;
    
    @Override
    public Health health() {
        try {
            // 执行一个轻量级查询验证连通性
            vectorStore.similaritySearch(
                SearchRequest.builder()
                    .query("health check")
                    .topK(1)
                    .build()
            );
            return Health.up()
                .withDetail("store", vectorStore.getClass().getSimpleName())
                .build();
        } catch (Exception e) {
            return Health.down(e)
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

七、总结

本章深入 Spring AI VectorStore 的多库统一抽象层。通过单一接口支持 PGVector、Redis、Qdrant、Milvus、Neo4j 等多种后端,实现了向量存储层的供应商过滤和零代码切换。Document 模型和 SearchRequest 提供了元数据过滤、相似度阈值和分页等丰富的检索能力。

各后端选型建议

场景 推荐后端 理由
中小规模项目,已有 PostgreSQL PGVector 复用现有基础设施,维护成本低
大规模生产环境(>100万向量) Qdrant / Milvus 专用优化,支持水平扩展和高级索引
高吞吐、低延迟实时场景 Redis 内存存储,毫秒级响应
图数据库场景(知识图谱) Neo4j 向量检索与图查询结合
AWS 生态 Aurora pgvector 云原生托管,免运维

最佳实践总结

  1. 接口依赖 :业务代码始终依赖 VectorStore 接口,而非具体实现类
  2. 批量操作:使用批量 API 而非单条操作,显著提升吞吐量
  3. 索引策略:根据数据量选择合适的索引类型(IVFFlat vs HNSW)
  4. 元数据设计:保持元数据扁平化,避免过度嵌套
  5. 监控告警:集成指标监控和健康检查,及时发现存储层问题
  6. 降级策略:为 VectorStore 操作设置超时和重试机制
  7. 数据版本管理:使用元数据字段追踪文档版本,支持回滚
  8. 灰度切换:生产环境中使用多数据源逐步切换,降低变更风险
相关推荐
AI天行健43 分钟前
文生视频与图生视频的技术区别及适用场景分析
人工智能·音视频
今天AI了吗43 分钟前
Codex 配置自定义 AI API 完整指南:从零到一接入你的专属模型
java·人工智能·python·数据分析·embedding
东离与糖宝43 分钟前
不用高端显卡!本地大模型量化入门|Ollama+transformers+llama.cpp实战
人工智能
森山冶仁44 分钟前
治理知识库构建:用 RAG 把制度、文档、经验变成 AI 能力
人工智能·智能问答·rag·企业知识库·大模型落地·ai治理
安科瑞黄益鸣1 小时前
筑牢配电安全:安科瑞 ARB 弧光保护在半导体厂房的应用
人工智能
VIP_CQCRE1 小时前
用 Ace Data Cloud 快速接入 OpenAI Chat Completion API:一套 Token 打通主流 AI 能力
人工智能·chatgpt·openai·api·acedatacloud
甲维斯1 小时前
GPT6真的是“AGI”!首测完成瘫坐沙发!
人工智能
zcmodeltech1 小时前
源网荷储一体化沙盘模型多场景控制系统设计——基于STM32与Modbus RTU的源网荷储、多能互补、冷热电三联供全场景联动方案,服务范围覆盖全国
数据库·人工智能·stm32·单片机·嵌入式硬件
鲲穹AI种草1 小时前
小红书内容 AI 创作工具横向对比:创作者实用能力与局限解析
人工智能·小红书文案