外观
检索
选择向量检索、全文检索或混合检索,返回应用真正需要的那批记录。
适用: 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() 会真正执行查询并报告各项度量。每次只改一个变量做对比,比如加一个索引、少返回几列,或者调整检索参数。