向量存储与后端
AI4J 这一层如果只写成“支持 Pinecone / Qdrant / Milvus / PgVector / Redis / Elasticsearch / Chroma / InMemory”,信息密度其实很低。
真正重要的是:它怎样用统一 VectorStore 契约把不同后端收口,同时又不假装这些后端完全等价。
1. 统一契约到底有多大
VectorStore 接口本身只有 5 个方法:
int upsert(VectorUpsertRequest request) throws Exception;
List<VectorSearchResult> search(VectorSearchRequest request) throws Exception;
boolean delete(VectorDeleteRequest request) throws Exception;
boolean exists(VectorExistsRequest request) throws Exception; // default false
VectorStoreCapabilities capabilities();
这个抽象很克制,但已经覆盖了 RAG 最核心的三件事:
- 向量写入
- 向量检索
- 数据删除
- metadata-only 存在检查(可选能力)
以及一件非常关键的事:
- 后端能力声明
也就是说,AI4J 当前不是靠“文档备注”来告诉你后端差异,而是把差异正式变成了 capabilities()。
2. dataset 在这层不是附属字段,而是硬边界
看请求对象你就会发现:
VectorUpsertRequest.datasetVectorSearchRequest.dataset
都是主字段,不是可选标签。
更关键的是,7 个外部内置后端都把 dataset 当作必填:
- Pinecone:
requiredDataset(...) - Qdrant:
requiredDataset(...) - Milvus:
requiredDataset(...) - PgVector:
requiredDataset(...) - Redis:
requiredDataset(...) - Elasticsearch:
requiredDataset(...) - Chroma:
requiredDataset(...)
也就是说,在 AI4J 当前实现里,dataset 不是“如果你想分库再填”,而是:
所有向量写入、检索、删除操作的默认边界。
3. 同一个 dataset,在不同后端里具体落到哪里
这恰恰说明统一接口不等于相同语义实现。
Pinecone
dataset 被映射到:
namespace
Qdrant
dataset 被嵌进:
- URL 模板路径
它更像集合级作用域。
Milvus
dataset 会被写到:
collectionName
PgVector
dataset 则被当成:
- 表中的筛选字段 / 查询条件
Redis
dataset 被用作:
- Hash key 分段(
<keyPrefix><dataset>:<id>)+ RediSearch 的datasetTAG 字段过滤
它落到一栈多用的 Redis 实例里,按 dataset 隔离的 key 空间 + 索引过滤。
Elasticsearch
dataset 被存成:
- 每条文档上的
datasetkeyword 字段,所有_search/_delete_by_query都带term过滤
也就是说:多个 dataset 共享一个索引(indexName 配置),靠字段过滤做边界。检索走 ES 8.x knn 查询,metadata 列是 flattened 类型,filter 键落到 metadata.<key> 的 term/terms 查询。索引在首次 upsert 时按 vectorDim 自动创建 dense_vector mapping;写入用 refresh=true,保证写完立即可查。
Chroma
dataset 被存成:
- 每条记录的
datasetmetadata 字段,所有 query/get/delete 都带where条件
也就是说:多个 dataset 共享一个 collection(collection 配置,首次使用自动 get_or_create 并以 cosine 距离建索引)。Chroma 返回的是 cosine distance,SDK 统一换算成 1 - distance 的相似度 score 返回。
Redis 是 opt-in 后端:需要 Redis Stack(含 RediSearch 模块);Jedis 是 optional 依赖,用户需自行在 pom 引入 redis.clients:jedis:4.x——4.x 是最后支持 JDK 8 的大版本,5.x 需 JDK 17,与 ai4j 的 JDK 8 字节码不兼容。若你的项目已绑定 Jedis 5.x 且不可降级,请改用其他向量后端(Milvus/Qdrant/Pinecone/PgVector),它们走同一套 VectorStore 契约。
所以从业务角度看大家都叫 dataset,但从存储现实看,它在不同后端对应的是:
- namespace
- collection
- URL scope
- relational filter column
- redis key 前缀分段 + TAG 过滤
- elasticsearch keyword 字段 + term 过滤
- chroma metadata 字段 + where 条件
这也是为什么你不能把“切换后端”理解成“换个连接串”。
4. VectorSearchRequest 真正抽象了哪些检索语义
搜索请求目前有这些关键字段:
datasetvectortopK,默认10filterincludeMetadata,默认trueincludeVector,默认false
这个设计说明 AI4J 当前的 vector search 抽象,不只是“给个向量查最近邻”,而是已经把:
- 检索边界
- 过滤条件
- 返回内容粒度
一起纳入了统一接口。
返回对象 VectorSearchResult 也对应给出:
idscorecontentvectormetadata
这正是 DenseRetriever 后面能把结果继续装回 RagHit 的前提。
5. capabilities() 为什么是这层最有价值的设计
7 个外部内置后端都会显式返回 VectorStoreCapabilities。
而且这不是装饰性信息,里面真的有差异。
当前源码里:
- Pinecone:
dataset=truemetadataFilter=truemetadataLookup=falsedeleteByFilter=truereturnStoredVector=true - Qdrant:
dataset=truemetadataFilter=truemetadataLookup=truedeleteByFilter=truereturnStoredVector=true - Milvus:
dataset=truemetadataFilter=truemetadataLookup=truedeleteByFilter=truereturnStoredVector=false - PgVector:
dataset=truemetadataFilter=truemetadataLookup=truedeleteByFilter=truereturnStoredVector=false - Redis:
dataset=truemetadataFilter=truemetadataLookup=truedeleteByFilter=truereturnStoredVector=false - Elasticsearch:
dataset=truemetadataFilter=truemetadataLookup=truedeleteByFilter=truereturnStoredVector=true - Chroma:
dataset=truemetadataFilter=truemetadataLookup=truedeleteByFilter=truereturnStoredVector=true
这里最值得注意的是:
returnStoredVector不是所有后端都支持。metadataLookup也不是所有后端都支持。
metadataLookup 对应 VectorStore.exists(...):它服务于按 metadata 判断某条记录是否存在,例如 ingestion 按 contentHash 跳过重复 chunk。
Pinecone 当前封装没有 metadata-only lookup,因此保留默认 false,不会为了“看起来统一”伪造能力。
也就是说,如果你的上层逻辑依赖“检索时顺便把已存向量取回来”,那么当前 Pinecone / Qdrant 更自然;如果依赖“入库前按 hash 判断是否已存在”,则优先使用 Qdrant / Milvus / PgVector / Redis。
这就是 capabilities() 的意义:
统一接口之上,仍然允许你看见真实差异。
5.1 exists(...) 不是向量检索
VectorStore.exists(VectorExistsRequest) 是给 ingestion 增量跳过用的 metadata-only lookup。
它不接收 query vector,也不返回相似度结果,只回答“这个 dataset 下是否存在匹配 filter 的记录”。
因此它是可选能力:
- Qdrant:走 scroll/filter
- Milvus:走 query/filter
- PgVector:走 SQL metadata filter
- Redis:走 RediSearch filter-only 查询
- Elasticsearch:走
_searchsize=0+terminate_after=1的 filter-only 查询 - Chroma:走
/getwhere+limit=1查询 - Pinecone:当前封装保持默认
false,不伪造 metadata-only lookup
调用方必须看 capabilities().isMetadataLookup(),不能假设所有向量库都支持。
5.2 进程内后端:InMemoryVectorStore
除五个外部后端外,SDK 还内置了 io.github.lnyocly.ai4j.vector.store.memory.InMemoryVectorStore——一个零依赖的 JVM 堆内向量库,new 即用:
VectorStore store = new InMemoryVectorStore();
// 直接接进现成的管线
IngestionPipeline pipeline = new IngestionPipeline(embeddingService, store);
DenseRetriever retriever = new DenseRetriever(embeddingService, store);
- 检索语义是精确余弦相似度(穷举打分 + 排序),不是近似索引
dataset隔离、metadata 等值filter、ids/filter/deleteAll删除、existsmetadata-only lookup 全部支持,capabilities()六项全开- 因此
skipExistingContentHash增量入库在它上面是真实生效的
定位要说清楚:它面向 demo、单元测试、冒烟测试和小规模内嵌场景(CLI/桌面/插件)。但它不是"重启即丢"的纯玩具——两个持久化方法覆盖小规模内嵌的落盘需求:
store.persistToFile(Paths.get("vectors.json")); // 全量 dataset 写 JSON
InMemoryVectorStore restored = InMemoryVectorStore.loadFromFile(Paths.get("vectors.json"));
生产部署仍然用 Qdrant / Milvus / PgVector / Pinecone / Redis / Elasticsearch / Chroma 这类持久化后端——它不是替代品,是让"不装任何服务也能跑真向量检索"成为可能的开发体验件。
另外,工厂层所有后端现在都有对称 getter:getPineconeVectorStore() / getQdrantVectorStore() / getMilvusVectorStore() / getPgVectorStore() / getRedisVectorStore() / getElasticsearchVectorStore() / getChromaVectorStore() / getInMemoryVectorStore()。
6. VectorStore 和 DenseRetriever 的关系是什么
DenseRetriever 并不会直接关心你后面是哪种向量库。
它只做两件事:
- 用
IEmbeddingService生成 query 向量 - 调
vectorStore.search(...)
这意味着 VectorStore 在整条 RAG 链里的位置是:
- 上接 embedding/query 向量
- 下接具体后端
- 输出统一
VectorSearchResult
这就是它真正的抽象价值。 不是为了“把 4 个 SDK 名字收进一个列表”,而是为了把检索链的上下游断开。
7. 为什么这一层仍然不能替你隐藏所有后端现实
虽然 VectorStore 已经做了统一抽象,但下面这些东西,AI4J 当前并没有帮你完全抹平:
- 后端的运维方式
- 向量列或 collection 的初始化策略
- 维度管理
- 性能特征
- 过滤表达式成本
- SaaS 与自管的部署差异
所以这层 abstraction 的正确理解不是“后端完全同构”,而是:
主链调用方式统一,但存储现实仍然存在。
8. 当前实现最容易踩的 5 个点
8.1 把 dataset 当标签用
在当前实现里,它是写入、检索、删除的边界,不是随手可空的 metadata。
8.2 忽略 capabilities() 差异
尤其 returnStoredVector / metadataLookup,并不是所有后端都支持。
8.3 只从产品名选后端
你真正该看的是:
- 你是 SaaS 优先还是自管优先
- 你要不要借 PostgreSQL 现有基础设施
- 你需不需要取回存量向量
8.4 让上层直接依赖具体后端语义
一旦这样写,VectorStore 这层抽象就被绕空了。
8.5 以为统一接口会自动解决维度与 schema 治理
当前它只统一调用,不替你做数据治理。
9. 从当前源码看,最稳的使用建议
在 AI4J 当前架构里,一个很稳的策略是:
- 先把
dataset设计成稳定边界 - 用
VectorStore写业务主链 - 仅在必要时根据
capabilities()分支处理后端差异 - 不要把后端专有逻辑泄漏进 retriever / ingestion 上层
这样你既能吃到统一接口的好处,又不会被“假等价”误导。
10. 这页最该记住的结论
AI4J 当前的向量存储层,本质上是:
- 用
VectorStore统一 upsert / search / delete / exists - 用
dataset统一知识边界 - 用
VectorStoreCapabilities显式暴露后端差异
它做的是“主链抽象统一”,不是“后端现实消失”。 把这一层看清楚之后,再去看 ingest、dense retrieval、hybrid,就能知道哪些问题该在向量层解决,哪些不该。