Skip to content

检索 ​

选择向量检索、全文检索或混合检索,返回应用真正需要的那批记录。

适用: Embedded ✅、Enterprise ✅

LanceDB 支持向量检索、全文检索与混合检索。要找回相似内容就用向量检索,要按词匹配就用全文检索,两者都要就用混合检索。过滤器则把结果收窄到符合你条件的记录。

选择检索方式 ​

你想找用需要准备
与查询相似的内容,比如相关段落或图片向量检索已存好的向量,以及维度兼容的查询向量
由多个向量表示的文档多向量检索一个多向量列,以及维度兼容的查询向量
包含特定词或短语的记录全文检索文本列上的 FTS 索引
既要语义相关,也要命中关键词混合检索向量 + FTS 索引
满足某个条件的行,不需要相关性排序过滤一个作用在表列上的谓词

向量检索不建索引也能跑。表变大时,一个近似最近邻索引能显著减少计算量,代价是召回率与延迟之间的取舍。全文检索则必须先建好 FTS 索引才能查询。

检索之前 ​

做向量检索时,为已有数据和查询生成向量要用同一个向量化模型与兼容的参数,两者维度必须一致。要把返回的字段(比如文档正文或条目 ID)和向量放在同一张表里。

做全文检索时,先选定文本列并为它建索引。Enterprise 是后台建索引的,所以要等索引就绪再查询。短语查询还要求索引里保留词元位置。详见 FTS 索引。

在 Python、TypeScript 或 Rust 里跑一个完整的检索示例

这些示例用三条样本记录和手写的向量,不需要任何向量化服务。每个程序都会建一张临时本地表,跑完就删掉。

  • Python: 用 pip install lancedb 安装,存成 search.py,然后执行 python search.py。
  • TypeScript: 用 npm install @lancedb/lancedb 安装,存成 search.mts,然后用 Node.js 22.18 或更高版本执行 node search.mts。
  • Rust: 下载并解压完整的 Rust 示例,然后在解压目录里执行 cargo run --locked --bin vector_search。
python
table = db.create_table(
    "documents",
    [
        {
            "id": 1,
            "text": "A puppy playing in the park",
            "category": "animals",
            "vector": [1.0, 0.0],
        },
        {
            "id": 2,
            "text": "A dog resting at home",
            "category": "animals",
            "vector": [0.9, 0.1],
        },
        {
            "id": 3,
            "text": "A guide to growing tomatoes",
            "category": "gardening",
            "vector": [0.0, 1.0],
        },
    ],
)
rows = (
    table.search([1.0, 0.0])
    .distance_type("l2")
    .select(["id", "text", "_distance"])
    .limit(2)
    .to_list()
)
print(rows)
typescript
const lancedb = await import("@lancedb/lancedb");
const { mkdtemp, rm } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const uri = await mkdtemp(join(tmpdir(), "lancedb-search-"));
try {
  const db = await lancedb.connect(uri);
  const table = await db.createTable("documents", [
    {
      id: 1,
      text: "A puppy playing in the park",
      category: "animals",
      vector: [1.0, 0.0],
    },
    {
      id: 2,
      text: "A dog resting at home",
      category: "animals",
      vector: [0.9, 0.1],
    },
    {
      id: 3,
      text: "A guide to growing tomatoes",
      category: "gardening",
      vector: [0.0, 1.0],
    },
  ]);
  const rows = await table
    .vectorSearch([1.0, 0.0])
    .distanceType("l2")
    .select(["id", "text", "_distance"])
    .limit(2)
    .toArray();
  console.log(rows);

} finally {
  await rm(uri, { recursive: true, force: true });
}
rust
use arrow_array::types::Float32Type;
use arrow_array::{FixedSizeListArray, Int64Array, RecordBatch, RecordBatchIterator, StringArray};
use arrow_schema::{DataType, Field, Schema};
use futures::TryStreamExt;
use lancedb::query::{ExecutableQuery, QueryBase, Select};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let directory = tempfile::tempdir()?;
    let db = lancedb::connect(directory.path().to_str().unwrap())
        .execute()
        .await?;
    let schema = Arc::new(Schema::new(vec![
        Field::new("id", DataType::Int64, false),
        Field::new("text", DataType::Utf8, false),
        Field::new("category", DataType::Utf8, false),
        Field::new(
            "vector",
            DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 2),
            true,
        ),
    ]));
    let batch = RecordBatch::try_new(
        schema.clone(),
        vec![
            Arc::new(Int64Array::from(vec![1, 2, 3])),
            Arc::new(StringArray::from(vec![
                "A puppy playing in the park",
                "A dog resting at home",
                "A guide to growing tomatoes",
            ])),
            Arc::new(StringArray::from(vec!["animals", "animals", "gardening"])),
            Arc::new(
                FixedSizeListArray::from_iter_primitive::<Float32Type, _, _>(
                    [[1.0, 0.0], [0.9, 0.1], [0.0, 1.0]]
                        .into_iter()
                        .map(|v| Some(v.into_iter().map(Some).collect::<Vec<_>>())),
                    2,
                ),
            ),
        ],
    )?;
    let reader = RecordBatchIterator::new(vec![Ok(batch)].into_iter(), schema);
    let table = db
        .create_table(
            "documents",
            Box::new(reader) as Box<dyn arrow_array::RecordBatchReader + Send>,
        )
        .execute()
        .await?;
    let query = table
        .vector_search(&[1.0_f32, 0.0])?
        .distance_type(lancedb::DistanceType::L2)
        .select(Select::columns(&["id", "text", "_distance"]))
        .limit(2);
    let rows: Vec<RecordBatch> = query.execute().await?.try_collect().await?;
    println!("{rows:?}");

    Ok(())
}

这个查询返回小狗和狗这两条记录,按向量距离排序。它直接在这张三行表上检索,既没有 ANN 索引,也没有向量化服务。连接配置见连接。

怎么看结果 ​

用 select() 挑出应用需要的列,用 limit() 设定最大返回条数。检索还会返回一个排序用的数值:

检索方式排序列顺序
向量_distance距离越小越靠前
全文_score文本相关性越高越靠前
混合_relevance_score综合相关性越高越靠前

这三个值的含义并不相同。不要拿向量距离直接跟全文分数比,也不要把重排序分数当成概率。

加过滤与重排序 ​

过滤器用来限定哪些行有资格参与,比如只查某个集合里的文档。前置过滤(prefilter)是先在这批行里检索;后置过滤(postfilter)则是从已经召回的候选里剔除不符合的行,因此返回条数可能少于你要求的数量。

重排序是对召回出来的候选重新排序。先从有代表性的查询和预期结果出发,再对比改动前后的相关性与延迟。重排序救不回一个压根没被召回的相关行。

分批读取大结果集 ​

一次把所有行都物化出来可能撑爆应用内存。做导出或训练读取时,请使用有界查询,或数据加载里介绍的分批读取接口。SDK 会先把 Enterprise 的查询响应缓冲下来再暴露批次,所以请设置查询的 limit,而不要只靠批次迭代来控制内存。

通过 Python 的 table.to_lance() 直接访问 Lance 数据集属于 LanceDB Embedded 的接口。如果需要同一份读取代码在 LanceDB Embedded 与 LanceDB Enterprise 上都能跑,请用 SDK 的查询构造器。

排查慢查询 ​

用查询计划查看一次查询最终选了哪些扫描方式、过滤条件和索引。explain_plan() 只看计划;analyze_plan() 会真正执行查询并报告各项度量。每次只改一个变量做对比,比如加一个索引、少返回几列,或者调整检索参数。