前情回顾
在 上一篇文章 中,我们成功将文本转化为了包含语义信息的向量,并学会了用余弦相似度来计算它们之间的"关系远近"。我们RAG系统的核心部件已经初具雏形。
但一个尖锐的工程问题也随之而来:
假设我们的知识库有100万个文本块(Chunks),那就是100万个向量。当用户提问时,我们要把问题的向量,和这100万个向量逐一计算相似度,再排序找到最相似的几个。
我们来简单算一下:即便每次计算和比较只需要1微秒(百万分之一秒),完成一次完整的搜索也需要整整 1秒!对于一个需要实时交互的应用来说,这个延迟是绝对无法接受的。
这就是暴力搜索(Brute-force Search)的局限。我们需要一个更聪明的办法,这便是向量索引(Vector Index)。
什么是向量索引?
向量索引是一种为了加速相似度搜索而对向量集合进行预处理的数据结构。
它的核心思想是:避免全局扫描,通过某种方式将向量空间划分成多个区域,搜索时只需访问可能包含查询结果的少数几个区域即可。
这个概念最好的类比就是图书馆的图书目录:
- 没有索引(暴力搜索):为了找一本关于"人工智能"的书,你把图书馆里成千上万本书一本一本地抽出来看书名和内容。
- 有索引(索引搜索):你直接走到计算机目录系统前,输入"人工智能",系统瞬间告诉你,这类书在"TP18"号书架上。你只需要去这一个书架寻找,效率天差地别。
向量索引就是我们向量世界的"图书目录"。它通过聚类等算法,提前将相似的向量"分门别类"地放在一起,构建起高效的查找路径。
见面实战:FAISS
要构建向量索引,我们得请出该领域的"王者"------FAISS (Facebook AI Similarity Search)。这是由Meta(原Facebook)AI团队开源的、用于高效相似度搜索和海量向量聚类的库,是目前工业界应用最广泛的工具之一。
首先,安装FAISS。我们先从CPU版本开始,它最容易安装:
bash
pip install faiss-cpu numpy
注:如果你有支持CUDA的NVIDIA显卡,可以挑战安装 faiss-gpu
版本,速度会更快。
四步构建你的第一个向量索引
我们将通过一个完整的流程,亲手构建、训练并使用一个向量索引。
第0步:准备数据
为了模拟真实场景,我们用 numpy
来生成一个包含10万个768维向量的随机数据集(模拟上一篇中 m3e-base
模型的输出)。
python
import numpy as np
# 设置向量维度
d = 768
# 设置数据集大小
nb = 100000
# 生成随机的向量数据集作为我们的知识库
np.random.seed(1234) # 保证每次生成的数据一致
xb = np.random.random((nb, d)).astype('float32')
# 生成1个随机的查询向量
xq = np.random.random((1, d)).astype('float32')
第1步:选择索引类型("目录的样式")
FAISS提供了丰富的索引类型,今天我们学习最常用的之一:IndexIVFFlat
。 IVF是"Inverted File"的缩写,它的原理就是我们上面提到的聚类 。它会先把10万个向量分成nlist
个簇(cluster),每个簇有一个中心点(centroid)。
创建它需要两个参数:
quantizer
:量化器,其实就是另一个"底层"的索引,用来对簇的中心点进行索引。我们这里用最简单的IndexFlatL2
(L2距离,即欧式距离的暴力搜索)。nlist
:簇的数量。一个经验法则是设为数据集大小的平方根附近,比如sqrt(100000)
约等于316,我们取一个接近的整数,比如256。
python
import faiss
nlist = 256 # 簇的数量
quantizer = faiss.IndexFlatL2(d) # 底层使用L2距离的暴力索引
index = faiss.IndexIVFFlat(quantizer, d, nlist)
第2步:训练索引("学习如何分类")
IndexIVFFlat
需要"学习"我们数据的分布,才能有效地划分簇。这个过程就是train
。
python
print(index.is_trained) # False,此时索引还未训练
index.train(xb)
print(index.is_trained) # True,训练完成
第3步:添加向量("将书上架")
训练好索引后,我们就可以把全部的向量数据批量添加到索引中。
python
index.add(xb)
print(f"索引中向量的总数: {index.ntotal}") # 100000
第4步:搜索!("按图索骥")
万事俱备,只欠搜索!search
方法接收两个参数:
xq
:查询向量。k
:希望返回的最相似结果的数量。
python
k = 5 # 我们想找最相似的5个
# D是距离,I是索引(在原始数据集xb中的位置)
D, I = index.search(xq, k)
print("查询结果的索引:", I)
print("查询结果的距离:", D)
FAISS瞬间就返回了最接近查询向量的5个向量的索引位置和它们的距离!
提速的关键 :IndexIVFFlat
还有一个重要的参数 nprobe
,它表示在搜索时,我们希望访问多少个簇(默认是1)。nprobe
越大,结果越精确,但速度越慢。这是一个速度与精度的权衡,也是近似最近邻搜索(ANN)的精髓所在。
python
# 增加nprobe可以提高精度,但会牺牲速度
index.nprobe = 10
D, I = index.search(xq, k)
print("设置nprobe=10后的索引:", I)
总结与预告
今日小结:
- 面对海量向量,暴力搜索效率低下,无法满足应用需求。
- 向量索引 (如FAISS)通过预处理和划分空间,实现高效的近似最近邻搜索(ANN)。
- 构建索引的基本流程是:准备数据 -> 选择索引类型 -> 训练索引 -> 添加向量 -> 搜索。
IndexIVFFlat
是一种常用的索引,它通过聚类实现加速,其nprobe
参数可以用来平衡搜索速度和精度。
我们今天用FAISS在内存中成功构建了索引,解决了"搜得慢"的问题。但是,新的问题又来了:
- 这个索引是临时的,程序一关,就没了!怎么持久化?
- 我们只存了向量,那每个向量对应的原始文本块存在哪?怎么关联起来?
- 每次启动都要重新加载100万个向量到内存,太麻烦了!
这些问题,正是专业的**向量数据库(Vector Database)**要解决的。
明天预告:RAG 每日一技(六):别再手动管理了!认识你的第一个向量数据库
明天,我们将上手一个轻量、好用的向量数据库(如ChromaDB),看看它是如何将向量存储、索引、元数据管理等工作"一条龙"搞定的,彻底解放我们的双手!