← 返回博客
用户案例

Doris 直查 Paimon 索引:快手湖上向量检索的共建与落地

快手数据团队 · 2026/9/10
SelectDB 微信公众号
SelectDB 公众号
获取技术干货和产品动态
标签: 湖仓一体

导读:Apache Doris 5.0 多模湖仓预告(3)|生产实践篇:快手在生产环境用 Apache Doris 直接查询 Apache Paimon 表上的向量索引。向量与业务数据留在 Paimon 表中,检索由 Doris SQL 发起并下推到湖上执行,原有的入湖链路和 OLAP 读取方式不变。本文介绍 Paimon 2.0 的索引底座和 Doris 与 Paimon 两个社区共建的读取链路,并用千万级与亿级向量的实测结果说明实际表现。

LLM 与 RAG 应用普及后,语义检索成为数据平台的常规需求。常见做法是把向量连同过滤字段、返回字段一起同步到独立的向量数据库。这个做法的代价不在第一次同步,而在此后的长期维护:同步链路常驻运行并带来新鲜度窗口;向量与业务字段在两个系统中各存一份;权限与治理拆成两套,跨系统一致性还需要额外保障。

快手选择了另一条路:向量和业务列留在 Paimon 表中,由 Paimon 2.0 的 Vector Index 提供检索能力;Doris 通过 Paimon Rust 直接读取索引与数据,按桶并行完成候选召回与回表;用户仍然使用 Doris SQL,语句与查询 Doris 内表一致。

1. 数据底座:Paimon 2.0 索引能力介绍

1.1 索引与数据物理解耦

Paimon 2.0 的 Vector Index 建立在普通 Paimon 表之上,有两种形态:append 表上的 Global Vector Index,以及主键表上的 PK Vector Index。两者共用同一套索引实现与检索模式,区别在于索引怎样构建、命中结果怎样回表。快手的业务表是主键表,本文后续链路对应后者。

两种形态都把索引文件与数据文件物理解耦。Snapshot 同时引用 Data Manifest 和 Index Manifest,后者记录索引文件的类型、字段、位置和覆盖范围:append 表记录覆盖的 Row ID 范围,主键表记录覆盖的源数据文件。基于这个设计,加索引时不需要改动数据布局,同一张表继续服务批处理、流处理和普通 OLAP 读取。

索引文件与数据文件的关系见图 1。

01\-fig1\-index\-data\-decoupling\.png

图 1:索引文件与数据文件分开存放,由同一个 Snapshot 通过两类 Manifest 关联;主键表的索引段随 compaction 生成并记录源文件。

向量列用 ARRAY<FLOAT> 或定长的 VECTOR<FLOAT, N> 声明。主键表的向量索引通过表属性配置,随 compaction 自动构建,不需要单独触发。以主键表为例,通过 Spark SQL 建表时声明索引列、索引类型和距离度量:

CREATE TABLE vector_items (
item_id BIGINT,
embedding ARRAY<FLOAT> COMMENT '__VECTOR_FIELD;2048',
payload STRING
) TBLPROPERTIES (
'primary-key' = 'item_id',
'bucket' = '32',
'deletion-vectors.enabled' = 'true',
'pk-vector.index.columns' = 'embedding',
'fields.embedding.pk-vector.index.type' = 'ivf-rq',
'fields.embedding.pk-vector.distance.metric' = 'l2'
);
  • Spark SQL 没有 VECTOR 类型,列注释 __VECTOR_FIELD;2048 把 ARRAY<FLOAT> 声明为 2048 维定长向量;

  • nlist 等构建参数通过 fields.<col>.pk-vector.index.options 以 JSON 传入;

  • 精排倍数 refine_factor 可在表属性或查询选项中设置。

写入后,compaction 把桶内文件合并到 Level 0 以上时,顺带为向量列生成一个索引段,登记到 Index Manifest 并由新的 Snapshot 引用;索引段记录它覆盖的源数据文件,命中结果映射回文件内的物理行位置。新写入的数据在下一次 compaction 前不在任何索引段内,这段中间状态由第 2.2 节的检索模式处理。

1.2 五类向量索引

Paimon 2.0 Vector Index 当前支持五类索引实现,各自的特点与取舍如下:

image\.png

不同的索引类型在查询性能、召回率、索引大小和构建耗时上各有取舍。亿级 2048 维向量若按原始精度存,索引体积随之增大,远端读取的数据量也随之上升;RQ 的旋转残差量化提供更高的压缩率,代价是需要按质量目标调参。快手最终选择 IVF-RQ,以获得压缩率和召回之间的平衡。第 3.3 节实测 Recall 为 96.0%~99.6%

五类索引的取舍与快手的选择见图 2。

02\-fig2\-five\-index\-types\.png

