jvector

jvector

高性能向量索引库 支持图索引和大规模搜索

JVector是一个基于图的向量索引库,采用DiskANN设计并支持可组合扩展。它实现单层图和非阻塞并发控制,具有线性扩展能力。该库提供两阶段搜索、量化压缩和大于内存的索引构建功能,有效降低内存使用并提升搜索速度。JVector主要用于大规模近似最近邻搜索,为高维向量检索提供高效方案。

ANN图索引向量搜索JVector产品量化Github开源项目

近似最近邻搜索简介

在高维空间中,精确的最近邻搜索(k-最近邻或KNN)的计算成本过高,因为在二维或三维中有效的搜索空间分割方法(如四叉树或k-d树)在高维中退化为线性扫描。这是所谓的"维度灾难"的一个方面。

对于大型数据集,以对数时间获得近似答案几乎总是比以线性时间获得精确答案更有用。这被简称为ANN(近似最近邻)搜索。

ANN索引主要分为两大类:

  • 基于分区的索引,如LSH、IVF或SCANN
  • 图索引,如HNSW或DiskANN

图索引通常更容易实现且速度更快,更重要的是它们可以增量构建和更新。这使得它们比只能在预先完全指定的静态数据集上工作的分区方法更适合作为通用索引。这就是为什么所有主要的商业向量索引都使用图方法的原因。

JVector是DiskANN家族中的一个图索引。

JVector架构

JVector是一个基于图的索引,在DiskANN设计的基础上增加了可组合的扩展。

JVector实现了一个具有非阻塞并发控制的单层图,允许构建过程随着核心数量的增加而线性扩展: 随着线程数增加,JVector呈线性扩展

图以每个节点的磁盘邻接表表示,内联存储额外数据以支持两遍搜索。第一遍由内存中保存的向量的有损压缩表示驱动,第二遍由从磁盘读取的更精确表示驱动。第一遍搜索可以通过以下方式执行:

  • 乘积量化(PQ),可选择带有各向异性加权
  • 二进制量化(BQ)
  • 融合ADC,其中PQ码本被转置并与图邻接表内联写入

第二遍搜索可以使用:

  • 全精度float32向量

这种两遍设计减少了内存使用并降低了延迟,同时保持了准确性。

此外,JVector独特地提供了使用两遍搜索构建索引本身的能力,允许构建大于内存的索引: 更大的索引

这一点很重要,因为它允许您在单个索引中利用对数级搜索,而不是溢出到多个索引的线性时间合并结果。

JVector步骤解析

所有代码示例都来自JVector源代码库中的SiftSmall,其中还包括siftsmall数据集。只需在IDE中导入项目并点击运行即可尝试!

步骤1:在内存中构建和查询索引

首先是代码:

