Lance Catalog
这是一个实验性功能。
Lance Catalog 自 Apache Doris 4.2 版本开始支持。
Lance 是面向分析和 AI 场景的列式数据格式。Doris 可以通过 Lance Catalog 发现 Lance Namespace 中的数据库和表,并直接查询存储在本地文件系统或 S3 兼容对象存储中的 Lance 数据集。
当前 Doris 对 Lance 提供只读能力,不支持创建、写入、更新或删除 Lance 表。
功能概览
| 功能 | 支持情况 |
|---|---|
| Filesystem Catalog | 支持本地文件系统、file:// 和 s3:// Warehouse |
| REST Catalog | 支持 Lance REST Namespace,以及无认证、Bearer Token、API Key 和自定义 HTTP Header |
| 元数据访问 | 支持 SHOW DATABASES、SHOW TABLES 和 DESC |
| 数据查询 | 支持列裁剪、并行扫描 Lance Fragment 和当前版本的快照一致性读取 |
| 谓词下推 | 支持将部分标量谓词下推到 Lance 执行 |
| 文件 TVF | 支持通过 s3() 和 local() 直接查询 Lance 数据集 |
| 向量检索 | 使用物理 Lance Index Segment 作为并行 Split,对未覆盖的 Fragment 保留 Flat Search Split,并由 Doris 合并全局 Top-K |
| 写入 Lance | 暂不支持 |
| Time Travel | 暂不支持 |
| Full-Text Search / Hybrid Search | 暂不支持 |
Lance 版本与兼容性
Doris BE 数据读取器使用 lance-c v0.1.6 构建。该版本在 Doris 中绑定的 Lance 源码版本为 9.1.0-beta.3(Lance commit e934cc2c)。lance-c 和 Lance Rust crates 的版本表示 Doris 集成的读取器实现版本,与数据集中记录的 Lance data_storage_version 不是同一个概念。
当前读取器的文件格式兼容情况如下:
data_storage_version | 读取支持 | 说明 |
|---|---|---|
0.1 / legacy | 支持 | Lance 的初始文件格式。 |
2.0(写入选项别名 0.3) | 支持 | Lance v2 文件格式的早期版本。 |
2.1 / stable | 支持,默认稳定格式 | 当前内置 Lance 的 stable 写入选项和新数据集默认格式均解析为 2.1。 |
2.2 | 支持 | 当前内置 Lance 将其视为稳定格式,但它不是默认写入格式。 |
2.3 / next | 实验性,不保证兼容 | 当前内置 Lance 将 2.3 标记为不稳定格式,next 写入选项解析为 2.3。 |
| 后续或未知版本 | 不支持 | 打开或扫描数据集时可能返回不支持存储版本的错误。 |
Lance SDK 发行版本号和文件格式版本相互独立。无论数据集由更早还是更新的 Lance SDK 写入,只有当其存储格式、必需的表级 Feature Flag、索引格式以及 Arrow/Lance 数据类型均可被 Doris 内置版本识别时,Doris 才能读取。因此:
- Doris 预期能够读取使用
0.1、2.0、2.1和2.2存储格式写入的数据集,同时还需满足下文所述的数据类型限制。 - 不保证向前兼容。更新的 Lance 版本写入或修改数据集后,如果使用了更新的存储格式、未知的必需 Manifest Feature、新索引格式或不支持的 Extension 类型,Doris 可能无法读取。
- 对于必须由当前 Doris 版本持续读取的数据集,建议使用当前默认稳定格式
2.1,且不要使用next。引入更新的写入器或可选 Lance 功能后,应先使用目标 Doris 版本验证生成的数据集,再用于生产环境。
配置 Catalog
语法
CREATE CATALOG [IF NOT EXISTS] catalog_name PROPERTIES (
"type" = "lance",
"lance.catalog.type" = "<filesystem|rest>",
{CatalogProperties},
{StorageProperties},
{CommonProperties}
);
通用属性
| 属性 | 是否必需 | 默认值 | 说明 |
|---|---|---|---|
type | 是 | - | 固定为 lance。 |
lance.catalog.type | 否 | filesystem | Catalog 类型,可选值为 filesystem 或 rest。 |
lance.namespace.parent | 否 | 空 | 仅访问指定 Lance Namespace 及其子 Namespace。例如默认分隔符下,production$analytics 表示两级 Namespace。 |
lance.namespace.delimiter | 否 | $ | 用于解析 lance.namespace.parent,同时会传递给 REST Namespace 客户端。该配置不改变 Doris 中多级 Namespace 的展示方式。 |
lance.namespace.root_database | 否 | default | Lance 根 Namespace 在 Doris 中映射的数据库名。 |
Filesystem Catalog
Filesystem Catalog 直接从 Warehouse 目录发现 Lance Namespace 和表。
| 属性 | 是否必需 | 说明 |
|---|---|---|
warehouse | 是 | Lance Warehouse 根路径。支持本地绝对路径、file:// URI 和 s3:// URI。 |
使用 S3 兼容对象存储
下面以 MinIO 为例创建 Catalog:
CREATE CATALOG lance_catalog PROPERTIES (
"type" = "lance",
"lance.catalog.type" = "filesystem",
"warehouse" = "s3://my-bucket/lance",
"s3.endpoint" = "http://127.0.0.1:9000",
"s3.access_key" = "admin",
"s3.secret_key" = "password",
"s3.region" = "us-east-1",
"use_path_style" = "true"
);
访问 AWS S3 时,可以省略 s3.endpoint,并按实际环境配置访问密钥、Region 和 Path Style。
使用本地文件系统
CREATE CATALOG lance_local PROPERTIES (
"type" = "lance",
"lance.catalog.type" = "filesystem",
"warehouse" = "/data/lance"
);
使用本地文件系统时,warehouse 必须是绝对路径。FE 需要通过该路径读取 Namespace 和表元数据,执行查询的 BE 也需要能够通过相同路径访问数据。因此在多节点环境中,应将相同的共享目录挂载到所有相关 FE 和 BE 节点。
REST Catalog
REST Catalog 通过 Lance REST Namespace 获取 Namespace、表地址和存储访问参数。REST Catalog 不需要、也不允许设置 warehouse。
| 属性 | 是否必需 | 默认值 | 说明 |
|---|---|---|---|
lance.rest.uri | 是 | - | REST 服务地址,必须使用 http:// 或 https://。 |
lance.rest.security.type | 否 | none | 认证方式,可选值为 none、bearer 或 api_key。 |
lance.rest.bearer-token | 使用 Bearer 认证时是 | - | Bearer Token。 |
lance.rest.api-key | 使用 API Key 认证时是 | - | API Key,通过 x-api-key Header 发送。 |
lance.rest.header.<header-name> | 否 | - | 发送给 REST 服务的自定义 HTTP Header。认证 Header 应使用上面的专用认证属性配置。 |
使用 Bearer Token 创建 REST Catalog:
CREATE CATALOG lance_rest PROPERTIES (
"type" = "lance",
"lance.catalog.type" = "rest",
"lance.rest.uri" = "https://lance.example.com",
"lance.rest.security.type" = "bearer",
"lance.rest.bearer-token" = "your-token"
);
使用 API Key 时,将认证配置替换为:
"lance.rest.security.type" = "api_key",
"lance.rest.api-key" = "your-api-key"
如果 REST 服务返回临时存储凭证,Doris 会使用这些凭证访问对应的 Lance 表。也可以在 Catalog 中配置 s3.endpoint、s3.access_key、s3.secret_key、s3.region 和 use_path_style,作为默认的对象存储访问参数。
当前 BE Reader 不支持由 REST Namespace 管理版本的 Lance 表(Managed Versioning)。
Namespace 映射
Lance 支持多级 Namespace,而 Doris Catalog 使用数据库名承载 Namespace:
| Lance Namespace | Doris 数据库名 |
|---|---|
| 根 Namespace | default,可通过 lance.namespace.root_database 修改 |
doris | doris |
doris.analytics | doris.analytics |
多级 Namespace 在 Doris 中使用 . 连接为一个数据库名。引用包含 . 的数据库名时,需要使用反引号:
SHOW TABLES FROM lance_catalog.`doris.analytics`;
SELECT *
FROM lance_catalog.`doris.analytics`.user_features;
lance.namespace.parent 可用于限定 Catalog 可见的 Namespace 子树。例如:
CREATE CATALOG lance_analytics PROPERTIES (
"type" = "lance",
"lance.catalog.type" = "filesystem",
"warehouse" = "s3://my-bucket/lance",
"lance.namespace.parent" = "production$analytics",
"s3.region" = "us-east-1"
);
此时 Doris 只展示 production.analytics 之下的表和子 Namespace。
查询 Lance 表
创建 Catalog 后,可以像查询普通外表一样浏览并查询 Lance 表:
SHOW DATABASES FROM lance_catalog;
SHOW TABLES FROM lance_catalog.default;
DESC lance_catalog.default.user_profiles;
SELECT user_id, name, age
FROM lance_catalog.default.user_profiles
WHERE age >= 18
ORDER BY user_id
LIMIT 100;
也可以将 Lance 数据写入 Doris 内表:
INSERT INTO internal.demo.user_profiles
SELECT user_id, name, age
FROM lance_catalog.default.user_profiles;
普通 Catalog 查询会在规划阶段固定一个 Lance 数据集版本,并按 Fragment 生成扫描任务。因此,同一条查询读取一致的快照,同时可以由多个 Scanner 并行扫描不同 Fragment,不会让每个 Scanner 重复扫描整个数据集。
类型映射
| Lance / Arrow 类型 | Doris 类型 | 说明 |
|---|---|---|
bool | BOOLEAN | |
int8 | TINYINT | |
uint8 | SMALLINT | 无符号整数无损提升 |
int16 | SMALLINT | |
uint16 | INT | 无符号整数无损提升 |
int32 | INT | |
uint32 | BIGINT | 无符号整数无损提升 |
int64 | BIGINT | |
uint64 | LARGEINT | 无符号整数无损提升 |
float16 | FLOAT | 提升为 32 位浮点数 |
float32 | FLOAT | |
float64 | DOUBLE | |
decimal128(P,S) | DECIMAL(P,S) | 最大精度为 38 |
decimal256(P,S) | DECIMAL(P,S) | 最大精度为 76 |
utf8、large_utf8 | TEXT | |
binary、large_binary | VARBINARY(2147483647) | |
fixed_size_binary(N) | VARBINARY(N) | 保留固定字节宽度 |
date32(day)、date64(ms) | DATE | date64 应表示完整自然日 |
time32(s) | TIME(0) | |
time32(ms) | TIME(3) | |
time64(us)、time64(ns) | TIME(6) | 纳秒精度截断为微秒 |
无时区 timestamp(s) | DATETIME | 不随 Session Time Zone 转换 |
无时区 timestamp(ms) | DATETIME(3) | 不随 Session Time Zone 转换 |
无时区 timestamp(us)、timestamp(ns) | DATETIME(6) | 纳秒精度截断为微秒 |
带时区 timestamp | TIMESTAMPTZ(0-6) | 保存时间点,按 Doris Session Time Zone 展示 |
struct | STRUCT | 子字段递归映射 |
list、large_list、fixed_size_list | ARRAY | 元素类型递归映射 |
map | MAP | Key 和 Value 类型递归映射 |
当前不支持以下类型:
- Arrow
null和duration。 - 带有
ARROW:extension:name元数据的 Arrow/Lance Extension 类型,例如 Lance Blob v2、Arrow JSON Extension 和 Lance BFloat16 Extension。 - 无法递归映射其子类型的复杂类型。
- 保留了 Dictionary 标记的 Arrow Dictionary 类型。
对于不支持的顶层列,Catalog 表的 DESC 和 Lance 文件 TVF 的 DESC FUNCTION 都会保留该列,并显示 unknown type: UNSUPPORTED_TYPE。如果复杂类型的任一子字段无法映射,则整个顶层复杂列会标记为不支持。查询只投影支持的列仍可正常执行;当 SQL 投影不支持的列时,Doris 会在分析阶段报错。例如:
SELECT * EXCEPT(blob_col, json_col)
FROM lance_catalog.default.all_types;
部分 Lance Java SDK 版本可能在读取 Schema 时丢失 Dictionary 标记,并将 Dictionary 列暴露为物理索引类型。该结果不代表 Doris 已支持 Dictionary 的逻辑值,请勿依赖这一行为。
谓词下推
Doris 会将语义兼容的谓词转换为 Substrait 表达式,并交给 Lance 在读取阶段执行。对于已完整下推的条件,Doris BE 不再重复计算该条件;不能安全下推的条件仍由 Doris 执行。
支持下推的数据类型
| Lance / Arrow 类型 | 下推范围 |
|---|---|
bool | 等值、空值和逻辑运算,不包含大小比较 |
int8/16/32/64 | 支持 |
uint8/16/32/64 | 支持 |
float32/64 | 支持 |
decimal128 | 精度 1-38,Scale 范围为 0 到 Precision |
utf8、large_utf8 | 支持 |
date32(day) | 支持 |
无时区 timestamp(s/ms/us) | 支持 |
其他可读取类型,例如 float16、decimal256、Binary、date64、Time、纳秒 Timestamp、带时区 Timestamp 和复杂类型,当前保留为 Doris 侧谓词。
支持下推的操作符
| SQL 谓词 | 下推条件 |
|---|---|
=、!=、<>、<、<=、>、>= | 直接的列与常量比较;支持将常量写在左侧 |
<=> | 直接的列与常量 Null-safe 等值比较;在 NOT、AND 或 OR 中仍保持非 NULL 的二值逻辑结果 |
IN、NOT IN | 非空常量列表,列表中不能包含 NULL |
IS NULL、IS NOT NULL | 直接引用列 |
AND | 顶层 Conjunct 可以分别下推,不能下推的部分保留在 Doris |
OR | 两个分支都能完整转换时下推 |
NOT | 操作数能完整转换时下推 |
以下形式通常不会下推:
- 列上包含函数或算术表达式。
IN列表为空或包含NULL。OR或NOT中只有部分表达式可转换。- 数据类型或常量值无法无损转换到 Lance。
可以通过 EXPLAIN 中的 lancePushdownPredicate 查看实际下推的条件:
EXPLAIN
SELECT user_id
FROM lance_catalog.default.user_profiles
WHERE age >= 18 AND country IN ('CN', 'US');
使用文件 TVF 查询 Lance
如果只需要读取一个已知路径的 Lance 数据集,可以不创建 Catalog,直接使用 s3() 或 local() TVF。uri 或 file_path 必须指向 Lance 数据集的根目录,而不是其内部的数据文件。
S3 TVF
SELECT user_id, name
FROM s3(
"uri" = "s3://my-bucket/lance/user_profiles.lance",
"s3.endpoint" = "http://127.0.0.1:9000",
"s3.access_key" = "admin",
"s3.secret_key" = "password",
"s3.region" = "us-east-1",
"use_path_style" = "true",
"format" = "lance"
)
WHERE user_id > 100;
S3 TVF 在 FE 获取 Schema、当前版本和 Fragment 列表,并固定该版本后按 Fragment 并行扫描。
Local TVF
SELECT user_id, name
FROM local(
"file_path" = "/data/lance/user_profiles.lance",
"backend_id" = "10001",
"format" = "lance"
);
file_path 会按用户填写的值直接传给目标 BE 上的 Lance Reader。Doris 不会自动拼接 user_files_secure_path,也不会对该路径执行 Glob 展开,因此该参数必须直接指向目标 BE 可访问的单个 Lance 数据集根目录;建议使用绝对路径。
Local TVF 的 Schema 发现和执行扫描会分别打开数据集的最新版本,当前不会将 Schema 发现时解析出的版本固定到后续扫描。如果数据集在查询分析与执行之间发生更新,Schema 和实际扫描的快照可能不一致。因此,应避免在 Local TVF 查询的分析和执行期间修改数据集。当前 Local Lance TVF 使用单个 Scanner。
Lance 文件 TVF 还有以下限制:
- 仅支持
s3()和local(),暂不支持 HDFS、HTTP 等其他文件 TVF。 - 不支持
path_partition_keys。 - 一个 TVF 路径只能表示一个 Lance 数据集。
DESC FUNCTION可以展示包含不支持类型的 Schema,但 SQL 不能投影不支持的列。
向量检索
vector_search() 是一个关系型 TVF,用于对 Lance 表的向量列执行 Top-K 检索。它既可以使用 Lance 中已建立的向量索引,也可以执行 Flat Search。
语法和示例
SELECT user_id, label, _distance
FROM vector_search(
"table" = "lance_catalog.default.items",
"column" = "embedding",
"query_vector" = "[0.1, 0.2, 0.3, 0.4]",
"top_k" = "10",
"offset" = "3",
"metric" = "l2",
"nprobes" = "20",
"refine_factor" = "10",
"filter" = "category = 'book'",
"use_index" = "true"
)
ORDER BY _distance ASC, user_id;
vector_search() 的关系 Schema 包含 Lance 源表的所有列,以及 Lance Scanner 为最近邻查询生成的 _distance 列;最终 SQL 结果只包含 SELECT 投影的列。Doris 将 _distance 作为 FLOAT 提供给用户。它表示距离而不是通用的相似度分数,值越小表示两个向量越接近。源表不能已经包含名为 _distance 的列。SQL 关系本身不保证最终展示顺序,因此需要稳定的最近邻顺序时,应显式使用 ORDER BY _distance ASC,并建议增加唯一列作为距离相同情况下的 Tie-breaker。
table 必须解析为恰好三部分的 catalog.database.table 名称。多级 Lance Namespace 在 Doris 中映射为包含 . 的单个数据库名,因此必须使用反引号将数据库部分括起来。例如,表 items 位于 doris.analytics Namespace 时,应写为:
"table" = "lance_catalog.`doris.analytics`.items"
不要写成未引用的 lance_catalog.doris.analytics.items,因为它会被解析为四部分名称并报错。只填写表名或 database.table 也会报错。
参数
| 参数 | 是否必需 | 默认值 | 说明 |
|---|---|---|---|
table | 是 | - | 完整的三部分 catalog.database.table 名称。多级 Namespace 对应的数据库名包含 . 时,必须使用反引号引用数据库部分。该表必须属于 Lance Catalog,用户需要拥有该表的 SELECT 权限。 |
column | 是 | - | 向量列名。当前支持 fixed_size_list<float16|float32|float64|uint8|int8>。 |
query_vector | 是 | - | JSON 数字数组。维度必须与向量列一致,元素值必须能由向量元素类型表示。 |
top_k | 否 | 10 | 跳过 offset 后返回的结果数,必须为正整数。 |
offset | 否 | 0 | 在向量检索内部跳过的最近邻数量,必须为非负整数。top_k + offset 不能超过无符号 32 位整数上限。 |
metric | 否 | 匹配索引的 Metric;无索引时 uint8 为 hamming,其他支持类型为 l2 | 距离类型:l2、cosine、dot 或 hamming。dot_product 是 dot 的别名。uint8 向量仅支持 hamming;其他当前支持的向量元素类型支持 l2、cosine 和 dot。 |
filter | 否 | - | Lance SQL 条件,在生成候选向量之前执行,即 Prefilter。 |
nprobes | 否 | 最少 1,不限制最大值 | IVF 索引探测的分区数量,必须为正整数。不设置时从 1 个分区开始;使用 Prefilter 且候选不足时,Lance 可以继续探测更多分区。显式设置为 N 时,最少和最多探测数都会固定为 N。 |
refine_factor | 否 | 不启用精排 | 候选集精排倍数,必须为正整数。不设置时不基于原始向量重新计算距离,量化索引返回的 _distance 可能是近似距离;设置为 N 后,Lance 先获取 (top_k + offset) × N 个候选,再用原始向量计算真实距离并重新排序。即使设置为 1 也会执行精排,因此与不设置不同。 |
ef | 否 | floor(1.5 × (top_k + offset)) | HNSW 图索引搜索时保留的候选宽度,必须为正整数。如果同时设置了 refine_factor,默认值为 floor(1.5 × (top_k + offset) × refine_factor)。对非 HNSW 索引无效。 |
use_index | 否 | true | true 表示将兼容的物理 Lance Index Segment 规划为索引 Split,并将未覆盖的 Fragment 保留为 Flat Search Split;如果没有可用的兼容索引元数据,则退回按 Fragment 拆分。false 表示每个可见 Fragment 生成一个 Split,并强制执行 Flat Search。 |
以上默认值对应 Doris 当前集成的 Lance Scanner 行为。metric 未指定时,如果向量列存在兼容索引,查询使用该索引创建时配置的 Metric;不存在兼容索引或 "use_index" = "false" 时,uint8 向量使用 hamming,其他当前支持的向量元素类型使用 l2。
支持的向量索引类型
当前内置 lance-c v0.1.6 明确支持以下 Lance 向量索引组合:
| 索引类型 | 说明 | 主要查询参数 |
|---|---|---|
IVF_FLAT | IVF 分区,分区内使用原始向量计算距离 | nprobes |
IVF_SQ | IVF 与 Scalar Quantization | nprobes、refine_factor |
IVF_PQ | IVF 与 Product Quantization | nprobes、refine_factor |
IVF_HNSW_FLAT | IVF 与 HNSW,图节点保存原始向量 | nprobes、ef |
IVF_HNSW_SQ | IVF、HNSW 与 Scalar Quantization | nprobes、ef、refine_factor |
IVF_HNSW_PQ | IVF、HNSW 与 Product Quantization | nprobes、ef、refine_factor |
vector_search() 只负责查询,不负责在 Doris 中创建索引,也不提供指定索引类型或索引名称的参数。当 use_index=true 时,FE 从固定的数据集快照读取向量索引元数据,并选择与向量列和 Metric 兼容的一个逻辑索引;随后将该逻辑索引中仍覆盖可见数据的每个物理 Segment 分配给一个索引 Scan Split。每个索引 Split 都携带 Segment UUID 以及该 Segment 覆盖且在当前快照中可见的 Fragment,因此 BE 会检索指定的物理 Segment,而不是再次让 Lance 自行选择索引。
一个 Lance 逻辑索引可以包含多个物理 Index Segment,一个物理 Segment 也可以覆盖多个 Fragment。未被所选索引覆盖的 Fragment 不会被遗漏:Doris 会为每个这样的 Fragment 增加一个执行 Flat Search 的回退 Split。如果 FE 无法生成可用的 Index Segment 计划,则退回按 Fragment 拆分。当 use_index=false 时,Doris 跳过索引元数据规划,并对每个可见 Fragment 强制执行 Flat Search。Flat Search 不是一种 ANN 索引,它需要在 Lance 内直接读取并比较向量。
Prefilter 和 Post-filter
TVF 的 filter 参数是 Prefilter。Doris 将该字符串传给每个搜索 Split 的 Lance Scanner,Lance 在 ANN 或 Flat Search 生成候选之前执行过滤:
SELECT user_id, category, _distance
FROM vector_search(
"table" = "lance_catalog.default.items",
"column" = "embedding",
"query_vector" = "[0.1, 0.2, 0.3, 0.4]",
"top_k" = "10",
"filter" = "category = 'book'"
)
ORDER BY _distance ASC, user_id;
filter 中引用的列由 Lance 内部读取并计算;如果该列没有被 SELECT 或其他 Doris 表达式引用,它不需要作为列返回给 Doris。
外层 WHERE 是 Post-filter。优化器会将它下移到 Doris 的 Lance Scan 中,但不会把它转换成 Lance 的 Prefilter。它的执行位置是:Lance 为每个搜索 Split 生成候选之后、Doris 执行局部和全局 TopN 之前。
SELECT user_id, category, _distance
FROM vector_search(
"table" = "lance_catalog.default.items",
"column" = "embedding",
"query_vector" = "[0.1, 0.2, 0.3, 0.4]",
"top_k" = "10"
)
WHERE category = 'book'
ORDER BY _distance ASC, user_id;
因此,外层 WHERE 只过滤已经生成的候选,不会触发 Lance 补充候选,最终结果可能少于 top_k。如果过滤条件应该缩小向量候选的搜索空间并保证在过滤后的数据中选择最近邻,应使用 TVF 的 filter 参数。
当前执行方式
vector_search() 使用分布式候选搜索,而不是由一个 Scanner 扫描整个数据集。Split 的边界取决于索引覆盖范围:
- FE 在规划阶段固定一个正数版本的 Lance 数据集快照,并读取该快照中可见的 Fragment;当
use_index=true时,还会读取向量索引元数据。 - 如果存在具有可用 Segment 覆盖信息的兼容逻辑向量索引,每个仍覆盖可见 Fragment 的物理 Index Segment 都会生成一个索引 Scan Split。该 Split 包含 Segment UUID,以及其 Fragment Bitmap 与固定快照中可见 Fragment 的交集,因此一个 Split 可以包含多个 Fragment ID。
- 没有被这些索引 Split 覆盖的每个可见 Fragment,都会生成一个独立的回退 Fragment Split。这样,即使数据是在索引创建后追加的、尚未执行索引优化,也仍然可以被检索。如果不存在可用的 Index Segment 计划,所有可见 Fragment 都按 Fragment 拆分;当
use_index=false时,所有可见 Fragment 直接使用 Flat Search Split。 - 假设查询参数为
top_k=K、offset=n,每个索引或回退 Split 都请求最多K+n个候选,并且不在 Split 内应用 offset。索引 Split 只检索为其分配的物理 Index Segment,回退 Fragment Split 对自身 Fragment 执行 Flat Search。TVF 的filter在候选生成前由 Lance 执行,外层WHERE则在候选生成后由 Doris Scan 执行。 - Doris 对所有 Split 返回的候选执行局部 TopN、Exchange 和全局 TopN,按
_distance ASC合并;只有全局 TopN 应用offset=n,跳过前n行后返回K行。
因此,Split 级候选集只用于向全局合并提供候选,不能直接视为最终结果。索引 Segment Split、回退 Fragment Split 以及后续按 Row ID 取列都使用同一个固定快照。刷新索引覆盖会改变新追加 Fragment 的检索方式,但未被索引覆盖的 Fragment 仍会通过 Flat Search 进入检索范围。
执行顺序可以概括为:
固定数据集快照
-> FE Split 规划
-> 索引覆盖:每个物理 Index Segment 一个 Split -> ANN Search
-> 未覆盖或无索引数据:每个 Fragment 一个 Split -> Flat Search
-> 每个 Split:Lance Prefilter -> ANN/Flat Search -> 最多 K+n 个候选
-> Doris Scan Post-filter
-> Doris 局部 TopN
-> Exchange
-> Doris 全局 TopN(应用 offset=n 和 limit=K)
-> 可选的延迟物化 Fetch
TopN 两阶段读取和延迟物化
当 experimental_topn_lazy_materialization_threshold 大于 0、top_k 不超过该阈值,并且存在可以延迟读取的顶层列时,vector_search() 可以使用两阶段读取。默认阈值为 1024。第一阶段只传递完成候选过滤和 TopN 所必需的列以及内部 Row Location;全局 TopN 完成后,第二阶段只为最终保留的行读取其他输出列。
例如,源表包含以下列:
| 列 | 用途 |
|---|---|
user_id | 最终输出列 |
category | 外层 WHERE 的 Post-filter 列 |
title、payload | 最终输出列 |
embedding | Lance 向量搜索列 |
执行以下查询,其中 K=10、n=3:
SET experimental_topn_lazy_materialization_threshold = 1024;
SELECT user_id, title, payload, _distance
FROM vector_search(
"table" = "lance_catalog.default.items",
"column" = "embedding",
"query_vector" = "[0.1, 0.2, 0.3, 0.4]",
"top_k" = "10",
"offset" = "3"
)
WHERE category = 'book';
典型的两阶段列流转如下:
| 阶段或算子 | 读取或输出的列 | 说明 |
|---|---|---|
| Lance Split Search | 内部使用 embedding;向 Doris 返回 _distance、category 和内部 Lance Row ID | embedding 用于 ANN/Flat Search,但没有被 SQL 投影时不作为结果列返回。每个 Index Segment 或回退 Fragment Split 最多产生 K+n 个候选。 |
| Doris Scan Post-filter | _distance、category、内部 Row Location | 执行 category = 'book'。外层 WHERE 的列必须留在第一阶段。Doris 将 Lance Row ID 和数据集映射编码为内部 Row Location,Fetch 再通过该映射解析到同一个固定快照。 |
| 局部和全局 TopN | 第一阶段必需列和内部 Row Location | 全局 TopN 按 _distance 合并,应用 offset=n 和 limit=K。 |
| Row ID Fetch | 使用内部 Row Location 读取 user_id、title、payload | 对全局 TopN 保留的行,在同一个 Lance 数据集快照上调用 Row-ID 随机读取,不重新扫描 Fragment。 |
| 最终 Materialize | user_id、title、payload、_distance | 将延迟列与第一阶段保留的列合并,形成 SQL 最终输出。 |
第一阶段的必需列不只包括 _distance 和 Post-filter 列。凡是在全局 TopN 完成前被 Doris 表达式或算子引用的列,都属于第一阶段列。例如,如果查询增加 ORDER BY _distance, user_id,user_id 也需要提前读取,不能再等到第二阶段 Fetch。嵌套子列投影当前也不会延迟到 Row-ID Fetch。相反,只被最终投影使用的顶层列可以在第二阶段读取。
TVF filter 中引用的 Prefilter 列与外层 WHERE 列不同:前者由 Lance 在搜索内部使用,并不因为出现在 filter 字符串中就必须返回到 Doris;后者由 Doris Scan 执行,所以必须进入第一阶段。
将 experimental_topn_lazy_materialization_threshold 设置为 -1 会关闭两阶段读取。如果 top_k 大于阈值,或者没有可延迟的列,也会使用单阶段读取。单阶段模式会在 Scan 阶段返回查询所需的全部输出列,但向量搜索仍然按 Index Segment 或回退 Fragment Split 并行生成候选,并由 Doris 合并全局 TopN;它不会因此退化为 Doris 对整张表做普通全列扫描。索引 Split 使用为其分配的物理 Index Segment,强制或回退到 Flat Search 时才直接比较向量。
当前限制和建议
- Lance Catalog 和 Lance TVF 当前仅支持读取,不支持
CREATE TABLE、INSERT、UPDATE、DELETE、TRUNCATE TABLE或写回 Lance。 - 查询总是读取规划时选择的当前版本,不支持通过 SQL 指定 Version 或执行 Time Travel。
- 使用不支持的列类型时,建议显式列出需要读取的列,避免
SELECT *投影到不支持的列。 - 对普通扫描使用
EXPLAIN检查lancePushdownPredicate,确认目标条件是否已下推。 - 向量检索前应在 Lance 中创建与查询方式匹配的索引;小数据集或验证场景可以设置
"use_index" = "false"使用 Flat Search。 - 向量查询需要稳定顺序时,显式使用
ORDER BY _distance ASC并增加唯一 Tie-breaker。 - 需要在向量候选生成前过滤时使用
vector_search()的filter;外层WHERE只过滤每个搜索 Split 已生成的候选,并在 Doris 全局 TopN 之前执行,应允许其最终结果少于top_k。 - 使用
EXPLAIN检查lanceSearchFragments和lanceSearchIndexSegments。前者表示固定快照中的可见 Fragment 数量,后者表示 FE 选择的物理 Index Segment Split 数量;此外还可能存在回退 Fragment Split。