土法炼钢兴趣小组的算法知识备份

【向量检索引擎】Lance / LanceDB 对照:格式还是服务

文章导航

分类入口
databasestorage
标签入口
#lance#lancedb#parquet#lakehouse#vector-format#milvus#manifest#fragment#vector-engine

目录

lakehouse/21 已用本机实测说明:同一批 768 维向量,Parquet 随机取 100 行与全扫同量级,Lance take 低三个数量级。该篇把「专用向量引擎」留给本系列。本文回答两个更具体的问题:Lance 的表格式规范里,一条写入具体落在哪个文件、由谁记录版本;以及湖侧 snapshot 与 Milvus 在线引擎的可见性水位如何对齐,而不是停在「Lance 是格式、Milvus 是引擎」这句定性描述上。

本文是「向量检索引擎」系列第 16 篇(共 19 篇)。→ 系列目录

版本锚定:湖侧数字引用 lakehouse/21(PyArrow 24 / Lance 7.0.0 本机中位数);Lance 表格式与索引机制引用官方 Table FormatLayoutVector Indexes(A 级)。不在此重跑实验,除非主版本跨越需更新 lakehouse/21。


一、三层不要混叫「向量库」

代表 解决什么
列式/向量友好格式 Lance 随机行寻址、可选 ANN 索引随数据集、版本
嵌入/本地库 LanceDB 等 在格式之上提供检索 API,常同进程或轻量服务
专用分布式引擎 Milvus(本系列)、Qdrant(第 15 篇) 持续写入、一致性、多租户、集群生命周期

Lance 首先是 存储格式(可对标 Parquet 的定位差异),不是「另一个 Milvus」。LanceDB 把检索 API 叠在格式上,仍通常假设 数据以数据集文件为中心,而不是 Proxy–Worker–对象段状态机。


二、最小故事:同一批 embedding,写入两套系统分别经过什么

设想同一批 10 万条 768 维向量,一份写进 Lance 数据集,一份写进 Milvus collection。

写进 Lance:调用方把这批向量组织成 Arrow Table,lance.write_dataset 或增量 insert 会把数据切成一个或多个新的 fragment(每个 fragment 是若干列式数据文件,外加可选的删除文件);写完后生成一个新的 manifest,其中记录完整 schema、这个版本包含的全部 fragment 列表、一个单调递增的版本号,以及可选的索引元数据引用。manifest 文件落在数据集根目录的 _versions/ 下,用支持原子提交的命名方案(例如反转版本号的 V2 命名,便于在对象存储上 \(O(1)\) 找到最新版本而不用 LIST 全部历史版本)。这一步一旦提交成功,这批向量就是数据集某个具体版本的一部分,可以立即读到。

写进 Milvus:同一批向量经 Proxy 拆包,进 Streaming Node 的 WAL,提交后应用到 Growing Segment——这一步之后即可被增量查询路径搜到(第 3、5 篇),但要等 flush 才落对象存储变成 Sealed,再要等 Data Node 建索引、Coordinator handoff,才进入「有索引、被 Query Node 服务」的状态。

两条路径的关键差别不是「哪个更快」,而是可见性单元不同:Lance 的「可见」以数据集版本(manifest)为粒度——一次写入提交后,整个数据集原子性地进入新版本,读者可以选择读最新版本或任意历史版本;Milvus 的「可见」以一致性级别裁剪出的水位为粒度(第 12 篇),同一时刻不同客户端按不同一致性级别可能看到不同的「已提交」集合,且「能搜到」与「索引已就位」是两次独立交付(第 10 篇)。Lance 没有类似 Growing/Sealed 那种「先能搜、后有索引」的中间态——一个 fragment 一旦提交进某个 manifest 版本,它对该版本的读者就是完整可见的普通数据;只是这批新数据默认还没有被纳入已训练好的 ANN 索引(下一节展开),走 ANN 搜索时可能需要一次 optimize 或重建索引才被覆盖,纯 scan/filter 访问则不受影响。


三、Lance 表格式:fragment、manifest、version

官方 Table Format 规范把数据集定义为 fragment、数据文件、删除文件与索引的版本化集合,每个版本由一份不可变的 manifest 描述:

概念 官方定义要点
Manifest 描述数据集的一个版本:完整 schema(含嵌套字段)、该版本包含的 fragment 列表、单调递增的版本号、可选的索引元数据引用
Fragment 数据集的一次水平分区:一个或多个存列数据的文件,加一个可选的删除文件;fragment id 在数据集内唯一,按当前最大 fragment id 递增分配
Deletion file 记录该 fragment 内被删除的行位置;每个 fragment 每个版本最多一个删除文件
Index 与某个版本关联的索引元数据,实际索引内容存在 _indices/{UUID}/

