快速入门
在本节内容中,我们假设已经正确安装了 Faiss。文档中的代码示例涵盖 C++ 和 Python 两种语言。你可以直接复制粘贴这些代码运行,或者在 Faiss 分发包的 tutorial/ 子目录下执行示例代码。
获取数据
Faiss 处理的是固定维度 的向量集合,通常 介于几十到几百之间。这些集合可以用矩阵来保存。这里我们假设采用按行存储(row-major)的方式,即编号为 的向量的第 个分量存储在矩阵的第 行第 列。Faiss 内部仅使用 32 位浮点型(float32)矩阵。
你需要准备两个矩阵:
xb: 作为数据库向量,存储需要建立索引、并用于检索的所有向量,尺寸为xq: 作为查询向量,用于查找最近邻,尺寸为 。如果只有一个查询,
下面的例子中,我们将生成服从均匀分布的 64 维向量()。为了演示,还会对每个向量的第一个分量加上与索引相关的小偏移量。
Python 示例
import numpy as np
d = 64 # 向量维度
nb = 100000 # 数据库向量个数
nq = 10000 # 查询向量个数
np.random.seed(1234) # 保证结果可复现
xb = np.random.random((nb, d)).astype('float32')
xb[:, 0] += np.arange(nb) / 1000. # 对第一个分量添加偏移
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq) / 1000. # 同理对查询向量添加偏移
在 Python 中,所有矩阵均使用 numpy 数组表示,数据类型 (dtype) 必须为 float32。
若数据类型不是 float32,Faiss 将无法正确处理。
C++ 示例
int d = 64; // 向量维度
int nb = 100000; // 数据库向量个数
int nq = 10000; // 查询向量个数
float *xb = new float[d * nb];
float *xq = new float[d * nq];
for(int i = 0; i < nb; i++) {
for(int j = 0; j < d; j++) xb[d * i + j] = drand48();
xb[d * i] += i / 1000.;
}
for(int i = 0; i < nq; i++) {
for(int j = 0; j < d; j++) xq[d * i + j] = drand48();
xq[d * i] += i / 1000.;
}
这个例子使用了基础数组的方式存储数据,这是大多数 C++ 矩阵库都支持的底层做法。Faiss 也可以支持任何只要能提供底层内存指针的数据结构,例如使用 std::vector<float> 时,可以通过 data() 方法获取指针。
建议数据尽量保证是连续存储、且为 float32 类型,以获得最佳兼容性和效率。
构建索引并添加向量
Faiss 的核心对象是 Index(索引对象),用于管理并加速数据库向量的搜索。Index 内部可以对数据做预处理,提高检索速度。Faiss 支持多种索引类型,这里我们使用最简单的暴力 L2 距离近邻搜索索引:IndexFlatL2。
所有索引在创建时都需要知道向量的维度 。大部分索引还需要一个“训练”阶段来分析数据分布,但 IndexFlatL2 无需训练,可直接使用。
建立好索引(并完成必要的训练)后,主要有两类操作:
add:向索引中添加数据库向量(如xb)search:向索引查询最近邻
另外可以查看索引的两个状态变量:
is_trained:布尔值,指示是否需要训练ntotal:已索引的向量个数
部分索引还可以存储自定义整数 ID(但 IndexFlatL2 不支持)。若未提供,默认会按照插入顺序分配 ID:第一个向量为 0,第二个为 1,依此类推。
Python 示例
import faiss # 导入 faiss
index = faiss.IndexFlatL2(d) # 创建 L2 距离索引
print(index.is_trained)
index.add(xb) # 向索引添加数据库向量
print(index.ntotal)
C++ 示例
faiss::IndexFlatL2 index(d); // 构造 L2 距离索引
printf("is_trained = %s\n", index.is_trained ? "true" : "false");
index.add(nb, xb); // 添加向量到索引
printf("ntotal = %ld\n", index.ntotal);
运行结果
你会看到如下输出:
True # 表示索引已训练好,无需额外训练 100000 # 表示索引中已有 100000 个数据库向量
检索操作
Faiss 支持的基本检索操作是k 近邻搜索(k-nearest-neighbor search)。也就是对于每个查询向量,找出它在数据库向量中的 个最邻近的向量。
检索结果可以存储为 大小的矩阵(二维数组):
- 每一行存放一个查询向量的 k 个近邻的 ID(按距离从近到远排序)
- 同时返回一个 浮点型矩阵,保存每个近邻的平方距离
在正式搜索前,可以先用数据库前几个向量自己去搜自己:理想状况下,每个向量的最近邻就是自己。
Python 示例
k = 4 # 查找 4 个最近邻
D, I = index.search(xb[:5], k) # 对数据库前 5 个向量做检索(自检)
print(I)
print(D)
D, I = index.search(xq, k) # 用全部查询向量检索
print(I[:5]) # 前 5 个查询向量的近邻
print(I[-5:]) # 最后 5 个查询向量的近邻
C++ 示例
int k = 4;
// 检查:用数据库前 5 个向量作为查询
{
idx_t *I = new idx_t[k * 5];
float *D = new float[k * 5];
index.search(5, xb, k, D, I);
printf("I=\n");
for(int i = 0; i < 5; i++) {
for(int j = 0; j < k; j++) printf("%5ld ", I[i * k + j]);
printf("\n");
}
// ... 可继续打印 D (距离)或释放内存
delete [] I;
delete [] D;
}
// 正式用查询向量检索
{
idx_t *I = new idx_t[k * nq];
float *D = new float[k * nq];
index.search(nq, xq, k, D, I);
// ... 可继续处理结果
}
由于 C++ 代码较长,详细用法请参考 Faiss 官方教程/Cpp 子目录里的完整代码。
检索输出结果
自检的结果应该类似如下:
[[ 0 393 363 78] [ 1 555 277 364] [ 2 304 101 13] [ 3 173 18 182] [ 4 288 370 531]]
[[ 0. 7.17517328 7.2076292 7.25116253] [ 0. 6.32356453 6.6845808 6.79994535] [ 0. 5.79640865 6.39173603 7.28151226] [ 0. 7.27790546 7.52798653 7.66284657] [ 0. 6.76380348 7.29512024 7.36881447]] 可以看到,每个向量的最近邻 ID 都是自身编号(如第 0 个向量最近邻为 0),最近距离也为 0。每一行距离都是递增排序的。
之后正式检索结果类似:
[[ 381 207 210 477] [ 526 911 142 72] [ 838 527 1290 425] [ 196 184 164 359] [ 526 377 120 425]]
[[ 9900 10500 9309 9831] [11055 10895 10812 11321] [11353 11103 10164 9787] [10571 10664 10632 9638] [ 9628 9554 10036 9582]] 由于向量的第一个分量加了编号相关的偏移,整个数据集在第一维上是“拉伸”的,所以头几个向量搜到的邻居都靠近前面,而靠近 的向量的邻居也多出现在索引 附近。
在一台 2016 年的普通电脑上,上述检索操作大约耗时 3.3 秒。
实际应用中,检索速度与硬件配置、数据规模、索引类型密切相关。当数据量很大时,可以考虑更高效或分布式的索引类型。