外观
快速开始
安装 LanceDB,创建一张表,跑通你的第一次向量检索。
本快速开始使用 LanceDB Embedded 与本地的数据库,不需要账号,也不需要 API key。 LanceDB Embedded 跑在你应用自己的进程里;数据可以落在本地磁盘,也可以放在对象存储。
1. 安装 LanceDB
按你要用的语言挑一个。Python 用 Pandas 展示检索结果;TypeScript 需要 Node.js 22.18 或更高版本才能直接运行 .mts 示例。
bash
pip install lancedb pandasbash
uv venv
uv pip install lancedb pandasbash
npm install @lancedb/lancedbbash
cargo init --name lancedb_quickstart
cargo add lancedb@0.31.0 --features polars,remote
cargo add tokio --features macros,rt-multi-thread在一个新建的空目录里执行那些 Rust 命令。polars feature 提供 DataFrame 输出;remote 让这个示例也能连到 Enterprise。
Python 预发布版本与较老的 CPU
Python 预发布版本
想在下一个稳定版发布前就用上最新特性和修复, 可以从 LanceDB 的 Fury 源安装预发布版。
bash
pip install --pre --extra-index-url https://pypi.fury.io/lancedb/ lancedbbash
uv venv
uv pip install --prerelease allow --index https://pypi.fury.io/lancedb/ lancedb
# To add to pyproject.toml, use:
uv add --prerelease allow --index https://pypi.fury.io/lancedb/ lancedb大多数应用只需要 lancedb。只有当你要构建查询引擎或连接器、直接调用格式层的 lance API, 或者要用 to_pandas() 转换普通扫描的惰性列 / 遗留 blob 列时, 才需要装可选的 pylance extra 并加上 Lance 的预发布源:
bash
pip install --pre \
--extra-index-url https://pypi.fury.io/lancedb/ \
--extra-index-url https://pypi.fury.io/lance-format/ \
"lancedb[pylance]"bash
uv pip install --prerelease allow \
--index https://pypi.fury.io/lancedb/ \
--index https://pypi.fury.io/lance-format/ \
--index-strategy unsafe-best-match \
"lancedb[pylance]"LanceDB 的预发布源里还留着旧的 pylance 版本;当前 Lance 的预发布版本发布在 lance-format 源上。默认情况下,uv 只会看到第一个包含该包的源里的版本, 于是可能选中旧的 pylance,或者一路回退到较老的 LanceDB 版本。 unsafe-best-match 会像 pip 一样搜索所有已配置的源;只在你信任这些源时使用。 请安装 extra 而不是不限版本的 pylance,这样 LanceDB 的最低支持版本才会被强制满足。
NOTE
预发布版本的测试力度与稳定版相同,但我们不承诺发布后 6 个月以上仍然可用。 用于真实业务负载时,请尽量使用最新的稳定版。
较老的 x86_64 CPU
默认的 lancedb wheel 面向 x86-64-haswell(AVX2 + FMA + F16C)。在没有 AVX2 的老 x86_64 CPU 上 (Intel Sandy Bridge、Ivy Bridge、Westmere;AMD Bulldozer、Piledriver、Steamroller), import lancedb 会以 Illegal instruction 崩溃。
这时改装 lancedb-compat。它对外暴露同样的 API(import lancedb 依然可用),以 x86-64-v2 为基线构建,并通过运行期 SIMD 分派在硬件支持时仍然使用 AVX2、FMA 或 AVX-512:
bash
pip install lancedb-compat两个包共用同一个 lancedb/ 命名空间,安装时会冲突,所以只能二选一。要切换的话, 先卸载另一个(pip uninstall lancedb && pip install lancedb-compat)。
2. 连接数据库
本地目录
LanceDB 会创建 ex_lancedb 目录来存放你的表,不需要任何服务端。
python
import lancedb
uri = "ex_lancedb"
db = lancedb.connect(uri)typescript
import * as lancedb from "@lancedb/lancedb";
const db = await lancedb.connect("ex_lancedb");rust
use lancedb::connect;
let db = connect("ex_lancedb").execute().await?;Python 的异步 API 见连接数据库。
对象存储
把本地路径换成 s3://、gs:// 或 az:// 形式的 URI 即可。凭据与各家服务商的专属选项见对象存储。
python
import lancedb
uri = "s3://your-bucket/path"
# You can also use "gs://your-bucket/path" or "az://your-container/path".
db = lancedb.connect(uri)typescript
import * as lancedb from "@lancedb/lancedb";
const uri = "s3://your-bucket/path";
// You can also use "gs://your-bucket/path" or "az://your-container/path".
const db = await lancedb.connect(uri);rust
use lancedb::connect;
let uri = "s3://your-bucket/path";
// You can also use "gs://your-bucket/path" or "az://your-container/path".
let db = connect(uri).execute().await?;Enterprise
使用管理员或 LanceDB 团队提供给你的数据库 URI、API key 与 endpoint,替换下面这些占位符。如果部署给了 region 就用它,否则保持 us-east-1。
python
import lancedb
db = lancedb.connect(
uri="db://your-database",
api_key="your-api-key",
region="us-east-1",
host_override="https://your-endpoint",
sql_host_override="grpc+tls://your-sql-endpoint:10026",
)typescript
import * as lancedb from "@lancedb/lancedb";
const db = await lancedb.connect("db://your-database", {
apiKey: "your-api-key",
region: "us-east-1",
hostOverride: "https://your-endpoint",
});rust
use lancedb::connect;
let db = connect("db://your-database")
.api_key("your-api-key")
.region("us-east-1")
.host_override("https://your-endpoint")
.execute()
.await?;说明
在应用代码里,请从环境变量或密钥管理服务加载凭据。示例见连接配置。
需要一套部署?联系 LanceDB 团队。本地执行与服务端执行的差异见 LanceDB Enterprise。
3. 建表并写入数据
每一行描述机器人视频片段中的一帧,除了视频、字幕和质量评分之外,还存一个向量。
说明
这里的向量是为了演示编出来的。在你的应用里,请用向量化模型从你的数据生成。
帧记录
python
data = [
{
"frame_id": "1",
"video_id": "clip-1",
"caption": "a robot arm reaching toward a cup",
"stats": {"motion": 3, "sharpness": 4, "exposure": 4, "framing": 3},
"embedding": [0.7, 0.1, 0.9, 0.7],
},
{
"frame_id": "2",
"video_id": "clip-1",
"caption": "a two-finger gripper closing on a cup",
"stats": {"motion": 5, "sharpness": 4, "exposure": 5, "framing": 2},
"embedding": [0.2, 0.9, 0.4, 0.9],
},
{
"frame_id": "3",
"video_id": "clip-2",
"caption": "a robot arm holding a cup steady",
"stats": {"motion": 2, "sharpness": 3, "exposure": 3, "framing": 4},
"embedding": [0.9, 0.1, 0.5, 0.4],
},
]typescript
const data = [
{
frame_id: "1",
video_id: "clip-1",
caption: "a robot arm reaching toward a cup",
stats: { motion: 3, sharpness: 4, exposure: 4, framing: 3 },
embedding: [0.7, 0.1, 0.9, 0.7],
},
{
frame_id: "2",
video_id: "clip-1",
caption: "a two-finger gripper closing on a cup",
stats: { motion: 5, sharpness: 4, exposure: 5, framing: 2 },
embedding: [0.2, 0.9, 0.4, 0.9],
},
{
frame_id: "3",
video_id: "clip-2",
caption: "a robot arm holding a cup steady",
stats: { motion: 2, sharpness: 3, exposure: 3, framing: 4 },
embedding: [0.9, 0.1, 0.5, 0.4],
},
];rust
use std::sync::Arc;
use lancedb::arrow::arrow_array::types::Float32Type;
use lancedb::arrow::arrow_array::{
FixedSizeListArray, Int8Array, LargeStringArray, RecordBatch, RecordBatchIterator,
RecordBatchReader, StructArray,
};
use lancedb::arrow::arrow_schema::{DataType, Field, FieldRef, Fields, Schema};
// Define structs representing the data schema
#[derive(Debug, Clone)]
struct Stats {
motion: i8,
sharpness: i8,
exposure: i8,
framing: i8,
}
#[derive(Debug, Clone)]
struct Frame {
frame_id: String,
video_id: String,
caption: String,
stats: Stats,
embedding: [f32; 4],
}
fn frames_schema() -> Arc<Schema> {
Arc::new(Schema::new(vec![
Field::new("frame_id", DataType::LargeUtf8, false),
Field::new("video_id", DataType::LargeUtf8, false),
Field::new("caption", DataType::LargeUtf8, false),
Field::new(
"stats",
DataType::Struct(Fields::from(vec![
Arc::new(Field::new("motion", DataType::Int8, false)),
Arc::new(Field::new("sharpness", DataType::Int8, false)),
Arc::new(Field::new("exposure", DataType::Int8, false)),
Arc::new(Field::new("framing", DataType::Int8, false)),
])),
false,
),
Field::new(
"embedding",
DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 4),
false,
),
]))
}
fn frames_to_reader(schema: Arc<Schema>, rows: &[Frame]) -> Box<dyn RecordBatchReader + Send> {
let frame_ids =
LargeStringArray::from_iter_values(rows.iter().map(|row| row.frame_id.as_str()));
let video_ids =
LargeStringArray::from_iter_values(rows.iter().map(|row| row.video_id.as_str()));
let captions = LargeStringArray::from_iter_values(rows.iter().map(|row| row.caption.as_str()));
let motion = Int8Array::from_iter_values(rows.iter().map(|row| row.stats.motion));
let sharpness = Int8Array::from_iter_values(rows.iter().map(|row| row.stats.sharpness));
let exposure = Int8Array::from_iter_values(rows.iter().map(|row| row.stats.exposure));
let framing = Int8Array::from_iter_values(rows.iter().map(|row| row.stats.framing));
let stats_fields: Vec<FieldRef> = vec![
Arc::new(Field::new("motion", DataType::Int8, false)),
Arc::new(Field::new("sharpness", DataType::Int8, false)),
Arc::new(Field::new("exposure", DataType::Int8, false)),
Arc::new(Field::new("framing", DataType::Int8, false)),
];
let stats = StructArray::new(
stats_fields.into(),
vec![
Arc::new(motion),
Arc::new(sharpness),
Arc::new(exposure),
Arc::new(framing),
],
None,
);
let embeddings = FixedSizeListArray::from_iter_primitive::<Float32Type, _, _>(
rows.iter()
.map(|row| Some(row.embedding.iter().copied().map(Some).collect::<Vec<_>>())),
4,
);
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(frame_ids),
Arc::new(video_ids),
Arc::new(captions),
Arc::new(stats),
Arc::new(embeddings),
],
)
.unwrap();
Box::new(RecordBatchIterator::new(
vec![Ok(batch)].into_iter(),
schema,
))
}
let data = vec![
Frame {
frame_id: "1".to_string(),
video_id: "clip-1".to_string(),
caption: "a robot arm reaching toward a cup".to_string(),
stats: Stats {
motion: 3,
sharpness: 4,
exposure: 4,
framing: 3,
},
embedding: [0.7, 0.1, 0.9, 0.7],
},
Frame {
frame_id: "2".to_string(),
video_id: "clip-1".to_string(),
caption: "a two-finger gripper closing on a cup".to_string(),
stats: Stats {
motion: 5,
sharpness: 4,
exposure: 5,
framing: 2,
},
embedding: [0.2, 0.9, 0.4, 0.9],
},
Frame {
frame_id: "3".to_string(),
video_id: "clip-2".to_string(),
caption: "a robot arm holding a cup steady".to_string(),
stats: Stats {
motion: 2,
sharpness: 3,
exposure: 3,
framing: 4,
},
embedding: [0.9, 0.1, 0.5, 0.4],
},
];用这些记录创建一张 frames 表:
python
table = db.create_table("frames", data=data, mode="overwrite")typescript
const table = await db.createTable("frames", data, { mode: "overwrite" });rust
use lancedb::database::CreateTableMode;
let schema = frames_schema();
let table = db
.create_table("frames", frames_to_reader(schema.clone(), &data))
.mode(CreateTableMode::Overwrite)
.execute()
.await
.unwrap();这个示例用 overwrite 模式,方便你反复运行——它会替换掉你所连接数据库里已有的 frames 表。
再追加一帧:
python
table.add(
[
{
"frame_id": "4",
"video_id": "clip-2",
"caption": "a gripper lowering a cup onto the table",
"stats": {"motion": 5, "sharpness": 4, "exposure": 4, "framing": 2},
"embedding": [0.3, 0.9, 0.6, 0.8],
}
]
)typescript
await table.add([
{
frame_id: "4",
video_id: "clip-2",
caption: "a gripper lowering a cup onto the table",
stats: { motion: 5, sharpness: 4, exposure: 4, framing: 2 },
embedding: [0.3, 0.9, 0.6, 0.8],
},
]);rust
let more_data = vec![Frame {
frame_id: "4".to_string(),
video_id: "clip-2".to_string(),
caption: "a gripper lowering a cup onto the table".to_string(),
stats: Stats {
motion: 5,
sharpness: 4,
exposure: 4,
framing: 2,
},
embedding: [0.3, 0.9, 0.6, 0.8],
}];
table
.add(frames_to_reader(schema.clone(), &more_data))
.execute()
.await
.unwrap();4. 检索这张表
找出与查询向量最接近的两个向量。这里用的是固定向量,不会调用向量化模型。
python
# Search for frames similar to "a gripper closing on an object"
query_vector = [0.2, 0.8, 0.4, 0.9]
# Ensure you run `pip install pandas` beforehand
result = (
table.search(query_vector)
.select(["frame_id", "video_id", "caption", "_distance"])
.limit(2)
.to_pandas()
)
print(result)typescript
// Search for frames similar to "a gripper closing on an object"
const queryVector = [0.2, 0.8, 0.4, 0.9];
const result = await table
.search(queryVector)
.select(["frame_id", "video_id", "caption", "_distance"])
.limit(2)
.toArray();
console.table(result);rust
use lancedb::arrow::IntoPolars;
use lancedb::query::{ExecutableQuery, QueryBase, Select};
// Search for frames similar to "a gripper closing on an object"
let query_vector = [0.2, 0.8, 0.4, 0.9];
let result = table
.query()
.nearest_to(&query_vector)
.unwrap()
.select(Select::Columns(vec![
"frame_id".to_string(),
"video_id".to_string(),
"caption".to_string(),
"_distance".to_string(),
]))
.limit(2)
.execute()
.await
.unwrap()
.into_polars()
.await
.unwrap();
println!("{result:?}");最先返回的是第 2 帧和第 4 帧——夹爪正在合上杯子,然后是把它放下。Python 展示为 Pandas DataFrame,Rust 展示为 Polars DataFrame,TypeScript 展示为结果数组。_distance 列衡量每条结果与查询向量的距离,数值越小表示越接近。
5. 用 SQL 整理数据
说明
第 5 步需要 LanceDB Enterprise:SQL 是 Enterprise 的特性。如果你用的是 LanceDB Embedded,请直接跳到下面的 展开嵌套字段。
LanceDB Enterprise 接受同一条连接上的 SQL,用来做单表扫描之外的抽样、连接与整理任务。先抽一小份样本看看数据:
python
sample = db.execute_query(
"SELECT frame_id FROM frames TABLESAMPLE (2 ROWS)"
).read_all()
print(sample.to_pylist())execute_query() 之外的 Flight SQL 客户端与 CLI 用法见 SQL。
接着加一条近重复的帧——第 5 帧是第 2 帧的后一帧,向量几乎一样——并把 dedupe 要读的向量索引建好:
python
# Frame 5 is a near-duplicate of frame 2: the gripper is still closing,
# one video frame later, with an almost identical embedding.
table.add(
[
{
"frame_id": "5",
"video_id": "clip-1",
"caption": "the two-finger gripper still closing on the cup",
"stats": {"motion": 5, "sharpness": 4, "exposure": 5, "framing": 2},
"embedding": [0.2, 0.9, 0.4, 0.89],
}
]
)
from lancedb.index import IvfFlat
table.create_index(
"embedding", config=IvfFlat(num_partitions=1, distance_type="l2")
)
table.wait_for_index(["embedding_idx"])把近重复项收敛进一个物化视图,让它在新增帧到来时保持最新,然后读回来:
python
db.execute_query("""
CREATE MATERIALIZED VIEW frames_deduped AS
SELECT * FROM dedupe(
'frames',
'embedding',
distance_threshold => 0.03
)
""")python
deduped = db.execute_query(
"SELECT frame_id FROM frames_deduped ORDER BY frame_id"
).read_all()
print(deduped.to_pylist())第 2 帧和第 5 帧合并成一条代表记录——五行变成四行。 这两个 id 里哪一个会存活,不保证在多次追加之间保持一致,所以别把它当作这个簇的稳定标识。 duplicate_pairs、各种消解策略以及 distance_threshold 的调参见近重复记录去重; 按百分比抽样、可重复抽取以及跨连接抽样见统计抽样。
6. 添加派生特征
展开嵌套字段
UDF 只能绑定到顶层列,所以先要把 stats 里的字段摊平。 这是一次普通的即时 add_columns 调用,因此在 LanceDB Embedded 与 LanceDB Enterprise 上行为一致——第 8 步两种情况下都要用到这些列:
python
table.add_columns(
{
"motion": "cast(stats.motion as float)",
"sharpness": "cast(stats.sharpness as float)",
"exposure": "cast(stats.exposure as float)",
"framing": "cast(stats.framing as float)",
}
)说明
这一步剩下的部分需要 LanceDB Enterprise——Functions 是 Enterprise 的特性。 如果你用的是 LanceDB Embedded,请直接跳到 第 7 步。
把 UDF 注册成 Function,再调用它填充一个由这些摊平字段算出来的列。这会对所有已有行立刻算一遍:
python
from lancedb import col, udf
@udf
def quality_score(
motion: float, sharpness: float, exposure: float, framing: float
) -> float:
return (motion + sharpness + exposure + framing) / 4.0
version = db.create_function_async(quality_score).wait()
table.add_columns(
{
"quality_score": version(
motion=col("motion"),
sharpness=col("sharpness"),
exposure=col("exposure"),
framing=col("framing"),
)
}
)
table.refresh_column("quality_score")像读其他列一样读它:
python
features = table.search().select(["frame_id", "video_id", "quality_score"]).limit(10).to_pandas()
print(features)refresh_column 只填充上次刷新之后新增的行,并重算输入发生变化的行,未变化的行不动。也可以把同样的刷新作为后台作业跑,这样不会阻塞:
python
job = table.refresh_column_async("quality_score")
job.wait()要写调用外部模型或服务的 UDF,见创建函数; 不想注册 Function、只用 SQL 表达式的话,见计算列。
7. 存储多模态数据
把图片、音频或其他二进制数据和向量、元数据存在一起,放在一个原始字节列里。下面这段从一个图片文件新建一张表——把路径换成你自己的文件:
python
from pathlib import Path
image_bytes = Path("data/sample-frame.jpg").read_bytes()
multimodal_table = db.create_table(
"frame_images",
data=[
{
"frame_id": "thumb-1",
"caption": "Sample frame thumbnail",
"image": image_bytes,
"embedding": [0.9, 0.1, 0.5, 0.4],
}
],
mode="overwrite",
)typescript
const arrow = await import("apache-arrow");
const { readFile } = await import("node:fs/promises");
const imageBytes = await readFile("data/sample-frame.jpg");
const imageSchema = new arrow.Schema([
new arrow.Field("frame_id", new arrow.Utf8()),
new arrow.Field("caption", new arrow.Utf8()),
new arrow.Field("image", new arrow.Binary()),
new arrow.Field(
"embedding",
new arrow.FixedSizeList(
4,
new arrow.Field("item", new arrow.Float32(), true),
),
),
]);
const imageData = lancedb.makeArrowTable(
[
{
frame_id: "thumb-1",
caption: "Sample frame thumbnail",
image: imageBytes,
embedding: [0.9, 0.1, 0.5, 0.4],
},
],
{ schema: imageSchema },
);
const multimodalTable = await db.createTable(
"frame_images",
imageData,
{ mode: "overwrite" },
);rust
use lancedb::arrow::arrow_array::BinaryArray;
let image_bytes = std::fs::read("data/sample-frame.jpg").unwrap();
let image_schema = Arc::new(Schema::new(vec![
Field::new("frame_id", DataType::LargeUtf8, false),
Field::new("caption", DataType::LargeUtf8, false),
Field::new("image", DataType::Binary, false),
Field::new(
"embedding",
DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 4),
false,
),
]));
let image_embeddings = [[0.9_f32, 0.1, 0.5, 0.4]];
let image_batch = RecordBatch::try_new(
image_schema.clone(),
vec![
Arc::new(LargeStringArray::from_iter_values(["thumb-1"])),
Arc::new(LargeStringArray::from_iter_values([
"Sample frame thumbnail",
])),
Arc::new(BinaryArray::from_iter_values([image_bytes.as_slice()])),
Arc::new(
FixedSizeListArray::from_iter_primitive::<Float32Type, _, _>(
image_embeddings
.iter()
.map(|vector| Some(vector.iter().copied().map(Some).collect::<Vec<_>>())),
4,
),
),
],
)
.unwrap();
let image_reader: Box<dyn RecordBatchReader + Send> = Box::new(RecordBatchIterator::new(
vec![Ok(image_batch)].into_iter(),
image_schema,
));
let multimodal_table = db
.create_table("frame_images", image_reader)
.mode(CreateTableMode::Overwrite)
.execute()
.await
.unwrap();更大的载荷以及能避免每次扫描都加载二进制的 Blob v2 列类型,见多模态数据类型。
8. 在自己的数据上训练
把表数据读进 PyTorch,只投影并打乱一个训练步真正需要的列:
python
import torch
from lancedb.streaming import StreamingDataset
dataset = StreamingDataset(table, columns=["motion", "sharpness"], shuffle_seed=42)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=1024, num_workers=0)StreamingDataset 把一张表包成可供 PyTorch 训练读取的数据源;columns 会在读取前先做投影,于是一个训练步只碰它选中的列,不会去读 caption、embedding 这类用不到的列。它在 LanceDB Embedded 与 LanceDB Enterprise 上行为一致。打乱、变换、断点续训与多 worker 加载见 PyTorch 数据加载器;它底层那套行序原语见置乱(Permutation)。