数据集根目录的物理布局(官方 Layout)把这些概念对应到具体路径:

{dataset_root}/
  data/          -- *.lance 数据文件,存列数据
  _versions/     -- *.manifest,每个版本一份;命名方案支持 O(1) 定位最新版本
  _transactions/ -- *.txn,提交协调用的事务文件
  _deletions/    -- *.arrow 或 *.bin,删除向量文件
  _indices/      -- {UUID}/,每种索引类型的内容各自组织
  _refs/         -- tags/、branches/,命名版本与分支元数据

3.1 删除文件的两种格式与「软删的代价」

Table Format 规范明确删除文件有两种存储格式:Arrow IPC.arrow,存一个稀疏的行偏移数组,适合稀疏删除)与 Roaring Bitmap.bin,压缩位图,适合密集删除)。读者在扫描该 fragment 时需要过滤掉出现在删除文件里的行偏移。

规范同时点明一个关键代价:删除可以通过重写数据文件、物理移除已删行来「物化」,但这会使行地址失效,并要求重建索引,代价可能很高。这与 Milvus 的软删机制(第 8、13 篇)是同一类工程权衡——先用一个轻量的可见性标记(Roaring Bitmap 或 bitset)把删除变成「查询时跳过」,而不是立刻重写底层结构;只有在后台整理(Lance 的 optimize/重写,Milvus 的 compaction)时才真正回收空间,并且这一步都要付出重建索引的代价。两边的差异在于触发时机与调用方——Lance 的物化通常是用户显式调用维护操作,Milvus 的 compaction 由 Coordinator 按策略自动调度(第 10 篇)。

3.2 版本、tag 与分支

_refs/ 下的 tags/branches/ 让 Lance 数据集支持给某个版本号打一个人类可读的名字,或者从某个版本拉出一条独立的写入分支——这类似给一个不可变版本号附加语义标签,方便后续引用而不用记住数字版本号。本篇不展开分支写入的合并语义(非本系列覆盖的湖侧运维细节),只强调一点:manifest 版本号本身已经是数据集状态的完整快照标识,tag 只是给它起了别名。

3.3 把 fragment 与 version 的演进摆成一张表

光看概念定义容易忘记 fragment id 和 version 号是两条互相独立递增的编号。用一段连续的写入操作把两者的演进具体化:

操作 Fragment 变化 Manifest 版本
初始 write_dataset(写入 3 个 fragment) 新增 fragment 0、1、2 version 1:包含 fragment {0,1,2}
增量 insert 一批新数据 新增 fragment 3(id 按当前最大值 +1 分配,不重用旧 id) version 2:包含 fragment {0,1,2,3}
对 fragment 1 执行 delete fragment 1 关联一个新的删除文件,fragment 本身不变 version 3:包含 fragment {0,1,2,3},fragment 1 的删除文件引用更新
触发 optimize 物化删除、合并小 fragment fragment 1 被重写(若物化)或被合并进新 fragment 4,旧 fragment 1、可能还有 0 被标记不再属于新版本 version 4:包含新的 fragment 集合,例如 {2,3,4}

这张表想说明两件事:fragment id 一旦分配就不会因为删除或合并而被重新利用(版本 4 里即便 fragment 0、1 已经不在当前版本的 fragment 列表里,它们的 id 依然是历史上分配过的,不会被后续新 fragment 复用);每一次会改变数据集内容的操作,都会产生一个新的 manifest 版本,即便这次操作只是标记删除而没有物理重写任何数据文件。这与 Milvus 的 segment id 分配(第 3 篇)逻辑上同构——都是”只增不重用”的编号策略,方便任何历史版本都能通过 id 精确定位到对应的物理对象,不会因为 id 复用产生歧义。


四、索引:IVF_PQ/HNSW 随数据集异步管理

