跳转到内容

近似最近邻(ANN)索引

ANN(近似最近邻)或向量索引是一种专门设计的数据结构,旨在通过选定的距离度量高效组织和搜索基于相似度的向量数据。通过构建向量索引,可以有效地缩小搜索范围,避免对整个向量空间进行暴力扫描。相比穷举搜索(kNN或平面搜索),向量索引速度更快但准确性稍低。LanceDB提供了许多参数来微调索引大小、查询速度和结果准确性。

基于磁盘的索引

Lance提供了一种基于磁盘的IVF_PQ索引。它首先使用倒排文件索引(IVF)将数据集划分为N个分区,然后应用乘积量化来压缩每个分区中的向量。有关其工作原理的更多信息,请参阅索引概念指南。

创建IVF_PQ索引

Lance默认支持IVF_PQ索引类型。

创建索引是通过create_index方法完成的。

import lancedb

import numpy as np

uri = "data/sample-lancedb"

# Create 5,000 sample vectors
data = [
    {"vector": row, "item": f"item {i}"}
    for i, row in enumerate(np.random.random((5_000, 32)).astype("float32"))
]

db = lancedb.connect(uri)
# Add the vectors to a table
tbl = db.create_table("my_vectors", data=data)
# Create and train the index - you need to have enough data in the table
# for an effective training step
tbl.create_index(num_partitions=2, num_sub_vectors=4)

创建索引是通过create_index方法完成的。

import lancedb

import numpy as np

from lancedb.index import IvfPq

uri = "data/sample-lancedb"

# Create 5,000 sample vectors
data = [
    {"vector": row, "item": f"item {i}"}
    for i, row in enumerate(np.random.random((5_000, 32)).astype("float32"))
]

async_db = await lancedb.connect_async(uri)
# Add the vectors to a table
async_tbl = await async_db.create_table("my_vectors_async", data=data)
# Create and train the index - you need to have enough data in the table
# for an effective training step
await async_tbl.create_index(
    "vector", config=IvfPq(num_partitions=2, num_sub_vectors=4)
)

创建索引是通过lancedb.Table.createIndex方法完成的。

import * as lancedb from "@lancedb/lancedb";
import type { VectorQuery } from "@lancedb/lancedb";

const db = await lancedb.connect(databaseDir);

const data = Array.from({ length: 5_000 }, (_, i) => ({
  vector: Array(128).fill(i),
  id: `${i}`,
  content: "",
  longId: `${i}`,
}));

const table = await db.createTable("my_vectors", data, {
  mode: "overwrite",
});
await table.createIndex("vector", {
  config: lancedb.Index.ivfPq({
    numPartitions: 10,
    numSubVectors: 16,
  }),
});

创建索引是通过lancedb.Table.createIndex方法完成的。

import * as vectordb from "vectordb";

const db = await vectordb.connect("data/sample-lancedb");

let data = [];
for (let i = 0; i < 10_000; i++) {
  data.push({
    vector: Array(1536).fill(i),
    id: `${i}`,
    content: "",
    longId: `${i}`,
  });
}
const table = await db.createTable("my_vectors", data);
await table.createIndex({
  type: "ivf_pq",
  column: "vector",
  num_partitions: 16,
  num_sub_vectors: 48,
});
// For this example, `table` is a lancedb::Table with a column named
// "vector" that is a vector column with dimension 128.

// By default, if the column "vector" appears to be a vector column,
// then an IVF_PQ index with reasonable defaults is created.
table
    .create_index(&["vector"], Index::Auto)
    .execute()
    .await?;
// For advanced cases, it is also possible to specifically request an
// IVF_PQ index and provide custom parameters.
table
    .create_index(
        &["vector"],
        Index::IvfPq(
            // Here we specify advanced indexing parameters.  In this case
            // we are creating an index that my have better recall than the
            // default but is also larger and slower.
            IvfPqIndexBuilder::default()
                // This overrides the default distance type of l2
                .distance_type(DistanceType::Cosine)
                // With 1000 rows this have been ~31 by default
                .num_partitions(50)
                // With dimension 128 this would have been 8 by default
                .num_sub_vectors(16),
        ),
    )
    .execute()
    .await?;

IVF_PQ索引参数在crate文档中有更完整的定义。

可以指定以下IVF_PQ参数:

  • distance_type: 使用的距离度量方式。默认使用欧几里得距离"l2"。 我们还支持"cosine"(余弦)和"dot"(点积)距离。
  • num_partitions: 索引中的分区数量。默认值为行数的平方根。

