LanceDB Node.js LsmWriteSpec 接口详解:用 MemWAL LSM 写路径改造 mergeInsert
【免费下载链接】lancedbDeveloper-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.项目地址: https://gitcode.com/gh_mirrors/la/lancedb
LsmWriteSpec是 LanceDB Node.js SDK(@lancedb/lancedb)中用于为mergeInsert选择 Lance MemWAL(LSM 风格写路径)的核心接口。本文以 LsmWriteSpec.md 为骨架,结合 table.ts、table.rs 及 Rust 内核实现,讲解三种分片策略(bucket / identity / unsharded)的字段语义、约束条件与完整配置示例。读完本文,你将掌握如何为高并发 upsert 场景安装、查询与移除 LSM 写路径配置,并理解其底层校验逻辑。
接口速览:五个字段、三个分片策略
LsmWriteSpec是一个 TypeScript 接口,定义于 nodejs/lancedb/table.ts,通过setLsmWriteSpec(spec)安装到表上。其核心结构如下:
export interface LsmWriteSpec { /** One of `"bucket"`, `"identity"`, or `"unsharded"`. */ specType: "bucket" | "identity" | "unsharded"; /** Bucket and identity variants: the sharding column. */ column?: string; /** Bucket variant: the number of buckets, in `[1, 1024]`. */ numBuckets?: number; /** * Indexes the MemWAL keeps up to date. Omit to maintain every supported * index, resolved on install — a snapshot, so indexes created later are not * maintained. Pass `[]` for none. */ maintainedIndexes?: string[]; /** Default `ShardWriter` configuration recorded in the MemWAL index. */ writerConfigDefaults?: Record<string, string>; }五个字段中只有specType是必填的,其余字段的必填性由specType决定:
| 字段 | 类型 | 必填性 | 语义 |
|---|---|---|---|
specType | "bucket" \| "identity" \| "unsharded" | 必填 | 分片策略 |
column | string | bucket、identity必填 | 分片列 |
numBuckets | number | bucket必填 | 桶数量,取值范围[1, 1024] |
maintainedIndexes | string[] | 可选 | MemWAL 保持同步的索引清单 |
writerConfigDefaults | Record<string, string> | 可选 | 写入 MemWAL 索引的默认ShardWriter配置 |
从底层实现看,Node.js 层通过 nodejs/src/table.rs 的 napi 结构体LsmWriteSpec与 Rust 内核互通,specType被转换为String,numBuckets为Option<u32>,writerConfigDefaults为Option<HashMap<String, String>>。Rust 内核中则对应rust/lancedb/src/table.rs的pub enum LsmWriteSpec(见 rust/lancedb/src/table.rs),包含Bucket { column, num_buckets, ... }、Identity { column, ... }、Unsharded { ... }三个变体。
三种分片策略详解
bucket:按主键哈希分桶
specType: "bucket"按单列 unenforced 主键做哈希分桶写。column与numBuckets均必填。语义要点:
- 桶数范围严格为
[1, 1024]; - 底层采用 Iceberg 兼容的 Murmur3-x86-32(seed 0)哈希,保证
bucket(column, numBuckets)在不同进程间计算稳定; column必须是支持的非嵌套标量列;- 分桶列必须是表的单列 unenforced 主键本身。
Node.js 层的校验逻辑位于 nodejs/src/table.rs:若specType为"bucket"而缺少column,抛出"LsmWriteSpec bucket requires column";缺少numBuckets则抛出"LsmWriteSpec bucket requires numBuckets";specType非法值时抛出"LsmWriteSpec 'specType' must be 'bucket', 'identity', or 'unsharded'"。
典型配置:
await table.setUnenforcedPrimaryKey("id"); await table.setLsmWriteSpec({ specType: "bucket", column: "id", numBuckets: 16, maintainedIndexes: ["id_idx"], });identity:按列原始值分片
specType: "identity"按column的原始值分片,每个不同的列值对应一个独立 shard。它适用于数据本身已按column天然分区、希望每个分区值落在独立 shard 的场景(例如按region分片,见 rust/lancedb/src/table/merge.rs 中LsmWriteSpec::identity("region")的测试用法)。
这里有一个需要特别注意的约束:identity要求column是未强制主键(unenforced primary key)的确定性函数。也就是说,同一主键的每一行,必须永远产生相同的column值;否则该键的 upsert 可能落入不同 shard,导致旧版本数据覆盖新版本(stale version can win)。
配置示例:
await table.setUnenforcedPrimaryKey("id"); await table.setLsmWriteSpec({ specType: "identity", column: "region", });unsharded:单 shard 全量写入
specType: "unsharded"将所有mergeInsert写入路由到单个 MemWAL shard,不进行任何分片。它不需要column和numBuckets,是最简单的启用方式,适合数据量不大或不需要并行 shard 写入的场景。Rust 内核通过LsmWriteSpec::unsharded()构造(见 rust/lancedb/src/table.rs)。
await table.setUnenforcedPrimaryKey("id"); await table.setLsmWriteSpec({ specType: "unsharded", });maintainedIndexes:MemWAL 维护的索引
maintainedIndexes指定 MemWAL 在追加行时持续维护(保持最新)的索引列表。其行为有三个关键点:
- 省略(
undefined):维护表上所有可维护的索引,在安装时解析并快照;此后新建的索引不会被维护。若某个索引无法被维护,安装会直接失败。 - 传
[]:不维护任何索引。 - 显式列出索引名:锁定一个精确的索引集合,且仍处于构建中的索引会被拒绝而不是被静默跳过。
getLsmWriteSpec返回时maintainedIndexes永远是安装时解析出的具体列表,undefined不会往返(见 nodejs/lancedb/table.ts 的注释说明)。
Rust 内核侧对「维护索引」的实现体现在 rust/lancedb/src/table/merge/lsm.rs:安装时调用resolve_maintained_indexes合并list_indices()的结果与显式传入的清单。测试用例中也大量使用with_maintained_indexes,例如 FTS 索引(rust/lancedb/src/table/merge.rs)与向量索引(rust/lancedb/src/table/merge.rs)。
这一点直接影响读取正确性:查询层的校验表明,MemWAL LSM 扫描器的全文检索要求 FTS 索引由 write spec 维护,否则未压缩的文档会被遗漏;向量索引同理,否则已压缩但尚未重建索引的行会被遗漏(见 rust/lancedb/src/table/query/lsm.rs 与 rust/lancedb/src/table/query/lsm.rs)。
writerConfigDefaults:默认 ShardWriter 配置
writerConfigDefaults是一组以字符串键值对记录的默认ShardWriter配置,安装时被写入 MemWAL 索引,后续每次mergeInsert打开 shard writer 时作为默认值使用。Node.js 层将其映射为HashMap<String, String>,未提供时在 nodejs/src/table.rs 以unwrap_or_default()兜底为空 map,并经由with_writer_config_defaults透传给内核。
配置示例:
await table.setLsmWriteSpec({ specType: "bucket", column: "id", numBuckets: 32, writerConfigDefaults: { maxRowsPerGroup: "1024", targetBytesPerGroup: "67108864", }, });完整使用流程:安装、查询与移除
安装前置条件
所有变体都要求表先设置 unenforced 主键(setUnenforcedPrimaryKey);bucket分片还要求该主键恰好是被分桶的单列。此外,Rust 内核安装时会做额外校验(见 rust/lancedb/src/table/merge/lsm.rs):
- 表必须可写(
ensure_mutable); - 表上不能已存在 LSM write spec(不支持变更,需先
unset); - 含计算列(computed columns)的表不支持安装,因为未压缩层级的行对 refresh 不可见;
- 物化视图(materialized view)不支持安装,原因同上。
安装:setLsmWriteSpec
import * as lancedb from "@lancedb/lancedb"; const db = await lancedb.connect("data/sample-lancedb"); const table = await db.openTable("my_table"); // 1. 设置 unenforced 主键(只支持单列,设置后不可更改) await table.setUnenforcedPrimaryKey("id"); // 2. 安装 bucket 分片规格 await table.setLsmWriteSpec({ specType: "bucket", column: "id", numBuckets: 16, maintainedIndexes: ["id_idx"], }); // 3. 后续 mergeInsert 走 MemWAL LSM 写路径 await table.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(rows);查询:getLsmWriteSpec
返回当前安装的规格;当 LSM 写路径未启用时返回undefined:
const spec = await table.getLsmWriteSpec(); if (spec) { console.log(spec.specType); // "bucket" console.log(spec.column); // "id" console.log(spec.numBuckets); // 16 console.log(spec.maintainedIndexes); // 安装时解析出的具体索引列表 }移除:unsetLsmWriteSpec
unsetLsmWriteSpec会删除 MemWAL 索引,使mergeInsert回退到标准写路径;如果当前未安装任何 spec,则抛出错误:
await table.unsetLsmWriteSpec(); const spec = await table.getLsmWriteSpec(); // undefined配套的 LSM 生命周期方法
安装 LSM 写路径后,Table还暴露了一组配套方法(见 nodejs/lancedb/table.ts):
closeLsmWriters():排空并关闭缓存的 MemWAL shard writer,下一次mergeInsert惰性重开;无缓存时为 no-op。flushLsm():将每个桶的活动 memtable 封存为新的 L0 代(seal),空 memtable 封存是 no-op,可重复调用。compactLsm():触发每个桶的后台 L0 → base 压缩,返回时仅表示压缩任务已派发,需通过getLsmStats观察进度。checkpointLsm():封存一次后触发压缩并轮询直到起始时刻的 L0 消失,目标集固定,因此写入负载下也能终止,可安全按周期运行(调用方负责超时)。
实现原理:从 TypeScript 到 Rust 内核的调用链
LsmWriteSpec的完整调用链体现了 LanceDB 的架构分层:
- TypeScript 层:nodejs/lancedb/table.ts 中
setLsmWriteSpec/unsetLsmWriteSpec/getLsmWriteSpec直接代理到内部this.inner(napi 绑定对象)。 - napi 绑定层:nodejs/src/table.rs 将 JS 对象转换为
lancedb::table::LsmWriteSpec(TryFrom实现见 nodejs/src/table.rs),完成必填字段校验后调用内核 API。 - Rust 内核层:rust/lancedb/src/table/merge/lsm.rs 的
set_lsm_write_spec负责前置校验(可变性、重复安装、计算列/物化视图限制)、解析维护索引,并通过dataset.initialize_mem_wal()构建InitializeMemWalBuilder——三种策略分别映射到bucket_sharding(column, num_buckets)、identity_sharding(column)与unsharded()(见 rust/lancedb/src/table/merge/lsm.rs)。
内核中还通过LsmWriteSpec::from将存储的 MemWAL 索引细节还原为公开的LsmWriteSpec(见 rust/lancedb/src/table/merge/lsm.rs),其中根据IDENTITY_TRANSFORM/UNSHARDED_TRANSFORM标记区分策略,这保证了getLsmWriteSpec能忠实还原安装时的配置(maintainedIndexes恒为解析后的具体列表)。
小结
LsmWriteSpec用极简的五字段接口为 LanceDB Node.js 提供了三种可选的 MemWAL LSM 写路径策略:bucket面向按主键哈希并行分片的高吞吐 upsert,identity面向天然按列分区的数据,unsharded面向最简单的单 shard 场景。理解maintainedIndexes的快照语义与索引维护对读取正确性的影响,是正确使用该特性的关键。完整接口文档见 LsmWriteSpec.md,源码入口见 nodejs/lancedb/table.ts 与 rust/lancedb/src/table.rs。
【免费下载链接】lancedbDeveloper-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.项目地址: https://gitcode.com/gh_mirrors/la/lancedb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考