Lance 官方索引文档(Vector Indexes、pylance Indexing and Searching)给出的机制要点:

  1. 索引不是默认创建的:LanceDB OSS 需要显式调用 create_index();托管版本可以自动推断向量列并创建优化过的 IVF_PQ 索引。
  2. 索引构建是异步的create_index() 调用会立即返回,真正的构建在后台进行;如需等待构建完成,可传入 wait_timeout
  3. 支持的索引族IVF_PQ(默认)、IVF_HNSW_FLATIVF_HNSW_PQIVF_HNSW_SQIVF_SQIVF_RQ 等,按召回/延迟/压缩目标切换。
  4. 训练参数num_partitions 控制 IVF 粗分区数,num_sub_vectors 控制 PQ 子向量数(压缩粒度);GPU 加速(accelerator="cuda")目前只支持 IVF_PQ 的分区训练,其它索引类型指定 accelerator 会退回 CPU 构建。
  5. 再次调用会替换旧索引create_index() 用不同参数再调用一次,会替换掉该列已有的索引,不是叠加多份。

「索引构建异步、与数据写入解耦」这一点与 Milvus Data Node 对 Sealed segment 异步建索(第 8、10 篇)同构:两边都不要求「写完立刻可用最优索引」,都允许「数据已可读(或已可搜)、索引还在后台构建」的中间态存在。差异在于索引的作用范围:Lance 的索引是整个数据集版本级别的对象(构建时基于当时的全部 fragment 训练),新增数据默认不会自动纳入已有索引,需要新一轮索引维护;Milvus 是每个 Sealed segment 一份独立索引(第 8 篇),新数据只需要为新产生的段单独建索引,不必重新训练整个 collection 的索引。这个差异直接决定了两边「新写入数据多久能被 ANN 索引覆盖」的运维模型不同:Lance 更接近「批量重建」节奏,Milvus 更接近「持续滚动」节奏。


五、lakehouse/21 已钉住的结构性事实

环境与口径见原文;此处只复述结论骨架:

指标 Parquet Lance
顺序扫 vec 1283.7 ms 430.0 ms
随机 take 100 行 1258.5 ms 1.2 ms

原文要点:

  1. Lance 用稳定行寻址,使 take(row_ids) 接近点查而非扫滤。
  2. 可原生挂 IVF-PQ / HNSW 等 ANN,索引随数据集管理(第四节已展开异步构建细节)。
  3. 自带 dataset versioning(第三节已展开 manifest/fragment 结构);与 Iceberg 主数据湖可并存,用 id + snapshot 对齐。
  4. 湖上 ANN 与表格式元数据如何统一 仍是开放问题(Iceberg Puffin 等装不下完整图索引)。

这些数字是 微基准、本地盘、随机向量,不代表生产对象存储吞吐;结构性差异(随机点取)由格式设计决定。


六、版本/snapshot 对齐:Lance manifest version 与 Milvus GuaranteeTs

三套系统各自维护一条独立的「版本轴」:Iceberg/Parquet 主表用 snapshot id,Lance 数据集用 manifest 版本号,Milvus 在线引擎用 GuaranteeTs / 一致性水位(第 12 篇)。它们互不感知对方的版本号,架构上常见的对齐方式是都携带同一个业务 id 与时间戳,靠应用层拼接:

flowchart LR
  subgraph iceberg ["Iceberg/Parquet main table"]
    snap["Snapshot id N<br/>row: business_id, feature, ts"]
  end
  subgraph lance ["Lance vector subset"]
    manifest["Manifest version M<br/>fragment: business_id, vec, ts"]
  end
  subgraph milvus ["Milvus online engine"]
    guaranteeTs["GuaranteeTs watermark<br/>segment: business_id, vec, ts"]
  end
  snap -->|"same business_id + ts"| manifest
  manifest -->|"ETL / sync job"| guaranteeTs
  snap -.->|"same business_id + ts"| guaranteeTs

对齐逻辑落地时要回答三个问题:

  1. 同一批数据在三边的时间戳是否来自同一次业务事件(例如同一次 embedding 生成任务),而不是各自系统各记各的写入时刻。
  2. 从 Lance 同步到 Milvus 的任务是否是 exactly-once——如果同步失败重试,Milvus 侧是否会出现重复向量或漏同步的向量,这需要业务 id 上的幂等写入(第 13 篇 upsert)配合。
  3. 版本回退时三边是否能一起回退——Iceberg snapshot 可以时间旅行,Lance manifest 版本也可以 checkout 到历史版本,但 Milvus 的 GuaranteeTs 水位只能前进不能后退(一致性模型不支持「回到过去某个可见集合」),这意味着三边对齐不是对称的:湖侧可以回看历史,在线引擎默认只服务当前水位之后的状态。