注意

在同步Python SDK和Node的vectordb中,默认值为256。这个默认值在异步Python SDK和Node的lancedb中已更改。

  • num_sub_vectors: 在乘积量化(PQ)过程中将创建的子向量数量(M)。 对于D维向量,它将被划分为M个维度为D/M的子向量,每个子向量将被替换为单个PQ编码。默认值为向量维度除以16。
  • num_bits: 用于编码每个子向量的位数。仅支持4和8。位数越高,索引的准确性越高,但搜索速度也越慢。默认值为8。

注意

在同步Python SDK和Node的vectordb中,当前默认值为96。这个默认值在异步Python SDK和Node的lancedb中已更改。

IVF PQ

IVF_PQ index with num_partitions=2, num_sub_vectors=4

使用GPU构建向量索引

Lance Python SDK 实验性地支持使用GPU创建IVF索引。 使用GPU创建索引需要安装PyTorch>2.0

您可以通过以下方式指定用于训练IVF分区的GPU设备

  • accelerator: 指定为cudamps(在Apple Silicon上)以启用GPU训练。

# 使用Nvidia GPU的CUDA创建索引
tbl.create_index(
    num_partitions=256,
    num_sub_vectors=96,
    accelerator="cuda"
)

# 在Apple Silicon上使用MPS创建索引
tbl.create_index(
    num_partitions=256,
    num_sub_vectors=96,
    accelerator="mps"
)

注意

我们的异步客户端目前还不支持基于GPU的索引。

故障排除:

如果看到AssertionError: Torch not compiled with CUDA enabled错误,您需要安装支持CUDA的PyTorch

查询ANN索引

查询向量索引是通过search函数完成的。

有几个参数可以用来微调搜索:

  • limit (默认值: 10): 返回结果的数量
  • nprobes (默认值: 20): 使用的探测数量。数值越大搜索越精确但速度也越慢。
    大多数情况下,将nprobes设置为覆盖数据集的5-15%即可在低延迟下实现高召回率。

    • 例如,对于一个包含100万向量并划分为256个分区的数据集,nprobes应设置为约20-40。该值可进行调整,以实现搜索延迟和搜索质量之间的最佳平衡。
  • refine_factor (默认值: None): 通过读取额外元素并在内存中重新排序来优化结果。
    数值越高搜索越精确但也会更慢。如果发现召回率不理想,可以尝试从refine_factor=10开始调整。

    • 例如,对于一个包含100万向量并划分为256个分区的数据集,将refine_factor设置为200会先从所有搜索分区中检索出前4,000个候选(top k * refine_factor)。这些候选结果随后会经过重新排序以确定最终的前20个结果。

注意

只有在存在ANN索引时,nprobesrefine_factor参数才适用。如果在没有ANN索引的表上指定这些参数,它们将被忽略。

tbl.search(np.random.random((32))).limit(2).nprobes(20).refine_factor(
    10
).to_pandas()
await (
    (await async_tbl.search(np.random.random((32))))
    .limit(2)
    .nprobes(20)
    .refine_factor(10)
    .to_pandas()
)
                                          vector       item       _distance