public static void siftInMemory(ArrayList<VectorFloat<?>> baseVectors) throws IOException { // 从第一个向量推断维度 int originalDimension = baseVectors.get(0).length(); // 将原始向量包装在RandomAccessVectorValues中 RandomAccessVectorValues ravv = new ListRandomAccessVectorValues(baseVectors, originalDimension); // 使用原始的内存中向量的分数提供者 BuildScoreProvider bsp = BuildScoreProvider.randomAccessScoreProvider(ravv, VectorSimilarityFunction.EUCLIDEAN); try (GraphIndexBuilder builder = new GraphIndexBuilder(bsp, ravv.dimension(), 16, // 图度 100, // 构建搜索深度 1.2f, // 构建过程中允许度溢出的因子 1.2f)) // 放宽邻居多样性要求的因子 { // 构建索引(在内存中) OnHeapGraphIndex index = builder.build(ravv); // 搜索随机向量 VectorFloat<?> q = randomVector(originalDimension); SearchResult sr = GraphSearcher.search(q, 10, // 结果数量 ravv, // 我们搜索的向量,用于评分 VectorSimilarityFunction.EUCLIDEAN, // 评分方式 index, Bits.ALL); // 要考虑的有效序号 for (SearchResult.NodeScore ns : sr.getNodes()) { System.out.println(ns); } } }

注释:

  • 所有索引都假设您有一个具有一致、固定维度(float32组件数量)的向量源。
  • 向量源通常表示为RandomAccessVectorValues的子类,它围绕getVector / getVectorInto提供了一个简单的API。请注意在多线程上下文中的isValueShared();作为经验法则,内存中的RAVV不会使用共享值,而从磁盘读取的RAVV会(作为一种优化,避免为每次调用分配新的堆上向量)。
  • 您不必一次性向索引提供所有向量,但由于这在原型设计时是一种常见场景,因此提供了一种便捷方法来执行此操作。我们稍后将介绍如何增量构建索引。
  • 对于溢出Builder参数,内存构建的最佳值约为1.2,磁盘上构建约为1.5。(允许的溢出越多,需要重新计算最佳边的次数就越少,但每次搜索时需要咨询的邻居就越多。)
  • alpha参数控制边距距离和多样性之间的权衡;通常对于高维向量,1.2就足够了;对于2D或3D数据集,建议使用2.0。有关更多详细信息,请参阅DiskANN论文。
  • GraphSearcher的Bits参数旨在根据外部谓词控制结果集,在本教程中不会使用。

步骤2:对GraphSearcher的更多控制

保持Builder不变,更新后的搜索代码如下:

// 使用GraphSearcher和SearchScoreProvider搜索随机向量 VectorFloat<?> q = randomVector(originalDimension); try (GraphSearcher searcher = new GraphSearcher(index)) { SearchScoreProvider ssp = SearchScoreProvider.exact(q, VectorSimilarityFunction.EUCLIDEAN, ravv); SearchResult sr = searcher.search(ssp, 10, Bits.ALL); for (SearchResult.NodeScore ns : sr.getNodes()) { System.out.println(ns); } }

注释:

  • Searcher的分配成本适中,因为在构造时需要初始化大量内部状态。因此,JVector支持搜索器池化(例如,使用ExplicitThreadLocal,下面会介绍)。
  • 在管理GraphSearcher实例时,您还负责构造一个SearchScoreProvider,您可以将其视为:给定一个查询向量,告诉JVector如何计算索引中其他节点的分数。SearchScoreProvider可以是精确的,如这里所示,或者是ApproximateScoreFunction和Reranker的组合,下面会介绍。

步骤3:测量召回率

如果一个速度极快的向量索引无法返回准确的结果,那么它就没有多大用处。作为健全性检查,SiftSmall包含一个辅助方法_testRecall_。将其连接到我们的代码主要涉及将SearchScoreProvider转换为工厂lambda:

Function<VectorFloat<?>, SearchScoreProvider> sspFactory = q -> SearchScoreProvider.exact(q, VectorSimilarityFunction.EUCLIDEAN, ravv); testRecall(index, queryVectors, groundTruth, sspFactory);

如果运行代码,您会看到每次召回率略有不同(由testRecall打印):

(OnHeapGraphIndex) Recall: 0.9898
...
(OnHeapGraphIndex) Recall: 0.9890

考虑到所创建索引的近似性质以及build调用的多线程并发引入的扰动,这是预期的结果。

步骤4:将索引写入磁盘并从磁盘加载

代码:

Path indexPath = Files.createTempFile("siftsmall", ".inline"); try (GraphIndexBuilder builder = new GraphIndexBuilder(bsp, ravv.dimension(), 16, 100, 1.2f, 1.2f)) { // 构建索引(在内存中) OnHeapGraphIndex index = builder.build(ravv); // 使用默认选项将索引写入磁盘 OnDiskGraphIndex.write(index, ravv, indexPath); } // 磁盘上的索引需要一个ReaderSupplier(而不仅仅是Reader),因为我们希望它为搜索打开额外的读取器 ReaderSupplier rs = new SimpleMappedReaderSupplier(indexPath); OnDiskGraphIndex index = OnDiskGraphIndex.load(rs); // 根据(精确计算的)基准真值测量我们的召回率 Function<VectorFloat<?>, SearchScoreProvider> sspFactory = q -> SearchScoreProvider.exact(q, VectorSimilarityFunction.EUCLIDEAN, ravv); testRecall(index, queryVectors, groundTruth, sspFactory);

注释:

  • 我们可以通过单个方法调用将内存中构建的索引(如本例)写入磁盘。
  • 加载和搜索磁盘上的索引需要一个ReaderSupplier,它提供RandomAccessReader对象。RandomAccessReader接口旨在由使用项目进行扩展。例如,DataStax Astra实现了一个由Cassandra块缓存支持的RandomAccessReader。JVector提供了两种现成的实现。
    • SimpleMappedReader:使用FileChannel.map实现,这意味着它与所有可运行JVector的Java版本兼容,但也意味着它限制为2GB文件大小。SimpleMappedReader主要用于示例代码。
    • MemorySegmentReader:使用较新的MemorySegment API实现,没有文件大小限制,但仅限于Java 22+。(实际的MemorySegmentReader代码与Java 20+兼容,但为了方便,我们将其保留在22+模块中。有兴趣的读者可以重构构建以改进这一点。)如果您没有特殊需求,建议在生产环境中使用MemorySegmentReader。

第5步:在搜索中使用压缩向量

使用乘积量化压缩向量的方法如下:

// 计算并将压缩向量写入磁盘 Path pqPath = Files.createTempFile("siftsmall", ".pq"); try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(pqPath)))) { // 使用PQ压缩原始向量。这代表了128 * 4 / 16 = 32倍的压缩比 ProductQuantization pq = ProductQuantization.compute(ravv, 16, // 子空间数量 256, // 每个子空间的质心数量 true); // 对数据集进行中心化 ByteSequence<?>[] compressed = pq.encodeAll(ravv); // 将压缩向量写入磁盘 PQVectors pqv = new PQVectors(pq, compressed); pqv.write(out); }
  • JVector也支持二进制量化,但由于BQ对搜索精度的影响很大,所以通常不如PQ有用。

然后我们可以通过从PQVectors获取快速ApproximateScoreFunction,并从索引View获取Reranker来连接压缩向量到两阶段搜索:

ReaderSupplier rs = new MMapReaderSupplier(indexPath); OnDiskGraphIndex index = OnDiskGraphIndex.load(rs); // 加载我们刚刚写入磁盘的PQVectors try (RandomAccessReader in = new SimpleMappedReader(pqPath)) { PQVectors pqv = PQVectors.load(in); // SearchScoreProvider首先使用加载到内存中的PQVectors进行初步筛选, // 然后使用存储在索引磁盘中的精确向量进行重新排序 Function<VectorFloat<?>, SearchScoreProvider> sspFactory = q -> { ApproximateScoreFunction asf = pqv.precomputedScoreFunctionFor(q, VectorSimilarityFunction.EUCLIDEAN); Reranker reranker = index.getView().rerankerFor(q, VectorSimilarityFunction.EUCLIDEAN); return new SearchScoreProvider(asf, reranker); }; // 对照(精确计算的)真实结果测量我们的召回率 testRecall(index, queryVectors, groundTruth, sspFactory); }
  • PQVectors提供precomputedScoreFunctionFor和scoreFunctionFor两种工厂方法。顾名思义,前者预先计算了ADC(非对称距离计算)所需的PQ码本距离片段。这对于搜索除最小索引之外的所有索引都更快,但如果您确实有一个微小的索引或需要在其他上下文中执行ADC,那么没有预计算的scoreFunctionFor版本会派上用场。

这组功能是经典的DiskANN设计。

第6步:构建大于内存的索引

JVector还可以应用PQ压缩来允许索引大于内存的数据集:只在内存中保留压缩向量。

首先,我们需要设置一个可以添加新向量的PQVectors实例,以及使用它的BuildScoreProvider:

// 计算码本,但暂不编码任何向量 ProductQuantization pq = ProductQuantization.compute(ravv, 16, 256, true); // 在构建索引时,我们将压缩新向量并将其添加到这个支持PQVectors的List中; // 这用于对构建搜索进行评分 List<ByteSequence<?>> incrementallyCompressedVectors = new ArrayList<>(); PQVectors pqv = new PQVectors(pq, incrementallyCompressedVectors); BuildScoreProvider bsp = BuildScoreProvider.pqBuildScoreProvider(VectorSimilarityFunction.EUCLIDEAN, pqv);

然后我们需要设置一个OnDiskGraphIndexWriter,以完全控制构建过程。

Path indexPath = Files.createTempFile("siftsmall", ".inline"); Path pqPath = Files.createTempFile("siftsmall", ".pq"); // Builder创建看起来大致相同 try (GraphIndexBuilder builder = new GraphIndexBuilder(bsp, ravv.dimension(), 16, 100, 1.2f, 1.2f); // 首次显式使用Writer,这是OnDiskGraphIndex.write背后的内容 OnDiskGraphIndexWriter writer = new OnDiskGraphIndexWriter.Builder(builder.getGraph(), indexPath) .with(new InlineVectors(ravv.dimension())) .withMapper(new OnDiskGraphIndexWriter.IdentityMapper()) .build(); // 压缩向量的输出 DataOutputStream pqOut = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(pqPath))))

完成后,我们可以一次索引一个向量:

// 逐个向量构建索引(在磁盘上) for (VectorFloat<?> v : baseVectors) { // 压缩新向量并将其添加到PQVectors(通过incrementallyCompressedVectors) int ordinal = incrementallyCompressedVectors.size(); incrementallyCompressedVectors.add(pq.encode(v)); // 将完整向量写入磁盘 writer.writeInline(ordinal, Feature.singleState(FeatureId.INLINE_VECTORS, new InlineVectors.State(v))); // 现在将其添加到图中 -- 之前的步骤必须先完成,因为在addGraphNode构建过程中运行的搜索会使用PQVectors和InlineVectorValues builder.addGraphNode(ordinal, v); }

最后,我们需要运行cleanup()并将索引和PQVectors写入磁盘:

// cleanup进行最终的maxDegree强制执行,并处理其他场景,如删除的节点 // 在这里我们不需要担心这些 builder.cleanup(); // 完成索引写入(通过填充边列表)并写入我们完成的PQVectors writer.write(Map.of()); pqv.write(pqOut);

注释:

  • 切换到增量索引构建时,搜索代码不会改变 - 磁盘上的索引结构相同,只是(可能)大得多。
  • OnDiskGraphIndexWriter::writeInline通过同步实现线程安全,但如果您计划在多线程场景中使用它们,必须注意支持结构也是线程安全的(本例不是多线程的)。或者,您可以序列化对PQVectors的更新,只保留对GraphIndexBuilder::addGraphNode的并发调用。这代表了构建时间的大部分,所以使用这种方法您将看到良好的性能。

不太明显的要点

  • 嵌入模型产生的输出来自一致的向量分布。这意味着您可以保存和重用ProductQuantization码本,即使是对不同的向量集,只要您第一次构建时有足够大的训练集。ProductQuantization.MAX_PQ_TRAINING_SET_SIZE(128,000个向量)已被证明足够大。
  • JDK ThreadLocal对象只能从创建它们的线程中引用。这是一个难以适应缓存Closeable对象(如GraphSearcher)的设计。JVector提供了ExplicitThreadLocal类来解决这个问题。
  • 融合ADC仅与乘积量化兼容,不与二进制量化兼容。这并不是什么大损失,因为很少有模型生成最适合BQ的嵌入。尽管如此,非融合索引仍然支持BQ。
  • JVector大量利用Panama Vector API(SIMD)进行ANN索引和搜索。我们发现在索引和乘积量化过程中,内存带宽可能会饱和,导致进程变慢。为避免这种情况,索引和PQ构建的批处理方法使用PhysicalCoreExecutor将操作量限制在物理核心数量。默认值是Java看到的处理器数量的1/2。这可能并不适用于所有设置(例如,无超线程或混合架构),因此如果您希望覆盖默认值,请使用-Djvector.physical_core_count属性,或传入您自己的ForkJoinPool实例。

高级功能

  • 融合ADC表示为在增量索引构建期间支持的Feature,类似于上面的InlineVectors。请参阅Grid类以获取示例代码。
  • 各向异性PQ内置于ProductQuantization类中,可以提高召回率,但除了在每个模型的基础上进行实验外,没有人知道如何调整它(使用T/threshold参数),而选择错误的设置可能会使情况变糟。从论文的图3中可以看出: APQ在Glove上的性能随T的增加先改善后降低
  • JVector通过GraphIndexBuilder::markNodeDeleted支持原地删除。被删除的节点会在GraphIndexBuilder::cleanup期间被移除,连接会被替换,运行时间与被删除节点的数量成正比。
  • 要checkpoint一个图并重新加载以继续编辑,请使用OnHeapGraphIndex::save和GraphIndexBuilder.load。

算法背后的研究

开发和测试

本项目组织为多模块Maven构建。目的是生成一个适合作为任何Java 11代码依赖的多版本jar包。当在启用Vector模块的Java 20+JVM上运行时,将使用优化的向量提供程序。一般来说,项目结构适合用JDK 20+构建,但当JAVA_HOME设置为Java 11到Java 19时,某些构建功能仍然可用。

基础代码在jvector-base中,将为Java 11版本构建,相应地限制语言特性和API。jvector-twenty中的代码将针对Java 20语言特性/API编译,并包含在最终的多版本jar中,面向支持的JVM。jvector-multireleasejvector-basejvector-twenty打包为发布用的多版本jar。jvector-examples是一个额外的同级模块,使用jvector-base/jvector-twenty的reactor表示来运行示例代码。jvector-tests包含项目的测试,能够在Java 11和Java 20+ JVM上运行。

要运行测试,使用mvn test。要在Java 20+上运行测试,使用mvn test。要在Java 11上运行测试,使用mvn -Pjdk11 test。要运行单个测试类,使用Maven Surefire测试过滤功能,例如,mvn -Dtest=TestNeighborArray test。你也可以使用方法级过滤和模式,例如,mvn -Dtest=TestNeighborArray#testRetain* test

你可以直接运行SiftSmallBench来了解这里发生的一切。Bench将自动下载所需的数据集到fvechdf5目录。SiftSmall使用的文件可以在项目根目录的siftsmall目录中找到。

要运行这两个类,你可以使用Maven exec-plugin通过以下命令:

mvn compile exec:exec@bench

或者对于Sift:

mvn compile exec:exec@sift

Bench接受一个可选的benchArgs参数,可以设置为以空格分隔的正则表达式列表。如果提供的任何正则表达式在数据集名称中匹配,该数据集将被包含在基准测试中。例如,要只运行glove和nytimes数据集,你可以使用:

mvn compile exec:exec@bench -DbenchArgs="glove nytimes"

要在没有JVM向量模块的情况下运行Sift/Bench,你可以使用以下命令:

mvn -Pjdk11 compile exec:exec@bench

mvn -Pjdk11 compile exec:exec@sift

... -Pjdk11调用也适用于JAVA_HOME指向Java 11安装的情况。

要发布,请配置~/.m2/settings.xml以指向OSSRH,然后运行mvn -Prelease clean deploy

编辑推荐精选

TRAE编程

TRAE编程

AI辅助编程,代码自动修复

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI辅助写作AI工具蛙蛙写作AI写作工具学术助手办公助手营销助手AI助手
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

咔片PPT

咔片PPT

AI助力,做PPT更简单!

咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。

讯飞绘文

讯飞绘文

选题、配图、成文,一站式创作,让内容运营更高效

讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。

热门AI辅助写作AI工具讯飞绘文内容运营AI创作个性化文章多平台分发AI助手
材料星

材料星

专业的AI公文写作平台,公文写作神器

AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。

下拉加载更多