图 2:五类索引各在索引体积与召回之间取一个点,快手选 IVF-RQ 以更高的压缩率换更少的远端读取。

2. 让索引回到数据湖:Doris 直查 Paimon 索引技术解析

让计算引擎直接利用湖上数据索引进行数据检索,需要具备以下几个能力:

  • 一个高性能的数据读取链路。

  • 对索引分布和索引覆盖范围的感知。

  • Top-K 等高频向量检索场景下的索引访问优化。

只有具备这些能力,才能进行高效的湖上数据检索分析。第 2.1 节介绍读取链路,第 2.2 节介绍索引感知与检索下推,第 2.3 节和第 2.4 节介绍 Top-K 场景下的两项索引访问优化。

2.1 高性能读取:从 JNI 到 Paimon Rust

Doris 早期读取 Paimon 时,对于 MOR(Merge-On-Read)场景,需要通过 JNI 调用 Paimon Java SDK 读取合并数据。该方案存在两个问题:

  • JVM 内存无法按查询粒度跟踪和管控。一个大查询耗尽内存时,Doris 侧无法进行细粒度的观测与控制。

  • MOR 的内存占用大,多并发会导致 JVM OOM;调大 JVM 堆又会挤占 BE 的 native 内存。

经过实际测试,快手最终选择了 Paimon Rust。Paimon Rust 由 Paimon 官方团队维护,在 MOR、索引读取方面,都可以和 Java 版本保持同步。实测中,Paimon Rust 的读取性能为 Paimon Java 的 2~3 倍,比 Doris 原生 reader 慢约 10%,差距主要来自 Arrow 到 Doris Block 的内存格式转换。同时,Paimon Rust 使用 native 内存,可以纳入 Doris 已有的内存管控体系,进行统一内存管理。

在代码层面,Doris 的 BE(Backend,C++ 执行进程)通过 C ABI 调用 Paimon Rust Reader。读取结果经 Arrow C Data Interface 在 Rust 与 C++ 之间传递,再转换成 Doris Block 交给执行层。Arrow 是列式格式,Rust 侧产出的数据可以被 C++ 侧直接按列消费,中间不需要一次逐行的转换。

读取链路见图 3。

04\-fig4\-doris\-paimon\-rust\-bridge\.png

图 3:Doris BE 经 C ABI 调用 Paimon Rust,结果以 Arrow 列式格式返回并转成 Doris Block;普通读取与向量检索共用这条链路。

2.2 把检索下推到湖上

向量检索和普通读取的区别在于:一次 Top-K 查询要读哪些行,在读之前并不确定,必须先在索引里算出来。如果 Doris 不利用索引,类似 ORDER BY ... LIMIT 形式的向量 Top-K 语句会退化成全量计算,即把整列向量读取到 BE 内存,逐行计算距离再排序。在 1.28 亿行、2048 维的规模上,单次查询要搬运约 1 TB 的向量数据。

因此,查询引擎必须能够将检索下推到湖上索引。为了实现这个目标,先要了解 Paimon 主键表向量索引的四个特性:

  • 索引按 (Partition, Bucket) 组织,桶(Bucket)既是数据分片单位,也是索引单位。

  • 索引由 Paimon 后台的 compaction 过程产生,因此只覆盖已 compaction 的数据,刚写入的 L0 文件不在任何索引中。一次正确的检索因此需要归并三个操作:索引召回、未覆盖文件的精确回退、删除向量(Deletion Vector)掩码计算。

  • 一个索引段是否有效,取决于它覆盖的源文件集合是否仍等于该桶当前的活跃文件集合。这个判断需要看到整个桶的完整文件集。

  • 快手的业务表是 Paimon 主键表,主键表的行会被更新、合并,没有稳定的全局 Row ID。所以索引记录的是物理坐标,即数据文件加文件内行号,召回后需要通过物理行号回到数据文件取行。

基于以上特性,Doris 使用如下索引查询规划:

  • Doris 的 FE(Frontend,负责查询规划)按 Paimon 表的桶枚举扫描单元,获得每个单元的完整文件清单。

  • 扫描单元下发给不同 BE 节点,多台 BE 并行处理各自获得的桶的文件清单,即在桶内完成召回、精确回退、精排、回表和删除向量的应用。

  • 跨桶的全局 Top-K 由 Doris 自身的排序归并算子完成。

在该方案中,向量检索由 Paimon 内核(Paimon Rust)完成,Doris 侧承担分布式调度与结果归并。

整桶下发与分桶并行的执行方式见图 4。

05\-fig5\-whole\-bucket\-pushdown\.png

图 4:每个扫描单元**是一个完整的桶,BE 在桶内完成索引召回、精确回退与删除掩码,跨桶 Top-K 由 Doris 归并。

2.3 Top-K 下推与 Index-Only Scan

