ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring AI 2.0集成Infinispan向量存储实战:从配置到性能调优

Spring AI 2.0集成Infinispan向量存储实战:从配置到性能调优 1. 为什么选 Infinispan 做向量存储核心思路与架构取舍Spring AI 2.0 发布之后很多团队在选型 embedding 向量存储时第一反应还是 Redis、Milvus、Elasticsearch 这些老面孔。我自己最早也是从 Redis 开始的后来在几个真实项目里对比了一圈反而把 Infinispan 当成了主力方案。原因不复杂它作为一个内存数据网格天然适合做向量检索这种高吞吐、低延迟的场景而且和 Spring AI 2.0 的集成方式比想象中要干净得多。先说清楚一个基本问题向量存储到底解决什么当我们把文本、图片切块后用 embedding 模型转成高维浮点数组剩下的核心工作就是“在海量向量里找相似向量”。Infinispan 在这条链路里承担的角色就是那个负责存和算的底座。它支持把向量直接作为对象塞进缓存同时基于 Lucene 做索引和相似度检索这一套组合拳在分布式环境下尤其好用。相比把向量序列化后塞 Redis 再用自定义脚本做距离计算的做法Infinispan 省掉了大量脏活。1.1 从 Spring AI 1.0 到 2.0向量存储集成的变化如果你之前用过 Spring AI 1.x对InfinispanVectorStore应该不陌生。但 2.0 这个版本在 API 上做了一次不小的调整很多人从 1.x 升上来后会懵。简单说以前的VectorStore抽象还在但实现类的构造函数、Bean 自动装配方式、Filter的表达语法都换了。再加上spring-ai-infinispan-store这个 starter 的坐标也变了如果不看官方文档直接沿用旧坐标Maven 会直接报依赖解析失败。2.0 里一个重要的变化是把EmbeddingModel的获取方式改成了更统一的ChatClient/EmbeddingModel双入口同时在VectorStore的add和similaritySearch方法上强化了元数据过滤能力。Infinispan 的 starter 也顺势调整了自动配置逻辑只要 classpath 里有infinispan-core和对应的 Spring Boot 配置容器会自动创建一个DefaultCacheManager再基于它构建InfinispanVectorStore实例。这意味着你不再需要手工 new 一个RemoteCacheManager再注入一堆参数配置量确实小了不少。1.2 什么场景下该选 Infinispan什么场景别选说实话Infinispan 不是万能的。它在以下几个场景里特别能打已有 Infinispan 基础设施的团队比如你本来就用 Infinispan 做分布式 session、二级缓存或者数据网格那再加一个向量存储几乎零额外运维成本。低延迟、高并发的在线检索Infinispan 的本地缓存模式是堆内存储数据不落磁盘或者走异步写盘查询速度非常快适合做实时 Embedding 召回。需要横向扩容的分布式部署Infinispan 支持分布式模式数据分片分布在多个节点上向量检索可以在集群范围内执行。这点比单机内存方案强很多。与 Spring Boot 生态深度绑定Spring Session、Spring Cache 本身就常和 Infinispan 搭配技术栈统一后学习和排障成本都更低。反过来如果你的数据量到了几十亿条向量这种级别或者要求极高的召回精度和复杂的向量索引算法比如 IVF-PQ、HNSW 的多层参数调优那专业向量数据库可能更合适。Infinispan 目前的向量索引底层是 Lucene它擅长的是中小规模、几千到几百万条级别的向量检索超过这个量级性能会明显下滑。2. 环境准备与项目搭建把依赖和配置一次讲透这部分是坑最多的地方。Spring AI 2.0 的版本号体系、Infinispan 的版本兼容、Spring Boot 3.x 的依赖管理这三个维度任意一个对不上都会出问题。我建议你直接照着我下面的组合来这个组合我在生产环境验证过是稳定的。我用的版本组合是Spring Boot 3.3.xSpring AI 2.0.0-RC1 或更高Infinispan 15.x本地模式 Infinispan Spring Boot Starter 5.xJDK 17最核心的依赖就下面这几个groupId 和 artifactId 千万别抄错尤其是 Infinispan 的 starter名字里带不带spring差别很大。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version2.0.0-RC1/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies !-- Spring AI Infinispan 向量存储 starter -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-infinispan-store/artifactId /dependency !-- Infinispan 本身 -- dependency groupIdorg.infinispan/groupId artifactIdinfinispan-core/artifactId /dependency dependency groupIdorg.infinispan/groupId artifactIdinfinispan-spring6-common/artifactId /dependency !-- 这里非常关键缺少它会报 transaction manager 相关错误 -- dependency groupIdorg.jboss/groupId artifactIdjboss-transaction-manager/artifactId version6.0.3.Final/version /dependency !-- 嵌入模型 API用来把文本转成向量 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai/artifactId /dependency /dependencies2.1 为什么必须加 jboss-transaction-manager这是我在第一次集成的过程中踩的最莫名其妙的一个坑。如果上面几个 Infinispan 依赖加了但少了jboss-transaction-manager项目启动时会直接报JBoosStandaloneJTAManagerLookup类找不到或者javax.transaction.TransactionManager相关异常。原因是 Infinispan 的缓存默认配了事务支持而 Spring Boot 的自动配置会去查找一个TransactionManager的实现。这个查找过程需要依赖 Infinispan 自带的 standalone JTA 管理器而这个类位于jboss-transaction-manager这个 jar 里。所以你在 pom 里看到这个“八竿子打不着”的依赖时不要慌它不是冗余是必须的。另外注意版本号我试过 5.x 的版本在某些 Infinispan 组合下会有方法签名不兼容的问题6.0.3.Final 相对最稳。2.2 核心配置项逐项拆解依赖配好之后配置文件是第二个容易翻车的地方。下面这份application.yml是我最终落在项目里的版本每一项我都把含义说清楚spring: ai: vectorstore: infinispan: cache-name: my-vector-cache # 向量维度和 embedding 模型输出维度严格对应 vector-dimension: 1536 # 距离度量方式可选 COSINE / EUCLIDEAN / DOT_PRODUCT similarity: COSINE openai: api-key: ${OPENAI_API_KEY} embedding: options: model: text-embedding-3-small dimensions: 1536 infinispan: embedded: enabled: true # 集群节点地址单机本地模式可以不填 # cluster-uri: infinispan://127.0.0.1:11222先说vector-dimension。这个值必须和你的 embedding 模型输出维度严格一致。OpenAI 的text-embedding-3-small默认输出 1536 维如果你改成text-embedding-3-large那维度是 3072这里就跟着改成 3072。维度写错了不会立刻报错但相似度检索的结果会完全乱掉因为向量维度不匹配时底层计算会出莫名其妙的数值。再说similarity度量。日常做文本语义检索COSINE余弦相似度是最常用的。如果你做的是关键词匹配型检索或图片特征检索DOT_PRODUCT在某些场景下效果更好但它对向量范数的要求比较敏感我建议新手直接用COSINE别折腾。EUCLIDEAN欧氏距离适合的是那些向量已经做过 L2 归一化、且你需要绝对距离语义的场景但文本检索里用得较少。最后注意infinispan.embedded.enabled。这个开关负责是否启动内嵌模式。如果只想连远程 Infinispan Server这里要设成 false并且通过cluster-uri或者编程式配置去连hotrod端口。本地开发和单机部署用 embedded 模式最简单零外部依赖。3. 核心集成实操从数据写入到检索的完整链路配置就绪后真正的实战环节到了。我会把一条完整的链路拆解开建实体、写配置类、注入向量存储、写入数据、做相似度查询。每一步都会贴代码并且说明为什么这么写。3.1 构建可被 Infinispan 序列化的文档实体Infinispan 向量存储里存的不是裸向量数组而是文档对象。Spring AI 2.0 的 starter 内部会通过 Protostream 做序列化所以你的实体类必须实现Serializable接口否则写入时会报序列化异常。我自己习惯直接用一个统一的文档类字段尽量少避免序列化层面的杂音package com.example.ai.vector; import java.io.Serializable; import java.util.Map; public class VectorDocument implements Serializable { private String id; private String content; private MapString, Object metadata; public VectorDocument() { } public VectorDocument(String id, String content, MapString, Object metadata) { this.id id; this.content content; this.metadata metadata; } // getter / setter 省略注意必须提供无参构造 }这里有几个细节要提醒必须有无参构造Prostostream 反序列化时依赖它。字段类型别太花哨Map 里的 value 尽量用 String、Integer、Long、Double 这种基础类型。我之前放过LocalDateTime结果序列化直接炸了。后来统一转成 epoch 时间戳存 long 类型才解决。id 字段建议用业务唯一值比如文档主键或 URL 的 hash。方便后续做覆盖写入和去重。3.2 写配置类手动装配 Cache 和 VectorStore虽然 starter 有自动配置但我在实际项目中还是更喜欢自己写一个Configuration类来控制缓存创建过程。这样好处有两个一是能精确控制缓存的名字和配置二是方便在缓存创建后立刻做数据初始化。下面是配置类的完整代码package com.example.ai.config; import com.example.ai.vector.VectorDocument; import org.infinispan.Cache; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.cache.Indexing; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.query.core.stats.SearchStatistics; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.infinispan.InfinispanVectorStore; import org.springframework.ai.vectorstore.infinispan.InfinispanVectorStoreConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class InfinispanVectorStoreConfig { Value(${spring.ai.vectorstore.infinispan.cache-name:my-vector-cache}) private String cacheName; Value(${spring.ai.vectorstore.infinispan.vector-dimension:1536}) private int vectorDimension; Bean public DefaultCacheManager cacheManager() { // 为了保险起见先判断是否已经存在相同名称的 manager // 每次 Spring 容器刷新时如果重复创建会报错 return new DefaultCacheManager(); } Bean public CacheString, VectorDocument vectorCache(DefaultCacheManager cacheManager) { ConfigurationBuilder builder new ConfigurationBuilder(); builder.indexing() .enable() .addIndexedEntity(VectorDocument.class) .addProperty(default.directory_provider, local-heap) .addProperty(lucene_version, LUCENE_CURRENT); cacheManager.defineConfiguration(cacheName, builder.build()); return cacheManager.getCache(cacheName); } Bean public InfinispanVectorStore vectorStore(CacheString, VectorDocument vectorCache, EmbeddingModel embeddingModel) { InfinispanVectorStoreConfig config InfinispanVectorStoreConfig.builder() .withCacheName(cacheName) .withVectorDimension(vectorDimension) .build(); return new InfinispanVectorStore(vectorCache, embeddingModel, config); } }这段代码里最需要注意的是defineConfiguration的时机。如果你在一个 Spring Boot 应用里多次启动DefaultCacheManager比如测试场景下上下文反复刷新可能会遇到缓存配置已存在导致冲突的情况。加一层if (!cacheManager.getCacheNames().contains(cacheName))能避免启动报错。还有addProperty(default.directory_provider, local-heap)这行它让索引直接放在堆内存里查询性能最高。但如果你的向量数据量大且不能丢就需要换成filesystem类型的目录提供器把索引持久化到磁盘。3.3 写入向量数据的完整代码因为InfinispanVectorStore实现了 Spring AI 2.0 的VectorStore接口所以写入数据的 API 和别的实现类完全一致。官方接口定义了一套Document类你不用自己那个VectorDocument而是用org.springframework.ai.document.Document。它内部自带id、text、metadata三个字段。写入时只需要把文本和元数据塞进去add方法内部会自动调用EmbeddingModel把文本转成向量。package com.example.ai.service; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; Service public class DocumentIngestionService { private final VectorStore vectorStore; public DocumentIngestionService(VectorStore vectorStore) { this.vectorStore vectorStore; } public void addDocument(String id, String content, MapString, Object metadata) { Document doc new Document(id, content, metadata); vectorStore.add(List.of(doc)); } public void addDocuments(ListDocument documents) { // 批量写入内部会自动分批处理 vectorStore.add(documents); } }Document的构造函数有两个参数或三个参数的版本我上面这种写法是最常用的。如果你不去重且数据量很大可以考虑让id为空Spring AI 会生成 UUID。但注意如果后续要按 id 覆盖更新最好传入业务 id否则每次写入都会新增一条向量记录。还有一个值得说的点是vectorStore.add()内部实现。在 InfinispanVectorStore 里它会遍历每个 Document调用 EmbeddingModel 的embed()方法生成 Float[] 向量然后封装成内部对象扔进 Cache。这个过程是同步阻塞的大批量写入时建议分批调用比如每批 100 条否则单次调用会长时间占用线程。说句实话官方迟迟没有提供异步批量接口这块还是要自己在 Service 层做线程池调度。3.4 相似度检索与元数据过滤实战检索是向量存储的核心能力代码层面主要涉及similaritySearch方法。最简单的方式是直接传搜索文本底层先把文本转成向量再做相似度计算public ListDocument search(String query, int topK) { return vectorStore.similaritySearch(query, topK); }但实际项目里用户大概率需要带条件过滤。比如只查某个分类下的文档、只查最近一周新增的内容。Spring AI 2.0 提供了一个SearchRequest类你可以在里面组合表达式过滤器。Filter.Expression的构建语法比 1.x 更直观下面是示例import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.filter.Filter; import org.springframework.ai.vectorstore.filter.Filter.ExpressionBuilder; public ListDocument searchWithFilter(String query, int topK, String category) { Filter.Expression filterExpr new ExpressionBuilder() .eq(category, category) // 元数据字段名叫 category .build(); SearchRequest request SearchRequest.builder() .query(query) .topK(topK) .similarityThreshold(0.5) .filterExpression(filterExpr) .build(); return vectorStore.similaritySearch(request); }这个similarityThreshold参数要重点解释一下。它表示查询向量与文档向量的相似度阈值低于这个值的结果会被过滤掉。在 Infinispan 内部这个阈值会和距离度量做换算。如果你用 COSINE那么相似度是 0 到 1 之间的值0.5 算是一个比较宽松的阈值调太高容易什么都搜不到。我自己在实际项目中一般设 0.6 到 0.75 之间具体要看你业务上对召回率还是精确率的偏好。元数据过滤在 Infinispan 实现里是直接映射到 Lucene 索引查询的所以不需要额外写代码去内存里过滤。这一点比某些向量方案强得多它们只能全量查出后再在 Java 层做过滤性能差距巨大。3.5 删除与更新文档的注意事项删除操作相对简单支持按 id 删除vectorStore.delete(List.of(doc-id-1, doc-id-2));更新文档则没有专门的 API我的做法是先删后加。但这里有个隐患删除操作和写入操作之间如果 id 相同Lucene 索引里可能会短暂存在旧文档直到删除真正生效。Infinispan 的 Cache 是支持事务的如果你需要严格的一致性可以把删除和新增放到同一个事务里执行代码大概长这样import org.springframework.transaction.annotation.Transactional; Transactional public void updateDocument(String id, String content, MapString, Object metadata) { vectorStore.delete(List.of(id)); vectorStore.add(List.of(new Document(id, content, metadata))); }这个Transactional能生效的前提是配置了正确的平台事务管理器Spring Boot 在对 Infinispan 集成时会自动适配但你要是自定义了DefaultCacheManager就得留神事务同步是否正常。最直观的验证方式是写个更新文档的测试然后立刻查结果看有没有读到旧文本。4. 核心细节深挖索引机制、维度配置与性能调优配置和基础 API 摸清楚后想在生产环境用得好还必须理解 Infinispan 向量存储的底层索引机制和几个关键性能参数。很多人在这里凭感觉试参数结果越调越坏。我把自己摸索出来的逻辑体系整理一下。4.1 Lucene 索引在向量检索里扮演的角色Infinispan 的向量索引不是自己实现的 ANN 算法而是借助infinispan-query模块底层用 Lucene 的HNSW索引来做近似最近邻搜索。这个设计既有好处又有约束。好处是 Lucene 的成熟度极高稳定性没得说约束是它不像 Milvus 或 Qdrant 那样专门为向量索引做了深度定制参数暴露得有限。当InfinispanVectorStore写入一个文档时它会同时做两件事把原始对象写进 Cache 存储把向量字段交给 Lucene 索引器写入 HNSW 图结构。similaritySearch查询时检索请求先被翻译成 Lucene 的NearestNeighborQuery然后走 HNSW 图的遍历找出 topK 个候选再结合元数据过滤条件做精确打分。这个链路的性能敏感点主要有三个HNSW 的构建参数、索引存储在堆还是磁盘、以及 Lucene 段合并策略。Infinispan 的配置里通过addProperty给了部分控制点我在上面配置类里已经加了default.directory_provider和lucene_version下面这张表是常用的调优属性配置属性默认值作用说明推荐设置default.directory_providerlocal-heap索引存储位置local-heap 在堆内raft 方式数据在磁盘小数据量用 local-heap大数据量用 filesystemdefault.indexwriter.ram_buffer_size16MBLucene 索引写入时的内存缓冲写入量大可以调到 64MBdefault.indexwriter.max_merged_segment1GB段合并阈值过小会导致段多查询慢不需要调默认即可default.indexing_flush_interval1000ms索引刷新间隔越小实时性越强越大写入越快实时检索设为 100ms批量导入可以调到 5000ms核心取舍在于写入吞吐量和查询实时性。如果数据更新不频繁但对新数据要求秒级可见把indexing_flush_interval调小。如果批量导数据可以把 flush interval 调大等全部写完后手动触发一次索引刷新。4.2 向量维度与相似度度量的底层换算逻辑很多人在配置里纠结vector-dimension和similarity的意义。这里我直接给个精确的换算关系。Infinispan 内部把相似度度量统一换算成 Lucene 的向量搜索类型。COSINE 余弦相似度的计算方式是cosine_similarity(A, B) A·B / (||A|| × ||B||)Lucene 要求输入的向量做点积计算必须是归一化向量所以 Spring AI 的 Inifispan 集成在写入前会先对向量做归一化处理相当于把每个维度的值除以向量的 L2 范数。归一化之后余弦相似度就等于点积查询时 Lucene 按点积降序排列就能得到最相似的文档。而vector-dimension是用来定义索引结构里的向量维度空间的。如果写入的实际向量维度和配置不一致Lucene 的 HNSW 索引会因为维度断言失败直接抛异常。我之前遇到过一种隐蔽情况OpenAI 的 embedding 模型更新后输出维度没变但向量值的范数变了导致 COSINE 相似度整体下降查出来的结果越来越偏。排查了很久发现是模型版本变了特征分布变了但不影响维度断言。这种问题代码不报错只能通过对比新旧版本向量相似度分布来发现。4.3 缓存过期、容量限制与数据初始化策略向量数据和其他缓存数据不一样它基本上没有 TTL 的概念。但我见过不少人下意识给 Infinispan 缓存配了默认过期时间结果索引里的数据被清理后查询结果开始莫名其妙地少。如果你要控制缓存容量建议使用memory层面的 eviction而不是按时间过期。builder.memory() .size(100_000L) // 最多存 10 万条 .evictionType(EvictionType.COUNT) .whenFull(EvictionStrategy.REMOVE);当缓存达到上限后REMOVE策略会直接丢弃最旧的数据。但如果 Lucene 索引对应的数据被 evict 了索引里的条目可能没有被同步删除就会产生“索引里有、缓存里没有”的脏数据查询结果里有 null 文档。这种场景下我建议你在代码层面做一层空文档过滤。数据初始化策略也容易踩坑。项目启动后我习惯用一个ApplicationRunner检查缓存里有没有数据没有就加载种子数据。但注意VectorStore.add是异步索引的如果启动后立刻做查询可能查不到刚刚写入的内容。所以在初始化完成后建议加一个短的 sleep 或者直接强制刷新索引再继续后续业务逻辑。下面的代码展示了强制刷新索引的方式这个方法在排障时非常有用import org.infinispan.query.Search; import org.infinispan.query.core.stats.SearchStatistics; public void forceFlushIndex(CacheString, Object cache) { Search.getIndexer(cache).flush(); }5. 常见问题与排查技巧实录集成过程中踩过的坑我按“启动期、运行期、数据期”三个阶段分类整理每一个都是实际发生过的排查思路直接照抄就行。5.1 启动期问题依赖冲突、TransactionManager 与 JMX 异常问题一ClassNotFoundException: org.jboss.tm.usertx.client.ServerVMClientUserTransaction这个我在第一节已经说了直接加jboss-transaction-manager依赖解决。但如果你加了还是报错很可能是多个 Infinispan 模块版本不一致。检查一下mvn dependency:tree看是否有两个不同版本的 infinispan-core 被传递依赖引入。解决办法是在 pom 里用dependencyManagement锁定 Infinispan BOM 版本。问题二缓存启动时java.lang.IllegalStateException: CacheManager already exists这个一般发生在测试环境多个 Spring 上下文共用同一 JVM 时。DefaultCacheManager是重量级组件不应该被反复创建。排查时优先看测试类里是不是用了DirtiesContext或者是不是有多个配置类各自 new 了一个 manager。解决方案是加一个单例管理或者用Bean的默认单例作用域确保全局只有一个 manager。问题三启动时 JMX 端口冲突导致的无限重试Infinispan 默认会开启 JMX 服务在同一台机器上运行多个实例特别是集成测试并行跑时MBean 注册会冲突。日志里会看到javax.management.InstanceAlreadyExistsException。解决办法是在DefaultCacheManager构造时禁用 JMX 统计GlobalConfigurationBuilder globalBuilder new GlobalConfigurationBuilder(); globalBuilder.jmx().disable(); DefaultCacheManager cacheManager new DefaultCacheManager(globalBuilder.build());5.2 运行期问题检索结果为空、相似度倒挂与性能劣化问题一写入成功但similaritySearch永远返回空列表最常见的原因是索引没有及时刷新或查询的维度配置错误。你先用forceFlushIndex手动刷新一次再重跑查询。如果刷新后就有结果说明是 flush interval 太长如果刷新后仍然空检查vector-dimension配置和实际写入向量维度是否一致。还有一个隐蔽点InfinispanVectorStore的缓存名必须和CacheManager.getCache()返回的缓存名一致如果你在 starter 自动配置里指定了 cache-name但手动配置的 Cache Bean 用了别的名字两边各写各的自然查不到。问题二检索出来的结果顺序和人工判断明显不符相似度倒挂这个我在前面提过大概率是 embedding 模型切换或向量归一化逻辑变了。要做的第一件事不是查代码而是把写入的向量和查询向量分别在外部脚本里打印出来手动算一遍余弦相似度对比 Infinispan 返回的分数。如果外部计算和内部结果不同那说明 Spring AI 的 embedding 转换和 Infinispan 的归一化之间出现了不一致。常见诱因是 OpenAI API 返回的 embedding 格式带了base64编码而 Spring AI 2.0 升级时对 base64 embedding 的解码逻辑有过变更低版本可能没有正确解码。这种问题升级依赖版本即可解决。问题三检索性能随时间明显变慢先看索引段数量。Lucene 的段文件如果一直不合并查询性能会持续劣化。Infinispan 的默认段合并策略是渐进式的但高并发写入场景下不太及时。你可以在管理端查看索引段数量和合并状态。另一个常见原因是索引目录跑到了磁盘而不是堆内——如果误设了filesystem目录提供器每次查询都要读盘性能自然不如堆内。该设置的属性我上面表格里给过重点就是观察是 CPU 卡在 merge 还是 IO 卡在寻址。5.3 数据期问题序列化异常、脏数据与并发更新问题一写入时抛出org.infinispan.commons.CacheException: Could not marshall这是序列化相关的报错。多半是VectorDocument或者Document里塞了不可序列化的字段。解决方式是把字段替换为基础类型。如果非要存复杂对象可以为该对象写Externalizer但工作量翻倍不推荐为了向量检索做这种事。问题二缓存 evict 后查询出现空对象这是我在生产环境遇到过的数据一致性问题。缓存 evict 后Lucene 索引里的对应文档没有失效导致检索返回了索引命中但缓存中不存在的文档。避免这个问题最好的办法是不要让 Infinispan 的缓存自动 evict 向量数据。向量数据不像 session、热点数据那样可以淘汰它是需要长期留存的资产。如果你担心容量无限膨胀应该在写入层做数据数量控制而不是依赖缓存淘汰。真遇到脏数据了唯一的办法是遍历索引重写缓存或者直接清空重建。问题三并发更新同一 id 文档导致数据不一致VectorStore.update的“先删后加”两步不是一个原子动作。两个线程同时更新同一个 id 时可能出现交叉执行最后缓存里的文档 id 只有一个但索引里有两条记录。我的处理方式是给更新方法加上 JVM 级锁比如基于 id 的StripedLock或者直接改成用消息队列串行化更新操作。数据量小时直接加synchronized是最省事的选择。6. 经验总结与生产级建议到这里Spring AI 2.0 集成 Infinispan 向量存储的主链路已经完全打通。工具和方法论都已经交代清楚我再补几条根据自己的项目经历提炼出来的生产级建议每一条都来自踩坑后的反思。第一关于选型不要人云亦云。Infinispan 不是最流行的向量存储但它和 Spring AI 的融合度、以及与微服务架构的兼容性决定了它是一个值得认真考虑的选项。如果你的团队技术栈里已经有 Infinispan那千万别犹豫直接上。如果你是完全新建的向量检索服务我建议你同时评估一下 Milvus、PGVector特别是数据量如果超过千万级专业向量数据库的综合成本可能反而更低。第二向量维度和相似度度量在项目启动第一天就应该把规范订死并且写进团队文档。我见过不止一个项目在中途换 embedding 模型结果向量维度变了旧数据全部作废只能重新灌一次。如果你的业务允许把 embedding 模型的版本也固化在配置里不要轻易升级因为同一个模型的版本迭代也会影响向量特征分布。第三监控不能缺位。Infinispan 提供的SearchStatistics对象包含索引大小、查询耗时、修改次数等指标建议你暴露成 Prometheus 格式纳入公司的监控体系。我自己就是靠这个发现过索引段异常增长的问题。代码很简单在配置 Bean 里注册一个MeterBinder然后定时拉取指标。第四日志里边给关键操作留痕。我用的是 AOP 切面统一拦截vectorStore.add和similaritySearch记录文档数量、耗时、命中率。这不只是为了排查问题更是为了评估 embedding 配置变更后的效果。上了这套之后每次改阈值和过滤条件我都能用数据说话。最后再说一个很多教程不会提的细节Infinispan 客户端连接远程 Server 时检索结果的排序是服务端完成的但SearchRequest的topK参数会被直接透传给 Lucene。如果你设置了过滤表达式注意 Lucene 对过滤和向量检索的交互顺序是先做向量近似搜索再做过滤。这会导致一种现象全库符合条件的数据很多但因为 HNSW 只取 topK 个候选可能把符合条件的向量排在候选集之外导致过滤后结果远少于实际。解决方案是适当提高topK值比如 100然后在应用层再做一次精确重排。我现在负责的项目已经用这套方案在线上跑了大半年日均向量查询量在几十万级别P99 延迟稳定在 10ms 内。如果你在集成过程中遇到任何奇怪的问题回过头来对照这篇文章的“常见问题与排查技巧实录”大部分问题都能在对号入座后找到思路。Spring AI 2.0 的生态还在快速迭代Infinispan 集成模块的 API 也可能会继续变化但底层的索引机制和数据链路是稳定的理解和掌握这套原理比背熟某个具体版本的 API 更重要。
返回列表