这一节要传达的核心结论是:不存在一个自动生效的统一版本号,湖侧两套系统(Iceberg snapshot、Lance manifest version)与在线引擎(Milvus GuaranteeTs)各自演进,对齐永远是应用层的显式设计,通常靠「同一业务 id + 同步任务记录的时间戳/版本号映射表」实现,而不是某种协议层面的自动版本传播。


七、与 Milvus 引擎的边界

问题 Lance / 湖侧 Milvus 引擎
主数据是否已在湖上 天然契合 需 ETL/同步进 Collection
在线 QPS、多租户、一致性级别 弱于专用引擎产品面 第 12–14 篇专长
持续高写入 + 实时可搜 取决于 LanceDB/服务封装;索引默认异步、批量重建节奏 Growing + WAL,段级持续滚动更新索引(第 5、8 篇)
随机取训练样本 / 特征 强项(fragment + 稳定行寻址) 杀鸡用牛刀
十亿级在线检索 + 过滤 常仍要专用引擎或分层 本系列主线
删除/软删的物化代价 重写 fragment + 重建索引(第 3.1 节) compaction + 建索队列(第 10、13 篇),机制同构

架构上常见分层(lakehouse/21 已写),把职责边界画成图更容易在做架构评审时对齐——每一层只对自己框内的问题负责,跨过边界的问题(例如「湖侧向量多久同步到在线引擎」)必须由应用层显式设计,不会自动解决:

flowchart TB
  subgraph lakeside ["Lake side: format owns version & random access"]
    iceberg["Iceberg/Parquet<br/>main table + features<br/>snapshot id"]
    lance["Lance dataset<br/>fragment + manifest<br/>row take, optional IVF_PQ/HNSW"]
  end
  subgraph onlineside ["Online engine: Milvus owns serving lifecycle"]
    segment["Segment: Growing/Sealed<br/>WAL, GuaranteeTs"]
    knowhere["Knowhere<br/>per-segment ANN index"]
    filterEngine["Bitset filter + consistency<br/>见第 11、12 篇"]
  end
  iceberg -->|"same business id"| lance
  lance -->|"ETL / sync job, not automatic"| segment
  segment --> knowhere
  segment --> filterEngine

图中箭头「ETL / sync job, not automatic」是这条边界最重要的标注:没有协议层面的自动同步,湖侧格式与在线引擎各自演进,谁负责同步、多快同步、失败怎么重试,都是应用层的显式设计(第六节继续展开版本对齐的三个具体问题)。


八、何时可以「不上向量集群」

同时满足时,优先考虑 Lance(± LanceDB)而不是先上 Milvus:

  1. 向量主要服务 训练、批推理、离线召回评测,不是高并发在线;
  2. 已有湖仓治理,要 同一 snapshot 复现
  3. 访问模式以 按 id 取向量 或中小规模 ANN 为主,且能接受索引批量重建的节奏;
  4. 团队运维预算不允许 etcd/对象段/多 Worker。

一旦出现:多租户在线 QPS、强 Session/Strong 可见性、独立建索队列与读写隔离、新数据需要持续滚动纳入 ANN 索引而非批量重建——回到 Milvus/Qdrant(第 15、18 篇)。


九、常见误解

误解一:Lance 的 dataset versioning 和 Milvus 的一致性级别是同一层面的东西,可以互相替代。 Lance 版本是数据集整体的离散快照,读者选择读哪个版本号,语义接近「时间旅行」;Milvus 一致性级别控制的是同一份持续演进数据的可见性水位裁剪,是连续的、只能前进的(第六节 3 点)。两者解决的是不同问题:一个给你「回到过去某个完整状态」,一个给你「在当前状态里愿意等多久换取多新鲜」。

误解二:Lance 支持向量索引,所以往 Lance 里灌数据后 ANN 检索会像 Milvus 一样「持续新鲜」。 第四节已说明索引构建异步且以数据集版本为整体训练单元,新增 fragment 默认不会自动进入已有索引;要覆盖新数据通常需要重新触发索引构建。这与 Milvus 「每个新 Sealed segment 单独建索引、持续滚动」的模型不同,直接套用 Milvus 的运维直觉(「写完等一下就有索引」)会在 Lance 上落空。

误解三:Lance 的软删(deletion file)和 Milvus 的 bitset 软删完全等价,选哪个都行。 机制相似(都是「先标记、后台物化」),但触发方式不同:Lance 的物化通常需要用户显式发起维护操作,且规范明确物化会使行地址失效并要求重建索引;Milvus 的 compaction 由 Coordinator 按策略自动调度(第 10 篇),运维介入程度不同,容量规划时不能假设两边的空间回收节奏一致。