0  [0.44949695, 0.8444449, 0.06281311, 0.23338133...  item 1141  103.575333
1  [0.48587373, 0.269207, 0.15095535, 0.65531915,...  item 3953  108.393867
const search = table.search(Array(128).fill(1.2)).limit(2) as VectorQuery;
const results1 = await search.nprobes(20).refineFactor(10).toArray();
const results_1 = await table
  .search(Array(1536).fill(1.2))
  .limit(2)
  .nprobes(20)
  .refineFactor(10)
  .execute();
let query_vector = [1.0; 128];
// By default the index will find the 10 closest results using default
// search parameters that give a reasonable tradeoff between accuracy
// and search latency
let mut results = table
    .vector_search(&query_vector)?
    // Note: you should always set the distance_type to match the value used
    // to train the index
    .distance_type(DistanceType::Cosine)
    .execute()
    .await?;
while let Some(batch) = results.try_next().await? {
    println!("{:?}", batch);
}
// We can also provide custom search parameters.  Here we perform a
// slower but more accurate search
let mut results = table
    .vector_search(&query_vector)?
    .distance_type(DistanceType::Cosine)
    // Override the default of 10 to get more rows
    .limit(15)
    // Override the default of 20 to search more partitions
    .nprobes(30)
    // Override the default of None to apply a refine step
    .refine_factor(1)
    .execute()
    .await?;
while let Some(batch) = results.try_next().await? {
    println!("{:?}", batch);
}
Ok(())

向量搜索选项在crate文档中有更完整的定义。

搜索将返回请求的数据以及每个项目的距离。

筛选(where子句)

您可以使用where子句进一步筛选搜索返回的元素。

tbl.search(np.random.random((32))).where("item != 'item 1141'").to_pandas()
await (
    (await async_tbl.search(np.random.random((32))))
    .where("item != 'item 1141'")
    .to_pandas()
)
const results2 = await table
  .search(Array(128).fill(1.2))
  .where("id != '1141'")
  .limit(2)
  .toArray();
const results_2 = await table
  .search(Array(1536).fill(1.2))
  .where("id != '1141'")
  .limit(2)
  .execute();

投影(select子句)

你可以使用select子句来选择查询返回的列。

tbl.search(np.random.random((32))).select(["vector"]).to_pandas()
await (
    (await async_tbl.search(np.random.random((32)))).select(["vector"]).to_pandas()
)
                                            vector _distance
0  [0.30928212, 0.022668175, 0.1756372, 0.4911822...  93.971092
1  [0.2525465, 0.01723831, 0.261568, 0.002007689,...  95.173485
...
const results3 = await table
  .search(Array(128).fill(1.2))
  .select(["id"])
  .limit(2)
  .toArray();
const results_3 = await table
  .search(Array(1536).fill(1.2))
  .select(["id"])
  .limit(2)
  .execute();

常见问题解答

为什么需要手动创建索引?

目前,LanceDB 不会 自动创建ANN索引。 LanceDB针对基于磁盘索引的kNN(穷举搜索)进行了高度优化。对于许多用例来说, 约10万数量级的向量数据集无需创建索引。如果您能接受最高 100毫秒的延迟,跳过索引创建是一个更简单的工作流程,同时能保证100%的召回率。

何时需要创建ANN向量索引?

LanceDB 内置了高度优化的SIMD代码用于计算向量相似度。在我们的基准测试中,计算10万对1K维向量的距离耗时不到20毫秒。我们观察到对于小型数据集(约10万行)或可接受100毫秒延迟的应用场景,通常不需要使用向量索引。

对于大规模或高维向量,创建向量索引可以提升性能。

我的索引有多大,会占用多少内存?

在LanceDB中,所有向量索引都是基于磁盘的,这意味着在响应向量查询时,只会从磁盘加载索引文件中的相关页面并缓存在内存中。此外,每个子向量通常会被编码为1字节的PQ码。

例如,对于一个1024维的数据集,如果我们选择num_sub_vectors=64,每个子向量将包含1024 / 64 = 16个float32数值。 使用乘积量化可以实现约16 * sizeof(float32) / 1 = 64倍的空间压缩。

如何为IVF_PQ索引选择num_partitionsnum_sub_vectors参数?

num_partitions 用于决定第一级 IVF 索引使用的分区数量。 分区数量越多,查询时的I/O效率可能更高,准确率也更好,但训练所需时间会大幅增加。 在 SIFT-1M 数据集上,我们的基准测试显示,保持每个分区4K-8K行数能获得较好的延迟/召回率平衡。

num_sub_vectors 指定每个向量上生成的产品量化(PQ)短码数量。该数值应为向量维度的因数。由于PQ是对原始向量的有损压缩,更高的num_sub_vectors通常会导致更小的空间失真,从而获得更好的精度。然而,更高的num_sub_vectors也会导致更重的I/O负担和更多的PQ计算,因此会增加延迟。dimension / num_sub_vectors应为8的倍数以获得最佳SIMD效率。

注意

如果num_sub_vectors设置的值大于向量维度,您将看到类似attempt to divide by zero的错误

如何为IVF_HNSW_*索引选择mef_construction参数?

m 决定新节点加入图时与其最近邻节点建立的连接数量。通常,m 的取值范围在5到48之间。较低的 m 值适用于低维数据或召回率要求不高的场景。相反,较高的 m 值有利于处理高维数据或需要高召回率的情况。本质上,较大的 m 会生成连接更紧密的稠密图,但代价是更高的内存消耗。

ef_construction 参数用于平衡构建速度与精度。数值越高精度越好但构建过程越慢,推荐取值范围为150至300。为保证搜索质量,建议最小值不低于100。多数情况下,将该值设为超过500并不会带来额外收益。请注意确保ef_construction的设定值始终大于或等于搜索阶段ef的参数值。