Lance 是一种为多模态数据(如图像、视频、音频和通用列式数据)设计的下一代列式存储格式。它支持本地 POSIX 文件系统和云对象存储(如 S3、GCS),以其极快的随机访问、零拷贝读取、与 Daft/DuckDB 的深度集成以及在向量检索工作负载中的卓越性能而著称。
Daft 为 Lance 提供了强大的支持,其核心能力包括:
- 并行与分布式读取:Daft 可以在默认的多线程或分布式的 Ray Runner 上并行执行 Lance 数据集的读取操作。
- 数据跳过(Data Skipping):利用 df.where() 谓词和文件/片段的统计信息,Daft 能够高效跳过不匹配的数据,加速查询。
- 版本与时间切片读取:通过 version 和 asof 参数,你可以精确读取 Lance 数据集的特定版本或某个时间点之前的最新版本。
- 扫描与缓存优化:支持通过 fragment_group_size 调整扫描效率,并通过 index_cache_size、metadata_cache_size_bytes 等参数微调缓存行为,优化大规模数据集的性能。
本指南将详细介绍如何使用 Daft 读写 Lance 数据集,并重点讲解如何结合火山引擎 TOS (S3 兼容) 进行配置,提供在仅安装 pylance 和 daft 的环境下可直接复制运行的最佳实践代码。
- 开通 Serverless EMR:Serverless EMR 当前支持 Daft/Ray 计算引擎,可以使用 serverless Ray 提交作业,作业提交方式参考 Serverless Ray 使用指南分布式的daft运行在 ray引擎上。
- EMR的Serverless Ray镜像默认已安装daft和pylance,用户无需单独安装。
- 如果需要在自己的机器上安装单机版本并验证代码正确性,操作如下。
可以按照以下步骤准备您的本地 Python 环境,用于单机调试代码
pip install -U pylance daft
Daft 提供了灵活的 S3 配置方式,支持标准凭证链和手动的 IOConfig 配置。
- 环境/CLI 凭证链(推荐):Daft 会自动查找标准 AWS 环境变量(AWS_ACCESS_KEY_ID 等)或 AWS CLI 配置文件(~/.aws/credentials)。
- 手动 IOConfig:构建一个 IOConfig 对象,在 read_lance 或 write_lance 调用中通过 io_config 参数传入。
以下代码片段定义了访问火山引擎 TOS 所需的配置。建议通过环境变量注入敏感信息,此处为方便演示,提供了测试用的默认值。后续所有示例将复用此配置。
from daft.io import IOConfig, S3Confi
AWS_ENDPOINT = os.getenv("AWS_ENDPOINT", "https://tos-s3-cn-shanghai.volces.com"
AWS_KEY_ID = os.getenv("AWS_KEY_ID", "your_ak")
AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY", "your_sk")
AWS_REGION = os.getenv("AWS_REGION", "cn-beijing")
BUCKET = os.getenv("BUCKET", "your bucket name"
endpoint_url=AWS_ENDPOINT,
access_key=AWS_ACCESS_KEY,
force_virtual_addressing=True
S3Config 提供了丰富的参数来定制 S3 访问行为:
- region_name (str): AWS 区域,例如 "us-west-2"。
- endpoint_url (str): S3 端点 URL。对于 AWS S3,通常不需要设置。对于 S3 兼容存储(如 TOS),此为必填项。
- key_id (str): Access Key ID。
- access_key (str): Secret Access Key。
- force_virtual_addressing (bool): 是否强制使用虚拟主机风格的地址(bucket.endpoint)。某些 S3 兼容服务需要设为 True。当它为 True 时,Daft 内部会将 endpoint_url 从 https://host 转换为 https://bucket.host 的形式。
使用 daft.read_lance() 函数可以轻松读取云端的 Lance 数据集。
此示例首先使用 daft.from_pydict 创建一个 Daft DataFrame,然后用 df.write_lance 将其写入 TOS,最后使用 daft.read_lance 将其读回。
df = daft.from_pydict({"a": [1, 2, 3], "b": ["x", "y", "z"
path = f"{TOS}/demo/my_lance_dataset"
df.write_lance(path, io_config=io_config, mode="overwrite"
print("### 从 TOS 读取数据集:")
df_tos = daft.read_lance(path, io_config=io_config)
以下为实际运行的输出片段,仅供参考。
(Showing first 3 of 3 rows)
Lance 支持数据版本控制。你可以使用 version 参数读取特定版本,或使用 asof 参数进行时间点查询。
from datetime import datetime
base_path = f"{TOS}/demo/versioned_dataset"
df_v1 = daft.from_pydict({"a": [1, 2]})
df_v1.write_lance(base_path, mode="overwrite", io_config=io_config)
df_v2 = daft.from_pydict({"a": [3, 4]})
df_v2.write_lance(base_path, mode="append", io_config=
df_latest = daft.read_lance(base_path, io_config=io_config)
print("\n### 按版本号读取 (版本 1):")
df_v1_read = daft.read_lance(base_path, version=1, io_config=io_config
以下为实际运行的输出片段,仅供参考。
(Showing first 4 of 4 rows)
(Showing first 2 of 2 rows)
Daft 会自动将 where()、select() 和 limit() 操作下推到 Lance 扫描器,以实现高效的数据过滤、列选择和行数限制。你可以通过 explain(True) 查看物理计划,确认下推是否生效。
from daft.expressions import col
base_path = f"{TOS}/demo/pushdown_dataset"
df = daft.from_pydict({"id": list(range(100)), "value": [i * 0.1 for i in range(100)]})
df.write_lance(base_path, mode="overwrite", io_config=io_
daft.context.set_planning_config(enable_strict_filter_pushdown=True
df_read = daft.read_lance(base_path, io_config=io_config)
filtered_df = df_read.where(col("id") > 50).select("value").limit(5
filtered_df.explain(True
以下为实际运行的输出片段,仅供参考。
== Unoptimized Logical Plan ==
* Filter: col(id) > lit(50)
* PythonScanOperator: LanceDBScanOperator(s3://lance-poc/demo/pushdown_dataset)
| File schema = id#Int64, value#Float64
| Partitioning keys = []
| Output schema = id#Int64, value#Float64
== Optimized Logical Plan ==
| File schema = id#Int64, value#Float64
| Partitioning keys = []
| Projection pushdown = [value]
| Filter pushdown = col(id) > lit(50)
| Output schema = value#Float64
| Stats = { Approx num rows = 0, Approx size bytes = 0 B, Accumulated selectivity
| Estimated Scan Bytes = 0
| Num Parallel Scan Tasks = 1
| Pushdowns: {projection: [value], filter: col(id) > lit(50), limit: 5}
| Schema: {id#Int64, value#Float64}
| {daft.io.lance.lance_scan:_lancedb_table_factory_function}
| Stats = { Approx num rows = 0, Approx size bytes = 0 B, Accumulated selectivity
| Batch Size = Range(0, 131072]
优化扫描:fragment_group_size
对于包含大量小文件(fragments)的 Lance 数据集,频繁的 I/O 和任务调度会带来显著开销。通过设置 fragment_group_size 参数,你可以将多个 fragments 分组到单个扫描任务中处理,从而有效减少开销。
base_path = f"{TOS}/demo/fragments_dataset"
df = daft.from_pydict({"id": [i]})
df.write_lance(base_path, mode="append" if i > 0 else "overwrite", io_config=io_
df_no_grouping = daft.read_lance(base_path, io_config=io_config)
print(f"不分组的分区数: {df_no_grouping.num_partitions()}"
df_grouped = daft.read_lance(base_path, fragment_group_size=3, io_config=io_config)
print(f"分组后的分区数: {df_grouped.num_partitions()}"
以下为实际运行的输出片段,仅供参考。
使用 DataFrame.write_lance() 方法可以将 Daft DataFrame 写入 Lance 格式。
write_lance 支持三种写入模式,并提供参数控制文件切分,这对于治理小文件至关重要。
- create: 创建新数据集,若路径已存在则失败。
- append: 向现有数据集追加数据,若路径不存在则失败。
- max_rows_per_file (int): 每个 Lance 文件的最大行数。
- max_bytes_per_file (int): 每个 Lance 文件的最大字节数(近似值)。
- max_rows_per_group (int): 每个 Row Group 的最大行数。Row Group 是 Lance 内部的读写单元。
path = f"{TOS}/demo/my_lance_table.lance"
df = daft.from_pydict({"a": list(range(10000
print("### 创建模式 (max_rows_per_file):")
meta_create = df.write_lance(path, mode="create", io_config=io_config, max_rows_per_file=8192)
print("\n### 覆盖模式 (max_bytes_per_file):")
meta_overwrite = df.write_lance(path, mode="overwrite", io_config=io_config, max_bytes_per_file=1024)
print("\n### 追加模式 (max_rows_per_group):")
meta_append = df.write_lance(path, mode="append", io_config=io_config, max_rows_per_group=1024)
以下为实际运行的输出片段,仅供参考。
### 创建模式 (max_rows_per_file):
╭───────────────┬──────────────────┬─────────────────┬─────────╮
│ num_fragments ┆ num_deleted_rows ┆ num_small_files ┆ version │
│ --- ┆ --- ┆ --- ┆ --- │
│ Int64 ┆ Int64 ┆ Int64 ┆ Int64 │
╞═══════════════╪══════════════════╪═════════════════╪═════════╡
╰───────────────┴──────────────────┴─────────────────┴─────────╯
(Showing first 1 of 1 rows)
### 覆盖模式 (max_bytes_per_file):
╭───────────────┬──────────────────┬─────────────────┬─────────╮
│ num_fragments ┆ num_deleted_rows ┆ num_small_files ┆ version │
│ --- ┆ --- ┆ --- ┆ --- │
│ Int64 ┆ Int64 ┆ Int64 ┆ Int64 │
╞═══════════════╪══════════════════╪═════════════════╪═════════╡
╰───────────────┴──────────────────┴─────────────────┴─────────╯
(Showing first 1 of 1 rows)
### 追加模式 (max_rows_per_group):
╭───────────────┬──────────────────┬─────────────────┬─────────╮
│ num_fragments ┆ num_deleted_rows ┆ num_small_files ┆ version │
│ --- ┆ --- ┆ --- ┆ --- │
│ Int64 ┆ Int64 ┆ Int64 ┆ Int64 │
╞═══════════════╪══════════════════╪═════════════════╪═════════╡
╰───────────────┴──────────────────┴─────────────────┴─────────╯
(Showing first 1 of 1 rows)
write_lance 允许你通过 Daft 的 Schema 对象来精细控制写入的 schema,包括类型、顺序和可空性。
from daft_tos_config import io_config, TOS
"compressible_text": ["a" * 200],
"image_bytes": [b"very-large-image-data..."]
path = f"{TOS}/demo/schema_lance_dataset"
df.write_lance(path, mode="overwrite", io_config=io_config)
df_read = daft.read_lance(path, io_config=io_config)
print("### 写入后的 Schema:")
以下为实际运行的输出片段,仅供参考。
╭───────────────────┬────────╮
╞═══════════════════╪════════╡
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ compressible_text ┆ String │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ image_bytes ┆ Binary │
╰───────────────────┴────────╯
说明
关于高级编码元数据高级编码元数据(例如 zstd 压缩、Blob 存储等)的指定需要通过 PyArrow 的 Schema 元数据能力。若您的环境仅安装了 pylance 和 daft,本示例将不展示该功能,以确保代码的普适性。如果需要此功能,请安装 pyarrow 并参考相关文档。
补充说明:高级编码元数据(如 zstd 压缩、Blob)需要 PyArrow 的元数据能力;在仅安装 pylance+daft 的本地环境中,示例展示默认推断的 schema。
核心原则:避免产生过多的小文件(碎片)。大量小文件会导致 I/O 开销剧增、元数据管理复杂化,并最终导致查询性能严重下降,尤其是在对象存储上。
- 使用 Daft (df.write_lance):
- max_bytes_per_file: 首选参数。直接控制文件大小,最符合对象存储的最佳实践。
- max_rows_per_file: 当数据行大小比较均匀时,可作为备选。
- max_rows_per_group: 控制 Row Group 粒度。一个文件可以包含多个 Row Group。合理的 Row Group 大小(通常建议 20,000 - 100,000 行)有助于过滤下推。
- 读取优化:如果已存在大量小 fragment,在读取侧使用 daft.read_lance 的 fragment_group_size 参数。它可以将多个小文件合并成一个扫描任务,减少 I/O 和调度开销。
- 离线治理:定期对碎片化的数据集进行重写(Compaction)。在重写时,统一规划写入参数(如 max_bytes_per_file)和压缩策略,生成结构健康的新版本。
- Q: 遇到 403 Forbidden 或 404 Not Found 错误?
- A: 通常是权限问题。请检查凭证、存储桶策略和 IAM 策略。
- A: 确保目标路径不是一个已存在的文件或 Lance 数据集。
- A: 确保目标路径是一个已存在的 Lance 数据集,并且待追加数据的 schema 与目标表兼容。