标准的向量检索 SQL 按距离排序取前 K 条。Doris FE 为利用索引,新增了一条优化器规则,用于识别这个查询模式并改写执行计划:

  • 为扫描节点添加一个检索得分列。

  • 将原 SQL 中的距离表达式改成对这个得分的换算,不再重新计算距离。

  • 排序从按距离升序改成按得分降序。

  • 进一步优化:当 SELECT 列表中没有出现向量列本身时,则完全不读原始向量列,即 Index-Only Scan,省去向量列的读取与传输。

该查询模式的识别条件包括:

  • 距离函数与索引配置的度量方式一致。

  • 该列存在向量索引。

  • SQL 中不含聚合或 JOIN。

任一条不满足就退回到普通扫描,确保查询结果正确。

带过滤条件和分页的语句同样走这条规则。分区谓词在 FE 用于裁剪分区,只保留相关分区的桶;能转成 Paimon 谓词的条件下发到 Paimon Rust,在检索前收敛候选,即前置过滤(过滤列没有索引时需要扫描该列);用到 Doris 函数的谓词(如 length(a) > 1)无法下推,由 Doris 对检索结果做后置过滤。分页的 OFFSET 与 LIMIT 合并成一个更大的 K 下推,真正的偏移由上层排序算子处理。

用户使用与 Doris 内表向量检索相同的 SQL,不改语句即可用上 Paimon 表上的向量索引。

优化器规则的识别与改写见图 5。

06\-fig6\-topk\-pushdown\-index\-only\-scan\.png

图 5:满足三个条件**的向量 Top-K 语句被改写为按得分排序,不取向量列时不读向量列;任一条件不满足则退回普通扫描。

2.4 大 Top-K 的延迟物化:检索与物化分离

一个典型的 Top-K SQL 查询:

select id, embedding from vec_table
order by l2_distance_approximate(embedding, [...])
limit <K>;  

该 SQL 可以通过一次扫描获取 id 列和 embedding 后,在进行检索。K 较小时,这种执行方式开销不大。

但 K 很大时,一次扫描的代价随之上升:每个桶都要按 K 读出候选行的全部用户列,而这些行中绝大部分会在全局合并时被丢掉。K = 100 万、向量列 2048 维时,每个候选行携带 8 KB 的向量数据(2048 × 4 字节),被丢弃的候选行越多,无效读取越多。

快手当前的做法是把这类查询改写成自关联:先用只返回 id 的子查询完成召回,再关联原表取回命中行的向量,写法见第 3.2 节。关联时,候选 id 集合下推到 Paimon 读取层,读取器据此排除无关文件和行组,再用 Parquet 页级索引跳过无关页面,只物化命中行的向量列。第 3.3 节的数据中,Top-K = 100 万时只返回 id 为 7.9~8.0 秒,连向量一起返回为 65~76 秒,差距的主体是这 100 万行向量的读取与物化。虽然解决了问题,但是需要用户改写 SQL。

正在开发的下一步执行计划,是把检索和物化拆成三段:

  • 候选阶段:各 BE 在自己分到的桶上做检索,不读用户列,只产出候选集。

  • 合并阶段:收齐全部候选,做全局 Top-K 与精排,产出一份待物化的行清单(数据文件与文件内行区间)。清单按数据文件分组,并把命中行的行区间合并。

  • 物化阶段:按行清单只读命中行的用户列。

合并阶段收敛到一个节点是算法要求:精排要看到全局候选集才能算出正确的 Top-K,行清单的分组与区间合并也要看到全集才能做对。汇聚的候选集为 MB 级,全程没有 hash 或 range 重分布。

一次扫描与三段式的对比见图 6。

07\-fig7\-late\-materialization\.png

图 6:一次扫描让每个桶**按 K 读宽列;三段式先只产出候选,全局 Top-K 之后只物化命中行。

预期收益有三点:

  • Top-K 越大,被丢弃的候选行越多,省下的宽列读取越多。

  • 用户手写的自关联由物化阶段取代,从执行计划中消失。

  • 两种形态由 FE 按代价模型自动选择,用户不需要感知,也不需要改 SQL。

第 3 节用实测数据说明当前一次扫描形态的性能表现,三段式的进展见第 4 节。

3. 性能指标:快手生产环境的规模与成效

3.1 部署形态与测试口径

快手已在生产环境用 Doris 外表查询 Paimon 表上的向量索引。部署形态如下:

image\.png

测试集规模:

image\.png

3.2 两类查询

一类只返回业务标识,由向量索引直接给出 Top-K:

select id
from vec_table
order by l2_distance_approximate(embedding, [...]) /* 2048 维查询向量 */
limit <K>; -- K 取 100 / 1 万 / 100 万

