JVector原ReadMe
近似最近邻搜索(ANN)简介
精确的最近邻搜索(KNN)在高维空间中计算代价非常高,因为在2D或3D中有效的搜索空间划分方法(如 quadtree 或 k-d tree)在高维中退化为线性扫描。这种现象就是所谓的“维度灾难”。
在大规模数据集中,相较于线性时间获得精确结果,能够在对数时间内获得近似结果通常更有价值。这种方法被称为近似最近邻搜索(Approximate Nearest Neighbor,ANN)。
ANN 索引主要分为两类:
图索引不仅实现简单、搜索速度快,更重要的是它们支持增量构建和更新。这一点比起只能处理静态数据集的分区方法优势明显。因此,所有主流商业向量数据库都采用图索引。
JVector 是一种图索引方法,融合了 DiskANN 与 HNSW 的设计思想。
它借鉴了 HNSW 的分层结构,同时在每一层中使用了 DiskANN 背后的 Vamana 算法。
JVector 架构
JVector 是一种基于图的索引系统,在 HNSW 和 DiskANN 的基础上实现了可组合的扩展能力。
它构建了一个多层图结构,并实现了无阻塞并发控制,使得构建过程可线性扩展至多个 CPU 核心:
架构要点如下:
-
上层图:每个节点存储内存中的邻接表,用于快速导航,无需磁盘 IO。
-
底层图:邻接表存储在磁盘上,并附带额外信息用于两阶段搜索。
- 第一阶段:在内存中使用压缩后的向量进行粗略搜索(可使用 PQ / BQ / Fused ADC)
- 第二阶段:从磁盘读取更精确的表示进行精排(使用 float32 或 NVQ)
两阶段设计在保持准确性的同时显著降低内存占用与延迟
Why Vector Size Matters
JVector 还可以使用两阶段搜索来构建索引,从而实现构建超过内存大小的数据集:
这意味着可以在一个索引内部实现对数时间搜索,而不是多个索引线性合并。
JVector 使用步骤(Step-by-Step)
所有代码示例来自 JVector 仓库中的 SiftSmall。
第一步:在内存中构建并查询索引
核心代码如下:
public static void siftInMemory(ArrayList<VectorFloat<?>> baseVectors) throws IOException {
...
// 构建索引
OnHeapGraphIndex index = builder.build(ravv);
// 执行搜索
SearchResult sr = GraphSearcher.search(q, 10, ravv, VectorSimilarityFunction.EUCLIDEAN, index, Bits.ALL);
...
}
说明:
-
所有向量必须维度一致。
-
RAVV(RandomAccessVectorValues)用于抽象访问向量。
-
构建参数:
graph degree:每个节点的邻接边数量(通常为16)overflow和alpha:控制图构建过程中的冗余与多样性true表示使用多层图(Hierarchical)
第二步:使用 GraphSearcher 显式控制搜索器
try (GraphSearcher searcher = new GraphSearcher(index)) {
SearchScoreProvider ssp = SearchScoreProvider.exact(q, VectorSimilarityFunction.EUCLIDEAN, ravv);
SearchResult sr = searcher.search(ssp, 10, Bits.ALL);
}
说明:
GraphSearcher初始化代价较高,推荐使用对象池复用。SearchScoreProvider用于定义搜索评分策略(精确/近似 + 可能的 reranker)。
第三步:评估召回率(Recall)
Function<VectorFloat<?>, SearchScoreProvider> sspFactory = ...;
testRecall(index, queryVectors, groundTruth, sspFactory);
返回结果如:
(OnHeapGraphIndex) Recall: 0.9898
第四步:索引持久化到磁盘并加载
OnDiskGraphIndex.write(index, ravv, indexPath);
...
OnDiskGraphIndex index = OnDiskGraphIndex.load(rs);
说明:
- 磁盘索引读取器为
ReaderSupplier,用于创建 RandomAccessReader 实例。 - 推荐使用
MemorySegmentReader(Java 20+),或兼容 Java 11 的SimpleMappedReader(仅限 <=2GB 文件)。
第五步:使用压缩向量进行搜索
构建压缩向量(PQ):
ProductQuantization pq = ProductQuantization.compute(ravv, 16, 256, true);
PQVectors pqv = pq.encodeAll(ravv);
两阶段搜索(压缩 + 精排):
ApproximateScoreFunction asf = pqv.precomputedScoreFunctionFor(q, ...);
Reranker reranker = index.getView().rerankerFor(q, ...);
SearchScoreProvider provider = new SearchScoreProvider(asf, reranker);
第六步:构建超内存大小的索引(增量构建)
- 只将压缩后的 PQ 向量留在内存中,原始向量存入磁盘。
- 每个向量执行以下操作:
incrementallyCompressedVectors.add(pq.encode(v));
writer.writeInline(ordinal, ...); // 写入磁盘
builder.addGraphNode(ordinal, v); // 添加节点到图
最后执行清理和保存:
builder.cleanup();
writer.write(Map.of());
pqv.write(pqOut);
一些不太明显但重要的点
- 同一个嵌入模型生成的向量分布一致,可复用 PQ codebook。
- JVector 提供
ExplicitThreadLocal解决 ThreadLocal 对象跨线程访问限制。 - Fused ADC 仅支持 PQ(不支持 BQ)。
- 构建过程中可能因内存带宽饱和导致变慢,JVector 使用
PhysicalCoreExecutor优化线程数(默认核数一半,可配置-Djvector.physical_core_count)。
高级特性
- Fused ADC 与 InlineVectors 一样作为 Feature 支持增量构建。
- Anisotropic PQ 支持但难以调参,需实验确定。
- 图索引支持删除节点,并可通过
markNodeDeleted和cleanup移除。 - 可使用
OnHeapGraphIndex::save和GraphIndexBuilder.load进行快照与恢复。
相关研究
-
高层解释:Datastax HNSW 指南
-
附加论文:
开发与测试
本项目为 多模块 Maven 项目,支持 Java 11 向下兼容构建,但可在 Java 20+ 上启用优化功能。
模块结构:
jvector-base:Java 11 基础实现jvector-twenty:Java 20+ 扩展功能jvector-multirelease:打包发布版本jvector-examples:示例jvector-tests:测试模块
运行测试:
mvn test # Java 20+ 测试
mvn -Pjdk11 test # Java 11 测试
运行示例:
mvn compile exec:exec@sift # 运行 SiftSmall 示例
mvn compile exec:exec@bench # 运行基准测试
若仅需测试某个类或方法:
mvn -Dtest=TestClassName test
mvn -Dtest=TestClassName#methodName test
发布前需配置 Maven 仓库(OSSRH)并运行:
mvn -Prelease clean deploy
如果你想我帮你提炼要点、画流程图、总结原理或写中文教学文档也可以告诉我。

浙公网安备 33010602011771号