误解四:fragment id 和 manifest 版本号是同一个计数器,看到 fragment id 大概能推算出数据集的版本号。 3.3 节的表格已经说明这是两条互相独立递增的编号:fragment id 只在有新数据写入或重写发生时才分配新值,manifest 版本号在任何改变数据集内容的操作(包括只打删除标记、不新增物理文件)后都会递增。同一个版本号下可能引用一批 id 跳跃很大的 fragment(因为中间有过多次合并与物化),不能从 fragment id 反推版本号,两者要分别读取。


十、学术谱系与开放问题

10.1 谱系

代表 本篇角色
列式格式的不可变文件 + 增量 Parquet row group/page(对照 lakehouse 系列) Lance 用 fragment/manifest 做出不同的随机访问权衡
版本化元数据 Iceberg snapshot/manifest 谱系(lakehouse 系列已覆盖) Lance manifest 是同一类思想在向量友好格式上的实现
软删 + 后台整理 O’Neil et al., LSM-Tree, Acta Informatica 1996(第 10 篇已引) Lance deletion file 物化代价与 Milvus compaction 同构
向量索引随数据集管理 IVF-PQ(Jégou et al.)、HNSW(Malkov & Yashunin, TPAMI 2020) 算法细节外链 frontier/08;本篇只钉训练/构建的工程时机

Lance 与 Iceberg/Parquet 共享「不可变文件 + 版本化 manifest」的谱系起点,但为「按行随机访问」这一新目标重新设计了物理布局(细粒度 fragment、稳定行地址),这是本系列反复出现的模式——同一类工程问题(不可变性 + 后台整理)会在不同的访问模式假设下长出不同的具体结构(对照第 3 篇 Growing/Sealed 与 LSM MemTable/SST 的类比)。

10.2 开放问题

  1. Lance 索引与 Iceberg 事务提交如何原子对齐?——两边各有自己的提交协议(Lance _transactions/ 事务文件 vs Iceberg 的原子性 snapshot 替换),跨系统的原子性目前依赖应用层协调,没有统一协议。
  2. 对象存储上 Lance 随机 take 的 API 次数与缓存策略(对照第 6 篇 Milvus 对象寻址代价)——lakehouse/21 的 1.2 ms 是本地盘结果,对象存储上的随机 take 延迟与请求次数尚无本系列可引用的实测。
  3. 从 Lance 增量同步到 Milvus 的 exactly-once 边界,以及同步延迟窗口内两边 ANN 索引覆盖率不一致时,查询侧该如何声明这一批数据「湖侧可查、在线引擎暂不可搜」的过渡态。
  4. Lance 索引批量重建的节奏与 Milvus 持续滚动建索的节奏,是否存在中间形态(例如增量索引合并到已有索引,而非整体重训),目前官方文档未展开。

十一、小结

三句话小结:Lance 补的是 湖侧随机访问与可选 ANN,manifest + fragment 的版本化设计让「回到某个历史快照」和「按行 O(1) 取值」成为一等能力,但索引构建以数据集版本为单位、默认异步且不自动覆盖新增数据;Milvus 补的是 在线引擎生命周期,索引按 Sealed segment 持续滚动构建,可见性由 GuaranteeTs 连续裁剪而非离散快照;两边的软删物化都要付出重建索引的代价,只是触发方与调度自动化程度不同。实测口径以 lakehouse/21 为准。

下一篇:生产排障


参考资料

  1. lakehouse/21 湖上 AI 与向量(本机 Lance vs Parquet 实测)。
  2. Lance Documentation, Table Format(manifest、fragment、deletion file 的规范定义,Arrow IPC / Roaring Bitmap 两种删除文件格式,物化代价)。
  3. Lance Documentation, Layout(数据集根目录结构:data/_versions/_transactions/_deletions/_indices/_refs/)。
  4. LanceDB Documentation, Vector Indexes;pylance Documentation, Indexing and Searchingcreate_index 异步构建、IVF_PQ/IVF_HNSW_* 索引族、num_partitions/num_sub_vectors、GPU 仅加速 IVF_PQ 训练)。
  5. O’Neil, P. et al., The Log-Structured Merge-Tree (LSM-Tree), Acta Informatica 33(4), 1996(软删 + 后台整理谱系;经第 10 篇引入)。
  6. 本系列第 1、3、6、8、10、12、13 篇;系列 index

返回 系列目录 | 上一篇:Qdrant 对照 | 下一篇:生产排障

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .