lance数据的基础尝试
一、Lance 是什么,为什么要用它存数据
Lance 是一种面向 AI / 多模态数据的开放湖仓格式(Open Lakehouse Format)。它不只是一个文件格式,还包含表格式和版本管理能力,目标是把图片、文本、向量 embedding 和普通结构化字段放在同一个数据集里统一存储和检索。
和大家熟悉的 Parquet 相比,Lance 的几个特点更适合 AI 场景:
- 随机访问快:Parquet 更擅长整列扫描,训练/检索时按行随机取样本并不占优势;Lance 针对随机访问做了优化,官方称从 Parquet 转换后随机访问可快很多。
- 原生支持向量和多模态:一列存文本、一列存图片二进制、一列存向量,不用再把数据拆到多个系统里拼。
- 零拷贝版本管理:每次 append / overwrite 都会自动产生新版本,可以按版本读旧数据,像给数据集做快照,不用自己复制一份备份。
- 生态兼容好:基于 Arrow,和 pandas、PyArrow、DuckDB、Polars、PyTorch 都能打通,存进去的数据不用锁死在一个工具里。
一句话理解:Parquet 像一个适合分析扫描的仓库,Lance 更像一个适合 AI 训练和检索、还能随时回滚版本的数据集。
下面用 Python 的 pylance 做一次最基础的尝试:把数据写成 Lance、读回来、追加、按条件查询、看版本。
二、安装
Python SDK 的包名是 pylance,导入时叫 lance:
pip install pylance pandas pyarrow
三、第一步:把一个 DataFrame 存成 Lance
Lance 最核心的写入函数是 lance.write_dataset()。它支持直接写入 pandas DataFrame、PyArrow Table、PyArrow Dataset,甚至是 RecordBatch 的迭代器,大数据量时可以分批写,不用一次性全加载进内存。
先造一份最简单的数据:一批文档和它们的向量。
import lance
import pandas as pd
import numpy as np
# 模拟 4 条文档数据:id、标题、正文、向量
rng = np.random.default_rng(42)
df = pd.DataFrame({
"id": [1, 2, 3, 4],
"title": ["Lance 入门", "Parquet 对比", "向量检索", "数据版本"],
"text": [
"Lance 是一种面向 AI 的数据格式",
"Parquet 擅长列式扫描分析",
"向量列可以和文本存在同一张表里",
"每次写入都会自动产生新版本",
],
# 固定长度向量列:每条 4 维 embedding(真实场景常见 384 / 768 / 1024 维)
"vector": rng.random((4, 4)).astype("float32").tolist(),
})
uri = "./my_first.lance"
# 写入:mode 不写时默认会在目标不存在时新建
dataset = lance.write_dataset(df, uri, mode="overwrite")
print("写入完成,当前版本:", dataset.version)
print("行数:", dataset.count_rows())
运行后,本地会生成一个 my_first.lance 目录,这就是一个 Lance 数据集。注意它是一个目录而不是单个文件,里面包含数据文件和记录版本的 manifest,不要手动去改里面的文件。
四、第二步:把数据读回来
读取分两层:lance.dataset(uri) 打开数据集,.to_table() / .to_pandas() 才真正把数据读成表。
import lance
dataset = lance.dataset("./my_first.lance")
# 全部读回,转成 pandas
df = dataset.to_table().to_pandas()
print(df[["id", "title"]])
# 只读需要的列,避免把向量大列也一起加载
table = dataset.to_table(columns=["id", "title"])
print(table)
# 按条件过滤读取(filter 会下推到存储层,不用全量读回来再筛)
table = dataset.to_table(columns=["id", "title"], filter="id >= 3")
print(table.to_pandas())
日常用得最多的就是这三个参数:columns 选列、filter 过滤、limit 限量。做训练数据抽样或调试时,先小批量读回来看看,比直接全量加载稳妥得多。
五、第三步:追加数据(append)和覆盖(overwrite)
write_dataset 的 mode 决定了这次写入的语义,一共有三种:
create:新建数据集,目标已存在就报错;append:在最新版本后面追加新行,产生一个新版本;overwrite:用新数据整体覆盖,同样产生新版本(旧版本还在,可以回读)。
追加一批新数据:
import lance
import pandas as pd
new_df = pd.DataFrame({
"id": [5, 6],
"title": ["追加写入", "覆盖写入"],
"text": ["append 不会改动已有数据", "overwrite 会产生新版本"],
"vector": [[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8]],
})
dataset = lance.write_dataset(new_df, "./my_first.lance", mode="append")
print("追加后版本:", dataset.version) # 比之前 +1
print("追加后行数:", dataset.count_rows()) # 4 -> 6
注意追加时新数据的 schema 要和已有数据集一致:列名、类型、向量维度都要对得上,向量维度不一致是新手最常踩的坑。
六、第四步:版本和时间旅行
因为每次写入都会留版本,所以可以随时查看历史,并按指定版本读取:
import lance
dataset = lance.dataset("./my_first.lance")
# 查看所有版本
for v in dataset.versions():
print(v["version"], v["timestamp"])
# 按旧版本读数据:比如回到只有 4 条数据的第一个版本
old = lance.dataset("./my_first.lance", version=1)
print("v1 行数:", old.count_rows())
这个能力在 AI 数据集场景很实用:训练数据更新错了、标注改坏了,不用慌着找备份,直接按版本号读回上一版就行。
七、和 Parquet 互转(存量数据怎么办)
已有数据在 Parquet 里不用重造,两三行就能转成 Lance:
import lance
import pyarrow.dataset as ds
parquet_ds = ds.dataset("./data.parquet", format="parquet")
lance.write_dataset(parquet_ds, "./data.lance", mode="overwrite")
反过来,Lance 数据集本身兼容 PyArrow Dataset 接口,也可以读出来再写成 Parquet / DataFrame,迁移成本并不高。
八、常见坑总结
- 向量列要用固定长度:embedding 维度必须统一,用
float32的定长列表存储,不要每行维度都不一样。 - 不要手动改
.lance目录内部文件:版本一致性靠 manifest 维护,手动删文件会把数据集弄坏。 - 大数据分批写:数据量大时用 RecordBatch 迭代器传给
write_dataset,不要先拼成一个巨大的 DataFrame 再写,容易爆内存。 - Lance ≠ LanceDB:Lance 是底层数据格式,LanceDB 是建在它之上的向量数据库。只是存数据集、做训练数据管理,用 Lance 就够了;要做在线向量检索服务,再考虑 LanceDB 这一层。
- 存到对象存储也可以:
uri不仅能写本地路径,也能写s3://bucket/path这类对象存储路径,本地跑通后再切云上,代码基本不用改。
九、小结
这次基础尝试跑通了 Lance 存数据的最小闭环:
pandas / PyArrow 数据 → lance.write_dataset() 写入 → lance.dataset() 打开 → to_table() 按列、按条件读取 → append 追加 → 按 version 回读旧版本
如果你存的是普通分析数据,Parquet 可能就够了;但如果你的数据里同时有文本、图片和向量,还需要频繁追加、回滚和随机采样训练,Lance 这种为 AI 数据设计、带版本管理的格式,会比拼凑多个系统省事很多。建议先拿一个小数据集像上面这样跑一遍,再决定要不要迁移正式数据。
- 点赞
- 收藏
- 关注作者
评论(0)