该测试用例是典型的 Index-Only Scan 查询,主要反映召回本身的开销。

另一类把业务标识连同向量一起返回。当前形态下,这类查询由用户改写成自关联,先用子查询取出 Top-K 的 id,再关联原表取回命中行的向量:

select t1.id, embedding
from (select id, embedding from vec_table) t1
join (
select id
from vec_table
order by l2_distance_approximate(embedding, [...]) /* 2048 维查询向量 */
limit <K>
) t2 on t1.id = t2.id;

第 3.3 节表中“返回业务标识 + 向量”一列使用的就是这条自关联 SQL,主要反映向量列读取与物化的开销。第 2.4 节的三段式落地后,同样的查询可以直接写成:

select id, embedding
from vec_table
order by l2_distance_approximate(embedding, [...]) /* 2048 维查询向量 */
limit <K>; -- K 取 100 / 1 万 / 100 万

届时自关联由物化阶段取代,语句与查询 Doris 内表一致。

3.3 千万到亿级的实测结果

下表展示了不同参数组合下的查询性能和召回率。两列耗时都是端到端耗时,只返回业务标识的查询对应第 3.2 节第一条 SQL,连向量返回的查询对应自关联 SQL;表中为代表性单次查询结果:

image\.png

从表中可以得到两点结论:

  • 索引检索对数据规模不敏感。行数从 1600 万增至 1.28 亿,只返回业务标识的耗时在三档 Top-K 下分别多 0.4 秒、0.2 秒和 0.1 秒。

  • Top-K = 100 万时瓶颈在向量列的结果物化,不在检索。

上表为单次代表性查询,不覆盖 QPS 维度。

3.4 Paimon 表与 Doris 内表向量检索对比测试

快手还进行了 Paimon 表与 Doris 内表向量检索的对比测试,即把同一批 Paimon 数据导进 Doris 内表、用 Doris 自己的向量索引查询。下表展示了测试结果。

image\.png

  • 外表耗时为内表的 1.07~4.18 倍。只返回业务标识时,绝对差值仅为 0.7~1.3 秒。

  • Top-K = 100 万时差距收窄到 1.07~1.36 倍。此时耗时的主体是回表物化,两侧读取的数据量相同,缓存位置的影响被摊薄。

快手实测结果总览见图 7。

08\-fig8\-kuaishou\-results\-overview\.png

图 7:两个规模下 Recall* 为 96.0%~99.6%,只返回业务标识在 Top-K = 1 万以内不超过 2.0 秒;Top-K = 100 万时耗时的主体在物化。*

虽然内表性能更优,但从绝对值看,只返回业务标识时仅增加 0.7~1.3 秒的查询延迟,就能为快手换来如下收益:

  • 提升了数据从写入 Paimon 到可被检索之间的时效性。

  • 统一存储,避免数据多副本冗余。

  • 避免了数据在不同系统间传输转换的计算资源开销。

  • 省去两套系统各自的权限治理。

4. Doris 与 Paimon 社区共建成果与规划

这条链路是快手、Apache Doris 社区和 Apache Paimon 社区共同推进的结果。快手已在 Apache Paimon 和 Apache Paimon Rust 社区累计贡献 120 个以上 PR,Doris 侧的改动也会在未来两个月内合入 Doris 社区。

与此同时,快手也在推动如下工作,进一步提升 Doris on Paimon 的向量检索性能:

  • 三段式延迟物化:第 2.4 节描述的形态,把候选检索与向量列物化拆开,让 Top-K = 100 万这类工作点不再为最终会被丢弃的候选行读宽列,用户也不再需要手写自关联。

  • 索引的本地缓存:在调用进程内做本地磁盘与内存缓存,并由 Doris 配合做一致性哈希分发。

  • 全文检索与 Hybrid Search:全文检索路径已在 Paimon 侧完成。Doris 侧接入后,同一张湖表上可以组合结构化过滤、关键词匹配和语义召回。

  • 检索语义补齐:支持相似度范围检索(快手内部已经开始灰度,计划经过大规模生产验证后贡献回社区),并让检索参数能按查询覆盖,而不只是表级配置。


了解更多

目前,Paimon、Iceberg、Lance 等多模湖仓能力已合入Doris 4.2 发版分支,将随 4.2 版本于9 月底正式发布。

后续,我们还将围绕 Apache Doris 5.0 多模湖仓,针对 Iceberg、Paimon、Lance 等开放格式陆续推出系列文章,深入介绍相关能力的技术原理、性能测评与典型落地场景,请持续关注。

对于正在评估 Iceberg、Paimon、Lance 等开放数据格式,或有实时湖仓、多模数据分析等实际需求的用户,可联系我们。我们可以提供针对具体业务场景提供技术交流、方案评估与测试支持,帮助你更快完成验证和落地。