Skip to content

快速开始 ​

安装 LanceDB,创建一张表,跑通你的第一次向量检索。

本快速开始使用 LanceDB Embedded 与本地的数据库,不需要账号,也不需要 API key。 LanceDB Embedded 跑在你应用自己的进程里;数据可以落在本地磁盘,也可以放在对象存储。

1. 安装 LanceDB ​

按你要用的语言挑一个。Python 用 Pandas 展示检索结果;TypeScript 需要 Node.js 22.18 或更高版本才能直接运行 .mts 示例。

bash
pip install lancedb pandas
bash
uv venv
uv pip install lancedb pandas
bash
npm install @lancedb/lancedb
bash
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/ lancedb
bash
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)。

接下来看什么 ​

基础表操作创建、更新与管理你的表。搭一个 RAG 应用在语言模型应用里使用检索到的上下文。索引让大规模数据集上的查询变快。向量化模型从你自己的数据生成向量。