作者:赵裕隆,腾讯大数据研发工程师
本文整理自腾讯大数据工程师在 StarRocks 年度峰会上的分享,深入探讨了向量检索技术的原理与应用。此功能已应用到腾讯内部多个场景,引入 StarRocks 后,业务不仅不需要维护多套数据库还在性能上有了显著的提升:
性能提升:查询延迟从原本的 15 秒(TOP 10,000 数据)缩短到 2 秒,效率提升超过 7 倍。
成本优化:在实际验证中,系统运行成本降至原来的 1/3。
AI 和大模型无疑是当前的热门话题,作为从事数据工作的我们,也希望能够紧跟这一趋势,探索如何与 AI 实现更紧密的结合。这正是我们最初的诉求。随着大模型的兴起,推动了公司在这一背景下对向量检索场景的深入探索,也为我们进一步拓展在 StarRocks 上的应用提供了新的机遇。
向量检索技术浅析
什么是向量检索
什么是向量检索呢?简单来说,向量检索是通过给定一个查询向量,在特征数据库中找到与之距离最近的 k 个向量。举个例子,如果我们把今天会场的所有人作为特征向量,那么向量检索的任务就是找到与我最相似的 10 个人。用通俗的语言来说,它其实就是一个 Top N 查询。虽然本质上,向量检索就是一个 Top N 查询,但由于深度学习中几乎所有内容都用向量表示,所以我们将其称为“向量检索”。
近似最近邻查询
之所以在向量检索中进行 Top N 查询显得如此复杂,主要原因在于向量本身的维度通常非常高。这导致了计算的复杂性,每次计算都需要进行数百到数千次的浮点运算。此外,随着维度的增加,搜索空间呈指数级增长,出现了所谓的“维度灾难”。因此,传统的 Top N 查询方法无法满足这一业务需求。尤其是在大模型应用中,例如 RAG(Retriever-Augmented Generation),它对向量检索的响应时间要求通常在毫秒级。因此,传统的暴力检索方法或简单的 Top N 查询无法满足这样的性能要求。
为了解决这个问题,引入了近似最近邻查询(Approximate Nearest Neighbor Search, 下文简称 ANN)。虽然无法快速获得完全精确的 Top N 结果,但我们可以通过近似方法提高性能,从而达到更快的响应速度。这一技术其实在业内已经研究了几十年,只是在大模型兴起的背景下,我们在 StarRocks 上进行了一些扩展以及实现。
ANN 查询最常用的度量标准是欧式距离和余弦距离。这两个度量方法可以理解为 Top N 查询中的“度量单位”。它们本质上是 Function,通过计算向量之间的距离来排序结果。由于是近似查询,ANN 引入了“召回率”的概念,即近似答案与真实答案之间的匹配比例。
近邻索引技术
ANN 查询技术,经过了多年的发展,经历了不同的迭代过程。最早的实现方法包括哈希和树结构。哈希技术的思想与我们社区最近推广的 N-gram 布隆过滤器有相似之处,它的核心是局部敏感性哈希。具体来说,类似的向量会被映射到同一个哈希桶中。这种方法在效率上有所提升,但也存在准确性不高的问题。
树结构方法则通过对空间进行划分并进行剪枝来优化检索性能。ClickHouse 最早就是通过树结构实现向量检索的。然而,当搜索维度变得非常高时,这种树结构会退化为类似暴力检索的方式,其性能和暴力检索相差无几。因此,这些早期的技术现在基本上已经被淘汰。
目前,ANN检索中有两种非常主流的算法,分别是量化与倒排,它们的结合体是 IVFPQ(Inverted File with Product Quantization)。
首先,量化是一种压缩技术,它通过将所有向量进行聚类,将每个向量映射到其所在的簇,从而减少存储空间和计算开销。在查询时,我们只需计算查询向量与簇中心的距离,进一步降低了计算复杂度。然而,量化本身不具备剪枝功能,因此需要引入倒排技术来弥补这一不足。倒排技术可以通过快速定位候选向量集来提高检索效率。这是一种目前最主流的向量检索索引技术。
第二种主流的 ANN 算法是基于图的算法,代表作是 HNSW(Hierarchical Navigable Small World)。其基本思想是通过构建近邻图来进行高效的近邻查询。HNSW 在图结构的基础上进行了分层,并利用跳表的结构来进行剪枝,从而减少计算开销。
HNSW 是当前最精确的近邻检索算法之一,它提供了较为准确的查询结果。与 IVFPQ 相比,IVFPQ 是基于压缩的近似距离,它返回的是经过压缩的距离,因此不一定是完全准确的,而图算法则能够提供更加精确的检索结果。
主流的向量检索数据库
目前,向量检索的两种主流算法也是我们腾讯内部索引库支持的主要检索技术。实际上,向量检索已经不是一个全新的话题,许多业内数据库也在进行相应的扩展。如果不做这方面的拓展,可能会面临被淘汰的风险。以下是几种常见的向量数据库:
-
PostgreSQL:作为经典的数据库,它的代码质量高,设计也相对成熟,成为了我们后续实现的参考标杆。
-
Milvus:作为当前最火的向量数据库之一,Milvus 性能优秀,是新兴的主流选择。
-
腾讯 VectorDB:腾讯也有自己的向量数据库,名为 VectorDB,主要用于我们内部系统的核心部分,作为后续产品的基础。
此外,NoSQL 数据库领域,Elasticsearch 由于其基于倒排索引和文本检索的强大生态系统,广泛应用于各种场景。而在 OLAP 领域,Hologres 和 ClickHouse 也有向量检索的相关扩展。
业务场景
那么,我们的场景在哪里呢?在 StarRocks 上如何进行向量检索?我们有哪些优势?
我们团队的整体策略是这样的:尽管在性能上,我们可能没有像 Milvus 那样经过多年深耕,做到极致优化,但我们的优势在于成本更低,且能够处理更大的数据量。对于生态较为成熟的 Elasticsearch,尽管它的性能优越,但在某些场景下,我们的性能表现更好。因此,我们选择在这一“夹缝”中寻求生存,提出了一个经济适用型的 StarRocks 向量索引扩展方案。
这个方案已经应用到我们的真实业务中,解决了大规模数据处理的实际需求。例如,我们的业务数据量非常庞大,需要通过 VectorDB 内核处理,经过四套系统和三条链路的分容机延迟。具体流程如下:
-
数据压缩:由于无法直接处理 TOP100000 级别的查询,首先对数据进行压缩。
-
粗排:在压缩后进行粗排,减少计算量。
-
文本检索与精排:结合 MongoDB 和 Elasticsearch 完成文本检索和精排。
-
Redis 缓存:最后,通过 Redis 缓存获取真实结果。
这一链路非常复杂,响应时间通常在分钟级别。
因此,用户的诉求是希望将文本检索、向量索引和多维分析整合在一个系统中,形成闭环,并尽可能降低接入成本。这包括降低组件的使用成本和用户接入的成本。同时,系统需要具备高可用性 HA,友好的用户体验,并且支持亚秒级或秒级的查询延迟,召回率要达到 95% 以上。
当我第一次接触到这个业务时,就觉得它天生适合在 StarRocks 上扩展向量索引。实际上,StarRocks 非常适合承接这种业务,并且我们也成功地实现了这一落地。接下来,我会简单介绍一下我们是如何实现向量检索的。
StarRocks 实现向量检索的原理及优化
整体架构
我们内部的实现架构是基于服务分析一体化的向量数据库雏形。StarRocks 本身是 MPP 架构,适用于 OLAP 分析,但不适合高 QPS 的实时查询。因此,我们在 StarRocks 上拓展了服务架构,以支持高并发的专有向量查询服务。
对于需要联合分析的场景,我们仍然沿用了旧的 pipeline 链路。此外,我们自研了一个名为 TenANN 的索引库,集成了业内主流的两种向量检索算法:HNSW 和 IVFPQ。
语法设计
向量检索和传统的 SQL 查询在语义上存在一定冲突,所以我们首先要讨论语义冲突。具体来说,向量检索是基于 Top N 的索引,而传统的 Top N 算子不同,向量检索与 filter 算子有着等价关系,并且两者的顺序是有影响的。举个例子:在会场里找到 10 个与我最相似的人,然后加上 filter 条件,要求这 10 个人都是男性。
到底是先执行 Top N 查询,还是先进行 filter 过滤呢?这个顺序其实非常关键。如果我们先找到最相似的 10 个人,再进行男性筛选,显然 I/O 操作会较小,因为我们已经限制了候选人范围。但问题在于,这样的处理方式与用户的 “limit 10” 语义是有冲突的。最终的结果会与预期不一致,因为筛选顺序的不同,结果会有所不同。
如果我们先在所有用户中筛选男性,再从男性中找出最相似的 Top N,实际上就等同于全表扫描了。因此,这样的做法与单纯的 Top N 查询逻辑有很大不同,涉及到多个逻辑计划的设计。最初,我们考虑为向量检索设计一套专有的语法,以便根据不同场景生成不同的逻辑计划。然而,用户的需求是接入成本要低一些,因此我们最终选择了保持简单的 Top N 查询,并在内部自适应地生成不同的逻辑计划,确保满足用户对召回率的需求。
最终的实现方案以用户需求为基础,支持了纯向量检索、混合标量与向量检索、范围搜索,以及标量混合范围检索等功能。
查询过程
这套查询思路不仅适用于我们的 TenANN 索引库,理论上也可以在任何在线引擎中使用。无论是查询还是写入,我们的核心思想是尽量避免对引擎或 StarRocks 做过多侵入,考虑到人力有限且可维护性要求较高,因此我们保持对系统的低侵入性。
在具体实现上,我们的向量检索库不仅与 StarRocks 的 bitmap 索引或 BloomFillter 等机制兼容,而且其返回值也与传统的二进制值(yes/no)不同。相较于只返回匹配结果的行号,我们的检索结果不仅会返回对应的行号,还会提供向量距离,明确表示该向量与查询向量之间的相似度。
我们的方法本质上很简单。首先,在匹配向量检索时,我们会直接调整逻辑计划,将能下推到向量索引的所有条件都下推到底层执行。这样,我们可以通过上下文条件获得对应的距离信息,并将该距离从行转列,物化成一个新的列。接着,我们会将排序字段替换为计算出的向量距离,从而实现向量检索的查询过程。
不过,实际情况中,涉及的 case 非常复杂,因为向量检索的算法本身需要进行多方面的调优,且需要支持大量参数的灵活调整。
查询实现——具体索引的计算实现
我们的大体思路已介绍,但针对不同的索引类型,我们的实现策略有所不同。HNSW 是一种较为精确的索引类型,因此,它不需要遵循刚才所述的通用思路,直接在底层执行即可。
而 IVFPQ 是一种近似查询的算法,它在返回结果时,并不一定是完全精确的。尽管这种不完全精确的返回结果对用户来说是可预期的,但为了提高结果的准确性,我们在查询过程中,依然会进行二次精排。具体来说,我们会将初步得到的结果重新引入到原有的逻辑计划中,再进行一次精确排序,以确保最终返回的结果更为准确。
查询优化
在查询优化方面,我们也做了很多工作,尤其是在索引级别和查询性能的优化上。考虑到索引是在 segment 级别进行写入的,因此每次进行 segment Top N 查询时,可能会出现读放大的情况。这是因为,如果我们只希望得到 10 条数据,但每个 segment 都拿出 10 条数据,这样的 m × n 的读放大效应是非常明显的。
为了优化这一过程,我们采用了 tablet 级别的预排序。具体来说,在每个 segment 进行初始化时,我们会先从向量索引中获取 Top N 的 row ID,并在 tablet 级别进行预排序。这意味着在分发数据并进行物化处理之前,我们已经对数据进行了排序。
在向量检索中,前过滤和后过滤是影响查询效率和精度的重要概念。你提到的前过滤(先执行过滤再做检索)和后过滤(先执行检索再进行过滤)有明显的性能与精度差异:
-
前过滤:先根据某些共同条件(如性别、年龄等)过滤数据,然后在过滤后的数据中进行向量检索。这样的策略精度最高,因为它首先缩小了数据的范围,然后在有限的数据集上进行检索,能保证查询的准确性。但它的性能较差,可能会造成 I/O 负担,特别是在大数据量的情况下,和扫全表的成本差不多。
-
后过滤:则是先进行向量检索,再进行过滤。这种方式性能较好,因为它可以在更大的数据集上进行检索,减少了 I/O 的开销,但由于过滤是在检索后进行的,精度可能有所下降。
-
混合过滤和迭代式后过滤:为了平衡精度和性能,我们选择了迭代式后过滤,即通过模拟前过滤的效果,逐步进行多轮迭代,直到满足足够的召回质量。这种方式在性能上优于前过滤,能够保持较好的精度。
实际应用中,我们发现迭代式后过滤是最具综合效益的方案,它既能保证召回率,又能在性能上做出妥协,满足了用户的需求。因此,最终社区代码中提供了前过滤和迭代式后过滤这两种方案。
索引写入原理与实现
在写入方面,我们主要考虑了三个方面:一是存储级别的选型,二是如何构建索引的写入流程,三是聚类中心重训练的设计。这三个方面是我们在初期设计方案时重点考虑的问题。
首先,关于存储级别的选型,我们遇到的第一个问题是选择在 segment 级别还是 tablet 级别进行索引。利弊如下:
Segment 级别的索引:可以同时应用于主键表和明细表,且由于 StarRocks 本身使用 bitmap 索引,逻辑上可以直接跟随 StarRocks的处理方式。因此,不需要重新映射 UID,因为StarRocks 的 UID 映射是以 segment 级别为单位的。这意味着我们可以直接将向量索引逻辑嵌入进来,无需额外处理 UID 的映射。
Tablet 级别的索引:这种方式类似于主键索引,只能应用于主键表上。然而,它的缺点是需要进行二次映射,至少要跳两次才能找到真正的数据列,这增加了复杂性和延迟。
尽管 segment 级别的索引简便,但也可能会导致较严重的读放大问题,这也是我们在选型时考虑的一个潜在缺点。
索引重建
我们最终选择了 segment 级别 的索引的一个关键因素在于索引重建的代价。在业内,当前的大多数向量索引并没有增量索引的功能。例如,Milvus 最近在尝试图形增量索引,但整体而言,增量索引还没有得到广泛实现。如果我们选择 tablet 级别的索引,重建索引的代价将非常高,因为没有增量更新的机制。
为什么需要索引重建?
以 IVFPQ 为例,它使用聚类的方法进行索引。IVFPQ 在数据写入时,会通过聚类模型对数据进行剪枝。由于数据是动态变化的,随着数据的不断写入,聚类效果会逐渐变差。特别是当数据中存在“脏数据”时,聚类模型的效果会变得更差。因此,我们的策略是在每次 Compaction 时主动触发索引重建,以确保聚类模型保持最新和有效。
为什么选择 Segment 级别?
选择 segment 级别的原因之一就是它重建的代价相对较小。
此外,我们还发现,在进行聚类时,如果 segment 数据量过小,则无法聚类。通过实验,我们发现,在数据量较小的情况下,采用暴力方法处理反而能够获得更好的效果。
索引写入
为了解决小数据量聚类的问题,当数据量小到一定阈值时,我们选择写入空索引,并在查询时联动处理。当遇到空索引时,采用暴力计算函数对该 segment 的结果进行计算,最后通过整体排序生成最终的 Top N 结果。这样的处理方式既避免了 badcase 的出现,也实现了性能的提升。
此外,为了解决严重的读放大问题,我们引入了自适应的 segment 控制和内存刷写机制,以有效缓解该问题。这种优化结合了业务场景的实际需求,当业务规模增长到一定程度时,读放大问题会显著影响性能,针对这个问题做了优化。
TenANN 实现
在谈到查询和写入之后,我们来聊聊为什么要自研一个索引库。业内有句话说得好:“没有什么问题是一层封装解决不了的。”我们也是基于这样的理念,构建了一套专用的索引库。这套索引库不仅可以被 StarRocks 使用,还能为其他引擎提供统一接口,方便它们快速接入并实现向量索引的功能。在这个基础上,通过简单的逻辑,就能轻松实现我们讨论的各种向量检索能力。
我们的底层封装了多种索引库,实现了多种索引类型的支持。整体设计以通用性、可扩展性、易用性和高性能为核心目标。此外,我们还在此基础上自研了许多功能。例如,新增了索引缓存功能,这是传统索引库(如 Faiss)所不具备的。我们还原生支持了 HNSW 和 IVFPQ 的范围查询功能,将会在后续讲解中具体说明。
特别是在缓存优化方面,针对 IVFPQ 文件级别读取所带来的内存浪费和缓存效率问题,我们开发了基于 block 的内存缓存机制。通过这一优化,只需原内存的一半(50%),就能达到纯内存查询时的性能和召回率。同时,我们为外部引擎提供了统一的接口,大大提升了使用的便捷性。
Range Search
关于 Range Search,如果用 SQL 的语言来抽象,它相当于在 Top N 查询的基础上增加了一个 FILTER,对结果的分数范围进行了限定。这种范围查询模式在实际业务中非常常见。传统的做法通常是通过 TOP K 模拟 Range Search,即先查询出前 K 个结果,再进行范围过滤。但我们实现了一次查询直接识别这种模式,并在一次召回中就返回符合条件的向量索引结果。
在实现上,不同的索引方式有不同的处理方法:
-
HNSW
HNSW 基于图的分层结构,我们的 Range Search 实现是通过 ANN(近邻搜索)找到最近的点,然后在图的每一层中不断以半径 r 搜索所有距离目标点在 r 范围内的点。最后汇总结果,能够在一次召回中完成范围查询。
-
IVFPQ
对于 IVFPQ,我们使用了一个巧妙的小技巧——三角不等式。由于 IVFPQ 是基于向量压缩的算法,查询向量和压缩向量的距离(记为 b)可以在 IVFPQ 算法中直接得到。而压缩向量与原始向量的距离(记为 c)是已有的映射关系。因此,可以计算出 l = b - c。根据三角不等式,l 必然小于查询向量和原始向量的距离 a。如果 r 小于 a,则可以判定查询向量和原始向量的距离一定大于 r,进而过滤掉不符合条件的数据点。
Block Cache
在业内的评测中,通常存在两类榜单:一类是算法榜单,另一类是引擎榜单。而我们这里提到的是算法榜单,称为 ANN Benchmark。
引入 Block Cache 后的测试结果表明,性能并没有出现回退,同时召回率也完全满足业务需求。(这个结论,其实也是去年的结论,现在会更好一些。)
性能优化测试总结
在单机环境下,我们在 30 万到 100 万数据规模和 50 维向量的情况下,可以实现十几毫秒的延迟。而在 QPS 测试中,即便当时还未引入完整的 Serving 架构,测试结果已达到约 2000。尽管与 Milvus 等专用向量数据库中的高度优化模型相比略有不足,但在实际业务场景中已经完全足够。
在召回率测试中,我们对比了业务之前使用的专用向量数据库,结果显示召回率完全达标。同时,写入的稳定性也能够覆盖从百万到十亿级别的多种数据量场景,比 Elasticsearch 等其他方案表现更为优异。
最初的测试结论表明,在尚未优化的情况下,我们的向量检索就比暴力检索快了 4-5 倍。在更贴近现实的大规模数据场景中,性能提升往往能达到百倍。现阶段的系统实现中,与暴力检索相比,我们的性能普遍有 1 到 2 个数量级的提升。
性能对比
刚刚提到的第二个榜单是引擎端到端的性能榜单。在测试中,我们也进行了评比,对比了多种引擎,包括传统数据库如 PostgreSQL 和云上的 Elasticsearch 。结果显示,无论是在性能还是 QPS 上,我们的方案都表现出色。
不过,需要指出的是,OLAP 系统并不是很适合高 QPS 场景。如果深入分析榜单,第一名通常是一些专用向量数据库,其 QPS 能达到数万级别。
核心优势
在向量检索的业务落地中,我们拥有独特的竞争优势,总结为以下三点:
-
强大的数据管理能力
分析型数据库,能够高效支持海量数据管理。不仅能处理结构化数据,还支持多种数据类型,包括字符串、JSON、空间、时序、地理数据等。同时,系统具备高效的存储与查询能力,并支持多种表结构和灵活的导入方式,满足多样化业务需求。
-
面向 SQL 的友好生态
长期以来,我们以 SQL 为核心,提供标准化且先进的处理流程。这种对 SQL 的深度支持使得用户上手成本低,开发体验友好。此外,我们的架构设计简化了整个流程,将导入、粗排、精排、向量检索、复杂查询、分析和缓存整合为一站式闭环。
-
列式存储的显著优势
每列都可以根据其数据类型进行优化处理。有更高的压缩比,实现自列编码,有效降低存储成本。
StarRocks 向量检索在腾讯的实践案例
在完成初期系统构建后,我们成功将其应用到多个实际业务场景,并取得了显著成果。以下是以一个典型案例为标杆的成果分享:
之前,业务需要维护多套数据库,同时还需支持三套写入流程。这种架构带来了数据一致性难以保障的问题,同时调用链路冗长,整体耗时超过 15 秒,且资源成本居高不下。
引入 StarRocks 后,情况得到了大幅改善:
架构简化:通过 StarRocks 的一站式闭环能力,取代了原先的多套系统,大幅降低了成本。
性能提升:查询延迟从原本的 15 秒(TOP 10,000 数据)缩短到 2 秒,效率提升超过 7 倍。
成本优化:在实际验证中,系统运行成本降至原来的 1/3。
挑战及未来规划
在构建向量检索和优化系统性能的过程中,我们面临了多项挑战,以下是关键问题和对应的解决路径:
-
高并发场景挑战
OLAP系统在高并发、低延时场景下并无天然优势,尤其是在支持复杂语义(如 Top N 结合多级聚合算子)时,短路径优化方案难以直接适配。此外,传统 pipeline 调度的准备阶段通常需要数毫秒,这无法满足向量检索对毫秒级响应的要求。
后续规划:
自研 Serving 架构:借鉴 ES 的 scatter-gather 模型,结合我们内部的优化实践,重新设计了一个针对向量检索的高效 Serving 层架构。
-
大数据量场景挑战
大数据量和高 K 值的检索场景可能导致严重的小文件问题和读放大现象。
后续规划:
预排序与增量索引构建:通过在 Tablet 层进行预排序,并支持增量索引构建以减少随机读操作,提升读写性能。
范围查询优化:结合 range search 技术,通过自适应调整参数 R 限制搜索范围,有效降低了读放大的影响。
-
RAG(Retrieval-Augmented Generation)场景复杂性
RAG 是一个相对宽泛的领域,目前缺乏标准化的解决方案,且功能需求多样。各类引擎试图在自身架构中尽可能囊括 RAG 的场景,但完整覆盖所有功能极具挑战性。
后续规划:
-
支持新鲜度检索与向上检索
-
混合检索
-
多路召回与自定义排序
-
文本预处理
我们团队深度使用 StarRocks,也一直与社区保持紧密合作。未来,我们也将持续贡献更多功能。希望能借助社区的力量,进一步扩大影响力,提升技术水平,共同将整个生态做得更大、更强。
更多相关:
腾讯大数据基于 StarRocks 的向量检索探索
混元大模型业务向量检索建设经验分享
此功能已在 StarRocks 3.4 版本上线,有兴趣的小伙伴欢迎下载试用:https://www.mirrorship.cn/zh-CN/download/community
更多交流,联系我们:StarRocks