1. 摘要
在计算机视觉、自然语言处理、推荐系统等领域的工程实践中,"从百万/亿级向量中快速找出最相似的TopK个" 是一个极其常见的需求。比如:
- 以图搜图时,需要在海量图像特征库中匹配与查询图最相似的结果
- 在推荐系统中,需要根据用户的兴趣向量快速召回候选内容
对于这类问题,最直观的思路是暴力检索(brute-force search/retrieval):拿查询向量与库中所有向量挨个计算相似度,然后排序取TopK。但这种方法的时间复杂度是 O(n) ,当向量数量级达到百万级时,单次查询可能需要几秒;如果到了亿级,暴力检索几乎是"不可用"的------不仅慢,还会占用大量计算资源。
这时,Faiss 就成了解决这类问题的"神器"。
2. 什么是Faiss?
Faiss的全称是 Facebook AI Similarity Search,是Meta(原Facebook)AI研究院针对大规模向量相似度检索问题开发的开源工具库。
FAISS开源仓库地址:https://github.com/facebookresearch/faiss
它的核心特点是:
- 高性能 :使用C++编写,底层做了大量优化(如SIMD指令、GPU并行计算),对10亿量级的索引可以做到毫秒级检索
- 多语言支持:虽然核心是C++,但提供了简洁易用的Python接口,对Python开发者非常友好
- 索引类型丰富 :支持Flat、IVF、HNSW等多种索引结构,可根据数据量和精度需求灵活选择
- GPU加速:部分索引支持GPU构建和查询,速度比CPU版快10-100倍。
简单来说,Faiss的工作就是把我们的候选向量集封装成一个index(索引数据库),通过高效的索引结构替代暴力遍历,从而大幅加速TopK检索过程。
3. Faiss的核心优势
相比其他向量检索工具,Faiss的优势非常突出:
- 工业级稳定性:从2017年开源至今,经过近10年的工业打磨,被全球众多大厂(如Meta、谷歌、阿里等)广泛应用于生产环境
- 性能天花板高:在"纯向量检索库"赛道,它的速度和精度依然处于第一梯队
- 生态完善:文档齐全、社区活跃,遇到问题很容易找到解决方案
- 学习成本低:API设计简洁,Python接口上手极快,半天就能跑通基本流程
4. Faiss的安装指南
强烈建议安装 Faiss 1.7.3以上版本------新版本已经解决了老版本安装困难、bug多的问题,安装非常简单。根据我们的需求,可以选择CPU版或GPU版:
- 如果我们的数据量在几万到几十万条,或者部署环境没有GPU,CPU版完全够用
- 如果有NVIDIA显卡,且数据量在百万级以上,或者需要低延迟/高并发查询,GPU版是更好的选择
5. 从基础操作开始
我们将通过一个极简的官方示例,带你快速上手Faiss的核心流程------虽然整体步骤看似"把大象装冰箱"般简单,但每个环节都有需要注意的关键点,掌握这些能帮你少走很多弯路。
5.1. Faiss 核心流程:三步完成相似度检索
Faiss的所有检索任务,本质上都遵循这三个核心步骤:
- 准备向量数据:生成或加载待检索的向量库(database vectors)和查询向量(query vectors);
- 构建并填充索引:选择合适的索引类型,将向量库添加到索引中;
- 执行TopK检索:用查询向量在索引中搜索最相似的TopK个结果。
下面我们结合官方示例代码,逐一拆解每个步骤。
5.1.1. 第一步:准备向量数据
首先我们需要生成模拟的向量数据------在实际工程中,这一步通常是用预训练模型(如ViT、Sentence-Transformers)提取图像或文本的特征向量。
python
import numpy as np
# 1. 定义基础参数
d = 64 # 向量维度(比如ViT-Base输出768维,这里简化为64维)
nb = 100000 # 向量库的总数据量(模拟10万条候选向量)
nq = 10000 # 待检索的查询向量数量(模拟1万次查询)
# 2. 设置随机种子,保证结果可复现
np.random.seed(1234)
# 3. 生成向量库(xb)和查询向量(xq)
# 注意:Faiss要求所有向量必须是float32类型,这一点非常重要!
xb = np.random.random((nb, d)).astype('float32')
xq = np.random.random((nq, d)).astype('float32')
# 4. 给向量加一点"人工区分度"(仅用于测试,实际工程中不需要)
# 让向量的第一维随索引递增,这样每个向量都有明显的差异,方便验证检索结果是否正确
xb[:, 0] += np.arange(nb) / 1000.
xq[:, 0] += np.arange(nq) / 1000.
⚠️关键补充说明:
- 数据类型必须是float32 :Faiss底层是C++实现的,对数据类型有严格要求,
float64会报错,必须用.astype('float32')转换。 - 向量维度d要统一:向量库和查询向量的维度必须完全一致,否则无法构建索引。
- 人工区分度的作用 :示例中给第一维加递增项,是为了让"索引相近的向量更相似",这样检索结果会更直观(比如查询向量
xq[0]最相似的应该是xb[0]附近的向量)。
5.1.2. 第二步:构建索引并添加向量
这一步是Faiss的核心------我们需要选择合适的索引类型,然后把向量库"喂"给索引。这里我们先用最简单的暴力检索索引(IndexFlatL2) 来演示。
python
import faiss
# 1. 构建索引:IndexFlatL2
# - IndexFlat:表示这是"Flat索引"(暴力检索,无优化)
# - L2:表示相似度度量方法为L2范数(即欧氏距离,距离越小越相似)
index = faiss.IndexFlatL2(d)
# 2. 检查索引是否需要训练
# Flat索引不需要训练,直接添加向量即可,所以输出为True
print(f"索引是否需要训练: {index.is_trained}")
# 3. 将向量库添加到索引中
index.add(xb)
# 4. 查看索引中已添加的向量总数
print(f"索引中已添加的向量总数: {index.ntotal}")
plain
索引是否需要训练: True
索引中已添加的向量总数: 100000
⚠️关键补充说明:
- IndexFlatL2的特点:它是Faiss中最基础的索引,原理是"暴力遍历所有向量计算距离",精度100%但速度慢,适合几万条以内的小数据,或者作为其他索引的"量化器"(比如IVF索引)。
- is_trained的意义 :有些复杂索引(如IVF、HNSW)需要先"训练"(用数据学习聚类中心或图结构),而Flat索引不需要训练,所以
is_trained直接返回True。 - add的作用:把向量库的数据加载到索引的内存中,后续检索都基于这个内存中的索引进行。
5.1.3. 第三步:执行TopK相似检索
索引构建完成后,我们就可以用查询向量来搜索最相似的TopK个结果了。
python
k = 4 # TopK的K值,即每个查询向量找最相似的4个结果
# 执行检索
# - 输入:xq(待检索向量)、k(TopK数量)
# - 输出:
# D:距离矩阵,形状为(nq, k),每一行对应一个查询向量的TopK距离(从小到大排列)
# I:索引矩阵,形状为(nq, k),每一行对应一个查询向量的TopK向量在原库中的索引
D, I = index.search(xq, k)
# 查看前5个查询向量的TopK索引
print(f"前5个查询向量的TopK索引: \n{I[:5]}")
# 查看后5个查询向量的TopK距离
print(f"前5个查询向量的TopK距离: \n{D[:5]}")
plain
前5个查询向量的TopK索引:
[[ 381 207 210 477]
[ 526 911 142 72]
[ 838 527 1290 425]
[ 196 184 164 359]
[ 526 377 120 425]]
前5个查询向量的TopK距离:
[[6.815506 6.8894653 7.3956795 7.4290257]
[6.6041145 6.679695 6.7209625 6.828682 ]
[6.4703865 6.8578568 7.0043793 7.036564 ]
[5.573681 6.4075394 7.1395187 7.3555984]
[5.409401 6.2322083 6.4173393 6.5743713]]
⚠️关键补充说明:
- 返回值D和I的含义 :
I[i][j]:第 i 个查询向量,第 个查询向量,第 个查询向量,第 j 相似的向量在原向量库xb中的索引(比如I[0][0]就是xq[0]最相似的向量在xb中的位置)。D[i][j]:第 i 个查询向量和 ' I \[ i \] \[ j \] ' 对应向量的距离( 个查询向量和\`I\[i\]\[j\]\`对应向量的距离( 个查询向量和'I\[i\]\[j\]'对应向量的距离( L_2 距离,值越小越相似)。
- 🤔𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:为什么打印
**I[:5]**和**D[-5:]**? - 🥳**𝑨𝒏𝒔𝒘𝒆𝒓:**这是示例中的常用做法,通过看前几个查询的索引和对应的距离,快速验证检索结果是否符合预期(比如因为我们加了人工区分度,
I[i][0]应该接近 i )。
要读懂这个结果,我们只需要抓住 **「I 是索引地图,D 是距离标尺,两者一一对应」**这个核心即可。下面结合输出具体拆解:
5.1.3.1. 先看核心规则回顾
在 D, I = index.search(xq, k) 的输出中:
- I(Index,索引矩阵) :形状为
(nq, k),每一行对应一个查询向量 的 TopK 结果,存的是这些相似向量在**原向量库 **xb中的下标 。- 顺序:从左到右是「最相似 → 第 K 相似」。
- D(Distance,距离矩阵) :形状和
I完全一样,每一行对应同一个查询向量的 TopK 距离,存的是**查询向量与 **I中对应向量的 L2 欧氏距离 。- 顺序:从左到右是「距离最小 → 距离最大」(因为 L2 距离越小,相似度越高)。
5.1.3.2. 逐行拆解输出
我们先看 I 矩阵(前5个查询的 TopK 索引):
plain
前5个查询向量的TopK索引:
[[ 381 207 210 477] ← 第1行:对应 xq[0](第1个查询向量)
[ 526 911 142 72] ← 第2行:对应 xq[1](第2个查询向量)
[ 838 527 1290 425] ← 第3行:对应 xq[2](第3个查询向量)
[ 196 184 164 359] ← 第4行:对应 xq[3](第4个查询向量)
[ 526 377 120 425]] ← 第5行:对应 xq[4](第5个查询向量)
具体含义(以第1行为例) :
对于第1个查询向量 xq[0]:
- 最相似的向量是原向量库中的
xb[381](Top1); - 第2相似的是
xb[207](Top2); - 第3相似的是
xb[210](Top3); - 第4相似的是
xb[477](Top4)。
再看 D 矩阵(前5个查询的 TopK 距离):
plain
前5个查询向量的TopK距离:
[[6.815506 6.8894653 7.3956795 7.4290257] ← 第1行:对应 xq[0] 的 TopK 距离
[6.6041145 6.679695 6.7209625 6.828682 ] ← 第2行:对应 xq[1] 的 TopK 距离
[6.4703865 6.8578568 7.0043793 7.036564 ] ← 第3行:对应 xq[2] 的 TopK 距离
[5.573681 6.4075394 7.1395187 7.3555984] ← 第4行:对应 xq[3] 的 TopK 距离
[5.409401 6.2322083 6.4173393 6.5743713]] ← 第5行:对应 xq[4] 的 TopK 距离
具体含义(以第1行为例,与I矩阵第一行一一对应) :
对于第1个查询向量 xq[0]:
- 它与 Top1 向量
xb[381]的 L2 距离是6.815506(最小,最相似); - 它与 Top2 向量
xb[207]的距离是6.8894653; - 它与 Top3 向量
xb[210]的距离是7.3956795; - 它与 Top4 向量
xb[477]的距离是7.4290257(最大,第4相似,因为我们取得是的top-4,所以这里也可以说是最不相似😂)。
5.1.3.3. 一句话总结
I[i][j]告诉我们"第i个查询向量的第j相似向量是谁"D[i][j]告诉我们"它们俩有多相似",两者是完全绑定的对应关系。
5.1.4. 关键疑问:为什么 Top1 不是 i?
不知道你注意到了没有:之前代码里给向量加了"人工区分度"(xb[:, 0] += np.arange(nb)/1000),理论上 xq[i] 最相似的应该是 xb[i],但结果里比如 xq[0] 的 Top1 是 381 而不是 0。
这是因为:
- 我们只给第0维 加了递增项,但向量总共有
d=64维,其他63维都是随机生成的; - 随机生成的其他维度的差异,可能盖过了第0维的微小递增(
/1000把递增幅度放得很小)。
这恰恰说明:Faiss 是基于全维度的向量距离来做检索的 ,不会只看某一维。如果你想验证"人工区分度"的效果,可以把递增幅度改大(比如 /10),或者把维度 d 改小(比如 d=2),你会发现 Top1 会越来越接近 i。
下面是我们直接将/1000删除掉后的结果:
plain
前5个查询向量的TopK索引:
[[2 0 1 3]
[2 1 0 3]
[2 3 4 1]
[4 3 2 1]
[4 3 6 5]]
前5个查询向量的TopK距离:
[[11.594097 13.573288 14.573832 17.609764 ]
[10.761787 11.1898 12.431873 13.264725 ]
[ 9.495632 11.294586 12.441505 12.987556 ]
[10.976418 11.352524 11.621376 14.538639 ]
[ 8.472389 11.337753 13.0731735 13.188042 ]]
没有单维度主导时,检索结果完全由全维度的随机差异决定,Top1 更 "乱"。
我们再将其改为*1000后看看结果:
plain
前5个查询向量的TopK索引:
[[0 1 2 3]
[1 2 0 3]
[2 3 1 4]
[3 2 4 1]
[4 5 3 6]]
前5个查询向量的TopK距离:
[[1.35732880e+01 9.99150438e+05 3.99704675e+06 8.99615400e+06]
[1.07500000e+01 9.98991500e+05 1.00079456e+06 3.99836500e+06]
[9.00000000e+00 9.98857000e+05 1.00074350e+06 3.99775400e+06]
[2.60000000e+01 9.99980000e+05 1.00027800e+06 3.99871400e+06]
[1.80000000e+01 9.99976000e+05 1.00145000e+06 3.99757600e+06]]
- Top1 都是
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">i</font>(比如<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">xq[0]</font>的 Top1 是<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">0</font>,<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">xq[1]</font>的 Top1 是<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">1</font>),说明当单维度差异足够大时,会主导检索结果 - 距离用科学计数法是因为
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">*1000</font>后第 0 维的差异非常大,导致整体 L2 距离被拉大
这组实验完美验证了:Faiss 是基于全维度的向量距离来做检索的,单维度的影响取决于它的差异幅度 ------ 差异小时被其他维度覆盖,差异大时会主导结果。
5.1.5. 动手验证一下是不是L2距离
我们可以拿 xq[0] 和 xb[381] 手动算一下 L2 距离,看看是不是和 D[0][0] 一致:
python
# 手动计算 xq[0] 和 xb[381] 的 L2 距离
manual_distance = np.linalg.norm(xq[0] - xb[381])
print(f"手动计算的L2距离: {manual_distance}")
print(f"Faiss返回的Top1距离: {D[0][0]}")
plain
手动计算的L2距离: 2.6106534004211426
Faiss返回的Top1距离: 6.8155059814453125
我们会发现这两个值几乎完全一样(可能有微小的浮点误差),这就验证了 Faiss 结果的正确性。
😮😮😮但是但是但是,这结果差的也太大了吧。别慌!这不是Faiss算错了,而是一个非常关键的细节差异 :Faiss的IndexFlatL2返回的是「平方欧氏距离」,而不是开根号后的欧氏距离。
所以我们有两种验证方法:
- 把Faiss返回的结果开个根号
- 把手动计算的结果平方
python
# 【方法1】取平方根
print(f"Faiss结果的平方根: {D[0][0] ** 0.5}")
print(f"平方根后的结果是否相近: {np.isclose(D[0][0] ** 0.5, manual_distance)}")
# 【方法2】取平方
print(f"手动计算结果的平方: {manual_distance ** 2}")
print(f"平方后的结果是否相近: {np.isclose(D[0][0], manual_distance ** 2)}")
你会发现输出是:
plain
Faiss结果的平方根: 2.6106524053280844
平方根后的结果是否相近: True
手动计算结果的平方: 6.815511177130475
平方后的结果是否相近: True
两种方法都证明两者完全一致!
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:为什么Faiss要这么设计?
🥳 𝑨𝒏𝒔𝒘𝒆𝒓:这是一个性能优化:
- 开根号是一个相对耗时的计算操作;
- 在做TopK检索时,平方距离的大小关系和欧氏距离是完全一样的(因为平方根是单调递增函数)------比如A的平方距离比B小,那么A的欧氏距离也一定比B小。
所以Faiss直接返回平方距离,既不影响TopK排序的正确性,又能提升检索速度,是非常聪明的设计。
6. 选择索引前的两个核心准备
在上两篇中,我们介绍了Faiss的基本概念、安装方法和Flat索引的核心流程。这一篇我们将聚焦Faiss最核心的6种索引类型------这是工业界选择Faiss时的"必选项",掌握它们能帮你根据实际场景快速选对索引。
在讲具体索引之前,我们先搞懂两个关键概念:召回率(Recall)和index_factory,这是后续学习的基础。
6.1. 什么是召回率?
Faiss能加速的核心原因是:大部分索引用的是"模糊检索"(近似检索),而非"精确,"(暴力检索)。
- 精确检索(Flat):能100%找到最相似的TopK向量,召回率=100%;
- 模糊检索(如IVF、PQ):通过牺牲一点点精度,大幅提升速度、节省内存。
召回率的大白话定义:假设精确检索能找到100个"真正最相似"的向量,模糊检索找到了其中的95个,那么召回率就是95%。
⚠️注意:Recall的特点是只管有没有统计到,意思是如果错的那么不管,只管统计了几个对的。举个🌰,总共有100张猫的图片,我们用 FAISS 检索,返回了 50 张照片,其中 40 张是猫(这是 "被成功找出来的真正相关样本数"),10 张是狗(这是 "找错的",召回率不管它),那么召回率就是40/100=40%
- Flat:就是字面意思,平坦的,意味着"扁平化、无层级、无压缩"
- IVF:Inverted File,源自文本检索的倒排思想。先通过 k-means 把向量聚成 N 个聚类中心(桶),建立「聚类中心→对应向量 ID」的倒排表;查询时只找最相似的少数几个桶,大幅缩小检索范围,实现检索加速。
- PQ:Product Quantization,乘积量化。这里的 Product 指 "笛卡尔积",Quantization 指 "量化",完全对应你学的 "向量分段压缩" 逻辑 ------ 把高维向量切分成 M 个低维子向量,对每个子向量单独做 k-means 聚类生成码本,用子向量对应的聚类中心 ID 代替原始子向量,最终用多个低维量化码的笛卡尔积表示原始高维向量,实现极致的内存压缩和检索加速
6.2. 统一用 index_factory 构建索引
Faiss的索引类型非常多,记不同的类名(如IndexFlatL2、IndexIVFFlat)很麻烦。**强烈建议统一用 **faiss.index_factory,它用字符串参数就能构建所有索引,简洁又统一。
6.2.1. 基本用法
python
import faiss
# 三个核心参数
dim = 64 # 向量维度
param = 'Flat' # 索引类型字符串(核心参数)
measure = faiss.METRIC_L2 # 距离度量方式(常用L2或内积)
# 构建索引
index = faiss.index_factory(dim, param, measure)
6.2.2. 距离度量方式说明
Faiss官方支持8种度量方式,最常用的只有2种,其他了解即可:
METRIC_L2:欧氏距离(最常用,距离越小越相似);METRIC_INNER_PRODUCT:内积(如果要算余弦相似度 ,先把向量归一化,再用内积即可------归一化后的内积等于余弦相似度)。
其他6种(L1、Linf、Lp、BrayCurtis、Canberra、JensenShannon)仅在特定场景使用,这里不展开。
7. Faiss核心索引类型详解
我们按"从简单到复杂、从精确到模糊"的顺序,介绍6种最核心的索引,每种都包含:原理、优缺点、适用场景、参数说明、代码示例。
7.1. Flat:暴力检索(精确检索)
7.1.1. 原理
就是我们上一篇讲的"暴力遍历所有向量计算距离",没有任何优化,100%精确。
✨注:虽然是暴力检索,但Faiss的Flat比自己写的快很多------因为底层用了C++和SIMD指令优化(单指令多数据,一次算多个向量的距离)。
7.1.2. 优缺点
| 优点 | 缺点 |
|---|---|
| 召回率100%(最准确) | 速度慢(数据量>100万时明显卡顿) |
| 不需要训练,直接add向量 | 占内存大(存所有原始向量) |
7.1.3. 适用场景
- 向量数量少(50万以内);
- 对精度要求极高(必须100%召回);
- 内存不紧张。
7.1.4. 代码示例
python
dim = 64 # 向量维度
construction_method = 'Flat' # 索引类型字符串(核心参数)
measure = faiss.METRIC_L2 # 距离度量方式(常用L2或内积)
# 构建索引
index = faiss.index_factory(dim, construction_method, measure)
print(f"构建的索引不需要训练:{index.is_trained}") # 输出True,不需要训练
print(f"当前索引中向量总数:{index.ntotal}") # 输出向量总数
# 随机创建一个向量
import numpy as np
xb = np.random.random((1, dim)).astype("float32")
# 直接添加新向量
index.add(xb) # 直接添加向量
print(f"添加新向量后索引中向量总数:{index.ntotal}") # 输出向量总数
⚠️注意:FAISS构建的
index是向量数据库的核心检索引擎,仅负责内存中的向量存储与快速相似度检索,不具备数据持久化、元数据管理、分布式部署等完整数据库功能。
7.2. IVFx Flat:倒排暴力检索(分桶+精确)
IVFx Flat:Inverted File Flat Index (也常表述为 Inverted File Index with Flat Quantization)。其中:
- IVF = Inverted File(倒排文件,对应"分桶检索"逻辑);
- x = 聚类中心的个数(即"桶"的数量);
- Flat = 桶内使用 Flat 索引(暴力检索,保证桶内精度)。
7.2.1. 原理
想象我们在图书馆找书:
- Flat索引:把所有书翻一遍找
- IVFx Flat索引 :先把书按主题分成
x个"书架"(聚类中心),查询时先找最近的几个书架,再在这几个书架里翻书找。
核心逻辑 :用k-means把向量聚成x个"桶",查询时只查最近的nprobe个桶,不用查所有桶。
7.2.2. nprobe参数
nprobe 是 number of probes 的缩写,字面意思是"探测的数量",在IVF索引中具体指查询时需要探测(搜索)的最近聚类中心(桶)的数量。
它是IVF索引最重要的调优参数:
index.nprobe = 1(默认):只查1个最近的桶,速度最快但召回率最低;index.nprobe = 10:查10个最近的桶,召回率提升但速度稍降;
一般调优到"召回率满足需求,速度能接受"即可。
7.2.3. 优缺点
| 优点 | 缺点 |
|---|---|
| 速度比Flat快10-100倍 | 速度还不是最快(百万级以上仍有压力) |
| 召回率较高(调nprobe可接近Flat) | 仍占内存大(存所有原始向量) |
| 需要训练(用k-means聚类) | - |
7.2.4. 适用场景
- 向量数量百万级别;
- 对精度和速度都有要求;
- 内存不紧张。
7.2.5. 参数说明
IVFx中的x:k-means聚类中心的个数(即"桶"的数量),一般取100~10000(数据量越大,x越大)。
7.2.6. 代码示例
python
import numpy as np
dim = 64 # 向量维度
construction_method = "IVF100,Flat" # 100个聚类中心,桶内用Flat精确检索
measure = faiss.METRIC_L2
# 构建索引
index = faiss.index_factory(dim, construction_method, measure)
print(f"构建的索引不需要训练:{index.is_trained}") # 输出False,需要先训练
# 随机创建向量
xb = np.random.random((102400, dim)).astype("float32")
# 用向量库训练k-means
index.train(xb)
index.add(xb) # 训练后再添加向量
# 调优nprobe(可选,根据需求调整)
index.nprobe = 10
python
构建的索引不需要训练:False
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:为什么要先训练再添加向量?那训练了个啥呀?
🥳 𝑨𝒏𝒔𝒘𝒆𝒓:IVF的训练是"给图书馆分书架",只有先分好书架,后续的书(向量)才知道往哪放,查询时才知道去哪找。我们用图书馆的类比讲清楚:
一、为什么IVF必须先训练?
IVF的核心是"分桶检索",但分桶的前提是得先有"桶"(聚类中心)------没有桶的话,新向量来了不知道往哪个桶里存,查询时也不知道找哪个桶,所以必须先训练"造桶"。
二、IVF训练到底"训练了个啥"?
用你代码里的
IVF100,Flat举例子:训练的过程就是用k-means算法,把你提供的训练向量(xb)聚成100个"聚类中心"------这100个中心就是100个"桶的标签"(或者说图书馆的100个"书架位置")。
训练的输出:就是这100个64维的聚类中心,后续所有操作都围绕这100个中心展开。
三、训练后add向量做了啥?
有了100个"书架",add向量时就不是随便存了,而是:
- 对每个新向量,计算它和100个聚类中心的距离;
- 把这个向量放到距离最近的那个聚类中心对应的桶里;
- 建立一张「倒排表」:记录每个聚类中心(桶)里都有哪些向量ID。
这样查询时,只要先找最近的
nprobe个聚类中心,再查这几个中心对应的倒排表就行,不用查所有向量。四、一句话总结
IVF的训练是用k-means生成N个聚类中心(桶),只有先有了桶,后续的向量才能被分到对应的桶里,查询时才能通过"先找桶再找向量"实现加速。
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:那如果index.add的向量不是xb会怎么样?需要重新训练再添加新的向量吗?🥳 𝑨𝒏𝒔𝒘𝒆𝒓:这是个非常好的工程实践问题!我们分两部分用大白话+例子讲清楚:
一、如果
index.add的向量不是训练用的xb,会怎么样?完全没问题,可以正常工作!
IVF训练的核心是**"用训练向量(xb)学习数据的分布,生成聚类中心(桶)"**------它不是"记住xb的具体向量",而是"学会怎么给新向量分桶"。
只要你后续add的新向量和训练用的xb**"分布相似"**(比如都是猫的图片特征、都是同一类文本的语义向量),新向量就能被正确分到之前生成的桶里,检索也能正常进行。
用图书馆类比:训练是"根据旧书的主题分好100个书架",后续来的新书(不是旧书)只要主题和旧书差不多,就能直接放到对应的书架上,完全没问题。
二、需要重新训练再添加新的向量吗?
分两种情况:
1. 不需要重新训练(90%的增量场景)
如果新向量和训练用的xb分布相似 (比如都是电商商品的图片特征,只是新增了一批商品),直接
index.add(new_xb)就行,不用重新训练。注意 :FAISS的IVF索引支持分批增量添加向量------你可以先训练一次,然后分多次add新向量,不用每次都重新训练。
2. 需要重新训练(数据分布发生大变化)
如果新向量和训练用的xb分布差异极大(比如训练用的是"猫的图片",新向量是"汽车的图片";或者训练用的是"2020年的新闻文本",新向量是"2025年的科技论文"),这时候原来的聚类中心(桶)就不适合新向量了,会导致:
- 新向量被分到错误的桶里;
- 检索召回率大幅下降。
这种情况下,你需要把旧向量和新向量混合在一起,重新训练索引。
7.2.7. 三、一句话总结
只要新向量和训练数据分布相似,直接add就行,不用重新训练;如果数据分布发生大变化,才需要用混合数据重新训练。
7.3. PQx:乘积量化(压缩+模糊)
PQx:Product Quantization with x subvectors
在 FAISS 的官方体系中,PQx的 x 固定指子向量分段数
7.3.1. 原理
想象我们要存很多长句子,直接存太占内存。PQx的做法:把长句子切成x段,每段用一个"小字典"里的短词代替,存短词的索引而不是原句子------这样能大幅节省内存。
核心逻辑 :把向量切成x段,每段做量化(用聚类中心代替原向量段),存储量化后的索引,查询时用索引快速计算近似距离。
7.3.2. 优缺点
| 优点 | 缺点 |
|---|---|
| 内存占用极小(是Flat的1/10~1/100) | 召回率下降较多(比Flat低5%~20%) |
| 速度很快 | - |
| 需要训练(每段都要做k-means) | - |
7.3.3. 适用场景
- 内存极其稀缺;
- 需要较快的检索速度;
- 对召回率要求不那么高。
7.3.4. 参数说明
PQx中的x:向量切分的段数,必须能被向量维度整除(比如d=64,x可以是8、16、32);x越大,切分越细致,召回率越高但速度越慢。
7.3.5. 代码示例
python
import numpy as np
dim = 64 # 向量维度
construction_method = "PQ16" # 把向量切成16段(64/16=4,每段4维)
measure = faiss.METRIC_L2
# 构建索引
index = faiss.index_factory(dim, construction_method, measure)
print(f"构建的索引不需要训练:{index.is_trained}") # 输出False,需要先训练
# 随机创建向量
xb = np.random.random((102400, dim)).astype("float32")
# 用向量库训练PQ
index.train(xb)
index.add(xb) # 训练后再添加向量
plain
构建的索引不需要训练:False
7.4. IVFxPQy:倒排乘积量化(分桶+压缩,工业界最常用)
7.4.1. 原理
这是IVF和PQ的结合体,集两家之长:
- 先用IVF把向量分成x个桶(减少搜索范围);
- 再用PQ把每个向量切成y段压缩(节省内存)。
工业界90%以上的超大规模向量检索场景,用的都是这个索引!
7.4.2. 优缺点
| 优点 | 缺点 |
|---|---|
| 速度快、内存省、召回率可接受(综合性能最好) | 没有极端突出的单项性能(但综合最均衡) |
| 适合超大规模数据(亿级以上) | 需要训练(IVF和PQ都要训练) |
7.4.3. 适用场景
- 超大规模数据(亿级以上);
- 对速度、内存、召回率都有要求(无极端需求);
- 工业界生产环境(推荐系统、图像搜索、语义搜索等)。
7.4.4. 参数说明
IVFx中的x:聚类中心个数(同IVF);PQy中的y:向量切分段数(同PQ,必须能被维度整除)。
7.4.5. 代码示例
python
import numpy as np
dim = 64 # 向量维度
construction_method = "IVF100,PQ16" # 把向量分为100个聚类中心,每个聚类中心用16段PQ编码
measure = faiss.METRIC_L2
# 构建索引
index = faiss.index_factory(dim, construction_method, measure)
print(f"构建的索引不需要训练:{index.is_trained}") # 输出False,需要先训练
# 随机创建向量
xb = np.random.random((102400, dim)).astype("float32")
# 用向量库训练PQ
index.train(xb)
index.add(xb) # 训练后再添加向量
index.nprobe = 10 # 调优nprobe(可选,根据需求调整)
plain
构建的索引不需要训练:False
7.5. LSH:局部敏感哈希
7.5.1. 原理
想象你给相似的照片贴标签:
- LSH的做法:设计一种"标签规则",让相似的照片大概率贴一样的标签,不相似的照片大概率贴不一样的标签;查询时只查贴了相同标签的照片。
核心逻辑:用局部敏感哈希函数把向量映射到"哈希桶"里,相似的向量大概率在同一个桶,查询时只查同一个桶。
7.5.1.1. 优缺点
| 优点 | 缺点 |
|---|---|
| 训练极快 | 召回率非常低(比PQ还低) |
| 占内存很小 | - |
| 检索较快 | - |
7.5.1.2. 适用场景
- 候选向量库非常大(十亿级以上);
- 离线检索(对实时性要求不高);
- 内存资源极其稀缺;
- 对召回率要求很低。
7.5.1.3. 代码示例
python
import numpy as np
dim = 64 # 向量维度
construction_method = "LSH"
measure = faiss.METRIC_L2
# 构建索引
index = faiss.index_factory(dim, construction_method, measure)
print(f"构建的索引不需要训练:{index.is_trained}") # 输出False -> 不需要训练
# 随机创建向量
xb = np.random.random((102400, dim)).astype("float32")
# 直接添加向量即可
index.add(xb)
plain
构建的索引不需要训练:True
7.6. 🌟HNSWx:基于图的检索(速度与精度的极致平衡,最重要!)
HNSWx:Hierarchical Navigable Small World,分层可导航小世界
x 是 FAISS index_factory 接口的简写参数,严格对应 HNSW 原论文和 FAISS 源码中的 M 参数,即构建图时每个节点每层的最大连接数。
7.6.1. 原理
想象你在城市里找路:
- 普通索引:从起点一步步走到终点;
- HNSWx的做法:建"多层级导航图"------上层是"快速通道"(只有少数关键节点),下层是"详细路径"(所有节点);查询时先在上层快速定位到附近的关键节点,再到下层精确查找。
核心逻辑:构建分层的小世界图,查询时间复杂度为 O(\\log\\log n) ,几乎无视数据量级!
7.6.2. 优缺点
| 优点 | 缺点 |
|---|---|
| 检索速度极快(10亿级毫秒级出结果) | 构建索引极慢(比其他索引慢10-100倍) |
| 召回率极高(最高可达97%,接近Flat) | 占用内存极大(是Faiss中最大的,大于原向量内存) |
| 支持分批导入向量 | - |
| 时间复杂度loglogn,无视数据量级 | - |
7.6.3. 适用场景
- 不在乎内存;
- 有充裕的时间构建索引;
- 需要极致的检索速度和高召回率;
- 线上实时任务(毫秒级响应)。
7.6.4. 参数说明
HNSWx中的x:构建图时每个节点最多连接的边数,一般取4~64(常用16、32、64);- x越大,图越复杂,查询越精确,但构建越慢、占内存越大。
7.6.5. 代码示例
python
import numpy as np
dim = 64 # 向量维度
construction_method = "HNSW64" # 每个节点最多连64条边
measure = faiss.METRIC_L2
# 构建索引
index = faiss.index_factory(dim, construction_method, measure)
print(f"构建的索引不需要训练:{index.is_trained}")
# 随机创建向量
xb = np.random.random((102400, dim)).astype("float32")
# HNSWx的特点是不需要训练,训练是在add时进行
index.add(xb)
plain
构建的索引不需要训练:True
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:HNSW是不需要训练的,但在add时会计算还是需要训练?
🥳 𝑨𝒏𝒔𝒘𝒆𝒓:这是个非常关键的概念区分问题!
一、先明确FAISS里"训练(train)"的定义
在FAISS的体系中,"训练(调用
index.train())"特指「用一批数据预先生成全局的、固定的索引结构」------比如:
- IVF的训练是"预先生成100个聚类中心(桶)";
- PQ的训练是"预先生成16本小字典(每段的聚类中心)"。
这些全局结构一旦生成,后续add新向量时就不再改变,只需要按这个结构分桶/压缩就行。
二、HNSW的情况:不需要
train(),但add()时会做"图构建计算"HNSW的
index.is_trained返回True,意思是它不需要"预先生成全局固定结构"的train()步骤 ------但这不代表它什么都不做,它的"索引构建"是在add()时增量完成的:每
add一个新向量,HNSW内部会做这些计算:
- 计算新向量和已有部分节点的距离;
- 从顶层图开始,通过贪心搜索找到新向量在每一层的"插入位置";
- 为新向量在每一层选择并连接最多
x(即你代码里的64)个邻居节点;- 维护图的连通性(如果需要,会调整旧节点的连接)。
这个过程是增量的、边加边做的 ,不需要先拿一批数据跑
train(),所以FAISS认为它"不需要训练"。三、用之前的图书馆类比总结
- IVF :先根据旧书的主题分好100个书架(
train()),再把旧书、新书放到对应的书架上(add());- HNSW :不用先分书架,直接边放书边给每本书搭"导航路标"(
add()时直接构建图),放完书导航图也就建好了。四、一句话总结
HNSW不需要调用FAISS的
index.train()方法 (is_trained为True),但add()时会做"分层图结构的增量构建计算"------这不是FAISS定义的"训练",是它特有的索引构建方式。
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:按照HNSW的逻辑,是全量向量都会加载到内存中,并且还有顶层图的存在,所以内存占用还会更大吗?🥳 𝑨𝒏𝒔𝒘𝒆𝒓:你的理解完全正确 !HNSW 确实是 FAISS 常用索引中内存占用最大的 ------核心原因就是它不仅要存全量原始向量 (和 Flat 一样),还要额外存复杂的分层图连接信息(这部分是 Flat 没有的)。
我们用具体的内存构成拆解+对比讲清楚:
HNSW 的内存由两部分组成
1. 原始向量存储(和 Flat 完全一样)
这部分是"必选项",HNSW 为了保证高召回率,不会对原始向量做压缩(不像 PQ 那样切分量化),必须完整存所有向量的原始数据。
- 内存开销:
nb × dim × 4 字节(float32 每个维度占 4 字节)- 比如你代码里的 102400 条 64 维向量:
102400 × 64 × 4 ≈ 25MB2. 分层图的边连接信息(HNSW 独有,内存大的核心原因)
这部分是"额外开销",HNSW 要存每个节点在每一层的邻居连接关系 ------也就是你代码里
HNSW64中的"64 条边"对应的存储。
- 内存开销:主要由
M参数(即你代码里的x,每个节点每层的最大连接数)和层数决定
- 层数:随数据量
nb呈O(log n)增长(比如 100 万条向量大概 5-7 层,10 亿条大概 10-12 层)- 每个节点的总边数:大概是
M × (层数 + 1)(底层边数多,上层边数少)- 举个具体例子(HNSW64,100 万条向量):
- 假设平均每个节点有 80 条边,每条边存一个 4 字节的节点 ID
- 边的内存开销:
100万 × 80 × 4 ≈ 320MB- 加上原始向量的 250MB(100万×64×4),总内存是 570MB,是 Flat 的 2 倍多
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:这么理解对不对,Faiss构建的索引是在内存中,不会持久化,我们现在说的这些都是基于内存空间进行操作的,我的理解正确吗?🥳 𝑨𝒏𝒔𝒘𝒆𝒓:Faiss 索引默认在内存中操作、无自动持久化;但它支持手动将索引保存到硬盘文件,下次再加载回内存使用。
7.7. 核心索引对比表
| 索引类型 | 精度(召回率) | 速度 | 内存占用 | 适用数据量 | 是否需要训练 | 推荐场景 |
|---|---|---|---|---|---|---|
| Flat | 100% | 慢 | 大 | <50万 | 否 | 小数据、高精度 |
| IVFx Flat | 高(可调nprobe) | 中 | 大 | 百万级 | 是 | 百万级、精度速度均衡 |
| PQx | 中 | 快 | 极小 | 千万级 | 是 | 内存稀缺、召回率要求不高 |
| IVFxPQy | 中高 | 快 | 小 | 亿级以上 | 是 | 工业界生产环境(最推荐) |
| LSH | 低 | 快 | 极小 | 十亿级 | 否 | 超大规模离线、低召回 |
| HNSWx | 极高(~97%) | 极快 | 极大 | 任意量级 | 否 | 线上实时、高召回、不在乎内存 |
8. Faiss的实际应用场景
我们学的所有索引、train/add/search 操作,核心只解决一个行业级的刚需:在百万 / 亿 / 十亿级高维向量中,毫秒级找到和查询向量最相似的 TopK 结果。下面是4个最经典、我们每天都在接触的落地场景,瞬间就能让我们明白它的价值:
- 以图搜图(淘宝拍立淘、百度识图):把商品 / 图片转成 512/1024 维的特征向量,用 HNSW/IVF_PQ 建索引,用户上传图片时,检索库里最相似的图片,我们学的「索引速度 / 召回率平衡」就是这里的核心选型逻辑。
- RAG 检索增强生成(大模型知识库):现在最火的大模型问答场景,把 PDF / 文档 / 笔记切成片段,转成语义向量,用 Faiss 建索引;用户提问时,先检索和问题最相关的文档片段,再喂给大模型生成答案,Faiss 就是整个系统的 "大脑检索中枢"。
- 推荐系统召回阶段:抖音 / 淘宝的推荐,会给用户生成一个 "兴趣向量",给商品 / 内容也生成向量,用 Faiss 在亿级商品库中快速检索出和用户兴趣最匹配的几百个商品,再做精排,我们学的 IVF_PQ 就是工业界推荐系统最常用的索引。
- 本地语义搜索:给我们自己的几百篇笔记、文档建索引,不用记关键词,直接搜 "我之前写的关于 XX 项目的复盘",就能找到语义匹配的内容,这是我们 1 小时就能跑通的最小落地场景。
9. Faiss实战案例
9.1. 案例1
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
model = SentenceTransformer('all-MiniLM-L6-v2')
plain
/home/leovin/anaconda3/envs/llm/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
Loading weights: 100%|██████████| 103/103 [00:00<00:00, 3795.88it/s]
BertModel LOAD REPORT from: sentence-transformers/all-MiniLM-L6-v2
Key | Status | |
------------------------+------------+--+-
embeddings.position_ids | UNEXPECTED | |
Notes:
- UNEXPECTED :can be ignored when loading from different task/architecture; not ok if you expect identical arch.
python
# 2. 准备测试数据(模拟我们的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
python
# 3. 把文本转成384维向量(对应我们之前学的dim参数)
dim = 384
doc_vectors = model.encode(docs).astype("float32")
python
# 4. 使用HNSW构建索引
index = faiss.index_factory(dim, "HNSW32", faiss.METRIC_L2)
# 因为HNSW的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
plain
索引构建完成,向量总数:8
python
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些,分别有什么特点?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
python
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些,分别有什么特点?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 检索Top3最相似的文本
k = 3
D, I = index.search(query_vector, k)
python
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
plain
我们的查询是: Faiss的索引类型有哪些,分别有什么特点?
在索引中检索到的最相关的内容为(top 3):
排名 1: 距离=0.7001, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
排名 2: 距离=0.8114, 文档ID=0, 内容=Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索
排名 3: 距离=0.9050, 文档ID=2, 内容=PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库
我们逐个看 Top3 的文档,会发现它们都和查询 "Faiss 的索引类型有哪些,分别有什么特点" 有语义关联,这就是 Faiss + 语义模型的核心价值:
- Top1(文档 6) :内容是 "向量数据库的核心是向量检索引擎,很多底层基于 Faiss 实现"
- 虽然没有直接列索引类型,但提到了 "Faiss 作为底层引擎",和查询的 "Faiss 索引" 语义高度相关,所以距离最小。
- Top2(文档 0) :内容是 "Faiss 是 Meta 开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索"
- 直接提到了 "Faiss 支持多种索引类型",是查询的核心关键词之一,距离稍大但也非常接近。
- Top3(文档 2) :内容是 "PQ 乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库"
- 具体讲了 "PQ 索引的特点",正好是查询问的 "索引类型特点" 之一,所以也被检索出来了。
这说明:我们用的 <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">all-MiniLM-L6-v2</font> 语义模型把文本转成了有意义的高维向量 ,而 Faiss 的 HNSW 索引确实能快速找到语义相似的向量,两者结合实现了 "按语义搜内容",而不是简单的关键词匹配。
9.2. 案例2:加入正确答案
为了证明Faiss的特点,我们在上面的程序中直接把答案加进入,看看答案是距离是否是最短的,Faiss不是在瞎找:
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
model = SentenceTransformer('all-MiniLM-L6-v2')
# 2. 准备测试数据(模拟你的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
docs.append("Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案1
docs.append("Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案2
docs.append("Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",) # 正确答案3
# 3. 把文本转成384维向量(对应我们之前学的dim参数)
dim = 384
doc_vectors = model.encode(docs).astype("float32")
# 4. 使用HNSW构建索引
index = faiss.index_factory(dim, "HNSW32", faiss.METRIC_L2)
# 因为HNSW的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 检索Top3最相似的文本
k = 5
D, I = index.search(query_vector, k)
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
改动:
- query变了
- docs也变了,添加了3个正确答案
- top-k也变量
我们看一下结果:
python
索引构建完成,向量总数:11
我们的查询是: Faiss的索引类型有哪些?
在索引中检索到的最相关的内容为(top 5):
排名 1: 距离=0.7220, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
排名 2: 距离=0.8205, 文档ID=8, 内容=Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 3: 距离=0.8582, 文档ID=0, 内容=Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索
排名 4: 距离=0.8977, 文档ID=9, 内容=Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 5: 距离=0.9541, 文档ID=4, 内容=Flat索引是暴力检索,召回率100%,适合小数据量场景
我们可以发现,结果与我们预期的不符!
这种 "预期不符" 非常正常!不是代码或 Faiss 出了问题,而是有两个非常核心的「语义模型特性」和「小数据量特性」在影响结果:
- 核心原因 1: 语义模型的 "语义关联"≠"关键词完全匹配":我们用的
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">all-MiniLM-L6-v2</font>是一个轻量级通用语义模型 ,它的判断逻辑是 "整体语义的相似性",而不是 "关键词的完全重合":- Top1(文档 6):内容是 "向量数据库的核心是向量检索引擎,很多底层基于 Faiss 实现"------ 在模型看来,"Faiss 作为检索核心" 和 "Faiss 的索引类型" 在 "Faiss 的核心作用" 这个语义上高度相关,所以距离最小;
- Top2/4(文档 8/9):虽然关键词完全重合,但模型认为它们的 "整体语义关联强度" 稍弱一点,所以排到了后面。
这是轻量级语义模型的常见特性,不是错误。
- 核心原因 2: 数据量太小,向量分布的细微差异被放大。我们只有 11 条向量,数据量太小:
- 向量之间的 L2 距离差异非常细微(都在 0.7~0.95 之间);
- 模型的一点点 "语义偏好"、HNSW 图构建的细微偏差,都会导致排名波动;
- 如果数据量变大(比如 1000 条),这种细微差异会被稀释,关键词更匹配的文档会更稳定地排前面。
9.3. 案例3:换更大的编码模型
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
model = SentenceTransformer('all-mpnet-base-v2')
# 2. 准备测试数据(模拟你的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
docs.append("Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案1
docs.append("Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案2
docs.append("Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",) # 正确答案3
# 3. 把文本转成768维向量(对应我们之前学的dim参数)
doc_vectors = model.encode(docs).astype("float32")
# 4. 使用HNSW构建索引
dim = doc_vectors.shape[1] # 新增:自动获取向量的真实维度
index = faiss.index_factory(dim, "HNSW32", faiss.METRIC_L2)
# 因为HNSW的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 检索Top3最相似的文本
k = 5
D, I = index.search(query_vector, k)
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
python
索引构建完成,向量总数:11
我们的查询是: Faiss的索引类型有哪些?
在索引中检索到的最相关的内容为(top 5):
排名 1: 距离=0.6670, 文档ID=0, 内容=Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索
排名 2: 距离=0.7891, 文档ID=8, 内容=Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 3: 距离=0.8347, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
排名 4: 距离=0.9236, 文档ID=9, 内容=Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 5: 距离=0.9801, 文档ID=7, 内容=RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成
和我们第一次用 <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">all-MiniLM-L6-v2</font> 的结果相比,这次的结果明显更 "贴题":
- Top1(文档 0):内容是 "Faiss 是 Meta 开源的向量检索库,支持多种索引类型"------ 虽然没直接列索引,但它是查询的 "核心语境引入",语义模型认为 "先确认 Faiss 有多种索引" 是回答问题的第一步,整体语义关联最强;
- Top2(文档 8):直接列了 "Faiss 的核心索引有 Flat、IVFx Flat..."------ 这是你预期的 "正确答案",已经排到了第二,比之前的结果进步很大;
- Top3-5:虽然还有文档 6(向量数据库),但整体都围绕 "Faiss 的核心作用 / 相关技术" 展开,没有完全跑题的内容。
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:为什么 "正确答案" 没排第一?
🥳 𝑨𝒏𝒔𝒘𝒆𝒓:这是通用语义模型的 "语境优先" 特性,不是错误:
- 我们用的
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">all-mpnet-base-v2</font>是一个 "通用语义理解模型",它的判断逻辑是「整体语境的连贯性」,而不是「关键词的完全重合」; - 在模型看来,查询 "Faiss 的索引类型有哪些?" 是一个 "关于 Faiss 的问题",文档 0 "Faiss 支持多种索引类型" 是这个问题的 "自然承接和前提",比 "直接列答案" 的文档 8 在 "整体语义流" 上更连贯,所以排第一;
- 这就像你问别人 "今天吃什么?",对方先回答 "今天我们吃火锅"(承接语境),再列 "毛肚、鸭肠、牛肉"(具体内容),在语义上是更顺的。
9.4. 案例4:加入更多数据量
我们有一个凡人8万字大纲的.txt文件,我们读取以增加索引的数据量,并使用小模型看看效果:
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
model = SentenceTransformer('all-MiniLM-L6-v2')
# model = SentenceTransformer('all-mpnet-base-v2')
# 2. 准备测试数据(模拟你的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
docs.append("Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案1
docs.append("Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案2
docs.append("Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",) # 正确答案3
def read_txt_file(file_path):
"""读取txt文件内容并返回文本列表"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
# 按行读取并去除空行和首尾空白字符
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"错误:文件 {file_path} 不存在")
return []
except Exception as e:
print(f"读取文件时发生错误:{e}")
return []
docs.extend(list(set(read_txt_file("小说8万字大纲.txt"))))
print(f"{docs[0:10] = }")
# 3. 把文本转成向量
doc_vectors = model.encode(docs).astype("float32")
# 4. 使用HNSW构建索引
dim = doc_vectors.shape[1] # 新增:自动获取向量的真实维度
index = faiss.index_factory(dim, "HNSW32", faiss.METRIC_L2)
# 因为HNSW的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 检索Top3最相似的文本
k = 10
D, I = index.search(query_vector, k)
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
python
docs[0:10] = ['Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索', 'IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心', 'PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库', 'HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景', 'Flat索引是暴力检索,召回率100%,适合小数据量场景', 'Python是一门解释型编程语言,广泛用于机器学习、数据分析领域', '向量数据库的核心是向量检索引擎,很多底层基于Faiss实现', 'RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成', 'Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW', 'Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW']
索引构建完成,向量总数:3197
我们的查询是: Faiss的索引类型有哪些?
在索引中检索到的最相关的内容为(top 10):
排名 1: 距离=0.3579, 文档ID=2928, 内容=对七玄门的危难也有责任感。
排名 2: 距离=0.4440, 文档ID=62, 内容=降尘丹:具有增加结丹几率的灵丹。
排名 3: 距离=0.4446, 文档ID=103, 内容=料。此种飞针遁速奇快、若有若无,是偷袭的最佳利器。
排名 4: 距离=0.4830, 文档ID=557, 内容=灵暝决:此功法有较强的预感效果。
排名 5: 距离=0.4955, 文档ID=1010, 内容=轮回,有阴煞之气入体反噬的危险。
排名 6: 距离=0.5144, 文档ID=59, 内容=色的雾气戏耍。
排名 7: 距离=0.5188, 文档ID=1856, 内容=药园的职务。
排名 8: 距离=0.5192, 文档ID=1470, 内容=两只巨大的鱼鳍。
排名 9: 距离=0.5193, 文档ID=1709, 内容=【七级妖兽】相当于结丹后期,具有妖丹。
排名 10: 距离=0.5269, 文档ID=120, 内容=借体操纵的法术。
这个结果完全是「数据严重不平衡」+「未做向量归一化」+「HNSW 在小比例相关数据下的特性」共同导致的,不是 Faiss 或语义模型出了问题,我们拆解清楚原因:
- 最主要原因:数据严重不平衡,相关向量被 "淹没"
- 我们的数据里,Faiss 相关的只有 11 条 ,小说干扰数据有 3186 条 ,比例是
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">1:290</font>; - 在高维向量空间里,3000 多条小说向量占了绝对主导,HNSW 构建图时,大部分连接都是小说向量之间的,Faiss 相关的 11 条向量被 "孤立" 了;
- 查询时,HNSW 的贪心搜索先碰到了小说向量,就顺着小说的连接走了,根本没机会找到那 11 条 Faiss 相关的向量。
- 次要原因 :未做向量归一化,L2 距离受向量长度影响。我们用的是
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">faiss.METRIC_L2</font>(欧氏距离),但没有对向量做归一化:
- 欧氏距离不仅看 "向量方向的相似性",还看 "向量长度的差异";
- 小说文本的长度、语义模型对不同文本的编码长度可能有差异,导致小说向量和查询向量的 L2 距离看起来 "更小",但其实语义完全不相关。
- 辅助原因 :HNSW 的 "贪心搜索" 在小比例相关数据下容易走偏:
- HNSW 是基于分层图的贪心搜索,如果相关向量在图里的连接不好(因为数据太少),贪心搜索很容易一开始就走到错误的分支(小说向量),再也回不来了。
9.5. 案例5:使用Flat暴力检索
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
model = SentenceTransformer('all-MiniLM-L6-v2')
# model = SentenceTransformer('all-mpnet-base-v2')
# 2. 准备测试数据(模拟你的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
docs.append("Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案1
docs.append("Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案2
docs.append("Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",) # 正确答案3
def read_txt_file(file_path):
"""读取txt文件内容并返回文本列表"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
# 按行读取并去除空行和首尾空白字符
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"错误:文件 {file_path} 不存在")
return []
except Exception as e:
print(f"读取文件时发生错误:{e}")
return []
docs.extend(list(set(read_txt_file("小说8万字大纲.txt"))))
print(f"{docs[0:10] = }")
# 3. 把文本转成向量
doc_vectors = model.encode(docs).astype("float32")
# 4. 使用HNSW构建索引
dim = doc_vectors.shape[1] # 新增:自动获取向量的真实维度
index = faiss.index_factory(dim, "Flat", faiss.METRIC_L2)
# 因为HNSW的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 检索Top3最相似的文本
k = 100
D, I = index.search(query_vector, k)
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
python
索引构建完成,向量总数:3197
我们的查询是: Faiss的索引类型有哪些?
在索引中检索到的最相关的内容为(top 100):
排名 1: 距离=0.3579, 文档ID=61, 内容=对七玄门的危难也有责任感。
...
排名 73: 距离=0.7220, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
...
排名 100: 距离=0.7471, 文档ID=403, 内容=凝元功:普通法决,能加快修炼时聚集灵气的速度。
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:为什么只有文档 6 排 73,其他 10 条没进 Top100?
🥳 𝑨𝒏𝒔𝒘𝒆𝒓:这个结果包含两个关键信息,完全符合我们的预判:
- Flat 确实能找到相关文档:证明不是 Faiss 或语义模型的问题,是数据不平衡 + 向量空间分布的问题;
- 只有文档 6 排 73,其他 10 条更深 :这里有一个新的「语义模型编码特性」在起作用:
- 文档 6 :内容是 "向量数据库的核心是向量检索引擎,很多底层基于 Faiss 实现"------ 它是一个 "通用语义陈述句",和小说里的 "修仙设定陈述句"(比如 "对七玄门的危难也有责任感")在向量空间里的差异更大一点,所以能 "浮" 到 Top100;
- 其他 10 条 Faiss 文档 :比如 "Faiss 的核心索引有 Flat、IVFx Flat..."------ 它们是**"孤立的、无上下文的技术知识点短句"**,语义模型对这种 "定义式短句" 的编码,和小说里的 "修仙定义短句"(比如 "降尘丹:具有增加结丹几率的灵丹")在向量空间里的距离更近,因为它们都是 "X 是 Y" 的结构,所以被淹没得更深。
9.6. 案例6:对向量做归一化,排除向量长度的干扰
余弦相似度只看 "向量方向的相似性",不看长度,对语义匹配更友好。
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
model = SentenceTransformer('all-MiniLM-L6-v2')
# model = SentenceTransformer('all-mpnet-base-v2')
# 2. 准备测试数据(模拟你的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
docs.append("Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案1
docs.append("Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案2
docs.append("Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",) # 正确答案3
def read_txt_file(file_path):
"""读取txt文件内容并返回文本列表"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
# 按行读取并去除空行和首尾空白字符
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"错误:文件 {file_path} 不存在")
return []
except Exception as e:
print(f"读取文件时发生错误:{e}")
return []
docs.extend(list(set(read_txt_file("小说8万字大纲.txt"))))
print(f"{docs[0:10] = }")
# 3. 把文本转成向量
doc_vectors = model.encode(docs).astype("float32")
# 🌟 归一化文本向量
faiss.normalize_L2(doc_vectors)
# 4. 构建索引+内积度量(对应余弦相似度)
dim = doc_vectors.shape[1] # 新增:自动获取向量的真实维度
index = faiss.index_factory(dim, "Flat", faiss.METRIC_INNER_PRODUCT)
# 因为 Flat 索引的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 🌟 归一化查询向量
faiss.normalize_L2(query_vector)
# 检索Top3最相似的文本
k = 100
D, I = index.search(query_vector, k)
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
plain
索引构建完成,向量总数:3197
我们的查询是: Faiss的索引类型有哪些?
在索引中检索到的最相关的内容为(top 100):
排名 1: 距离=0.8211, 文档ID=256, 内容=对七玄门的危难也有责任感。
......
排名 73: 距离=0.6390, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
......
排名 100: 距离=0.6265, 文档ID=1543, 内容=凝元功:普通法决,能加快修炼时聚集灵气的速度。
归一化解决不了问题?并不是解决不了问题,而是我提供的小说文本每行都不长,所以对结果没有什么影响。
9.7. 案例7:直接换成对中文更有好的语义模型
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
# model = SentenceTransformer('all-MiniLM-L6-v2')
# model = SentenceTransformer('all-mpnet-base-v2')
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# 2. 准备测试数据(模拟你的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
docs.append("Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案1
docs.append("Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案2
docs.append("Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",) # 正确答案3
def read_txt_file(file_path):
"""读取txt文件内容并返回文本列表"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
# 按行读取并去除空行和首尾空白字符
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"错误:文件 {file_path} 不存在")
return []
except Exception as e:
print(f"读取文件时发生错误:{e}")
return []
docs.extend(list(set(read_txt_file("小说8万字大纲.txt"))))
print(f"{docs[0:10] = }")
# 3. 把文本转成向量
doc_vectors = model.encode(docs).astype("float32")
# 🌟 归一化文本向量
faiss.normalize_L2(doc_vectors)
# 4. 构建索引+内积度量(对应余弦相似度)
dim = doc_vectors.shape[1] # 新增:自动获取向量的真实维度
index = faiss.index_factory(dim, "Flat", faiss.METRIC_INNER_PRODUCT)
# 因为 Flat 索引的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 🌟 归一化查询向量
faiss.normalize_L2(query_vector)
# 检索Top3最相似的文本
k = 100
D, I = index.search(query_vector, k)
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
plain
在索引中检索到的最相关的内容为(top 100):
排名 1: 距离=0.8042, 文档ID=9, 内容=Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 2: 距离=0.7853, 文档ID=8, 内容=Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 3: 距离=0.7378, 文档ID=10, 内容=Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引
排名 4: 距离=0.7064, 文档ID=0, 内容=Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索
排名 5: 距离=0.6310, 文档ID=4, 内容=Flat索引是暴力检索,召回率100%,适合小数据量场景
排名 6: 距离=0.5939, 文档ID=3, 内容=HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景
排名 7: 距离=0.5936, 文档ID=1, 内容=IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心
排名 8: 距离=0.5845, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
排名 9: 距离=0.4903, 文档ID=7, 内容=RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成
排名 10: 距离=0.4875, 文档ID=1082, 内容=梦引术:玄阴经秘术之一,搜索、改动他人神识的秘术。
......
你看,问题直接就解决了。<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">BAAI/bge-small-zh-v1.5</font> 是**目前开源界最好的轻量级中文语义检索模型之一**,它的训练数据里包含了大量「中文查询 - 文档」的检索对,专门针对「用户问一个问题,找最相关的文档」这个场景优化,所以效果立竿见影。
9.8. 案例8:如果不归一化呢?
python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载开源语义模型(把文本转成向量,不用自己造随机向量了)
# model = SentenceTransformer('all-MiniLM-L6-v2')
# model = SentenceTransformer('all-mpnet-base-v2')
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# 2. 准备测试数据(模拟你的本地笔记/文档)
docs = []
docs.append("Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",)
docs.append("IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",)
docs.append("PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",)
docs.append("HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",)
docs.append("Flat索引是暴力检索,召回率100%,适合小数据量场景",)
docs.append("Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",)
docs.append("向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",)
docs.append("RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",)
docs.append("Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案1
docs.append("Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",) # 正确答案2
docs.append("Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",) # 正确答案3
def read_txt_file(file_path):
"""读取txt文件内容并返回文本列表"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
# 按行读取并去除空行和首尾空白字符
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"错误:文件 {file_path} 不存在")
return []
except Exception as e:
print(f"读取文件时发生错误:{e}")
return []
docs.extend(list(set(read_txt_file("小说8万字大纲.txt"))))
print(f"{docs[0:10] = }")
# 3. 把文本转成向量
doc_vectors = model.encode(docs).astype("float32")
# 4. 使用HNSW构建索引
dim = doc_vectors.shape[1] # 新增:自动获取向量的真实维度
index = faiss.index_factory(dim, "Flat", faiss.METRIC_L2)
# 因为HNSW的特性,不需要训练,直接添加向量即可
index.add(doc_vectors)
print(f"索引构建完成,向量总数:{index.ntotal}")
# 5. 执行索引(需要真正用Faiss了)
query = "Faiss的索引类型有哪些?"
# 因为是向量检索,所以需要把查询文本也转成向量
query_vector = model.encode([query]).astype("float32")
# 检索Top3最相似的文本
k = 100
D, I = index.search(query_vector, k)
# 6. 打印查询结果
print(f"我们的查询是: {query}")
print(f"在索引中检索到的最相关的内容为(top {k}):")
for idx, (distance, doc_id) in enumerate(zip(D[0], I[0])):
print(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}")
plain
在索引中检索到的最相关的内容为(top 100):
排名 1: 距离=0.3916, 文档ID=9, 内容=Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 2: 距离=0.4295, 文档ID=8, 内容=Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 3: 距离=0.5244, 文档ID=10, 内容=Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引
排名 4: 距离=0.5872, 文档ID=0, 内容=Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索
排名 5: 距离=0.7380, 文档ID=4, 内容=Flat索引是暴力检索,召回率100%,适合小数据量场景
排名 6: 距离=0.8123, 文档ID=3, 内容=HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景
排名 7: 距离=0.8129, 文档ID=1, 内容=IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心
排名 8: 距离=0.8311, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
排名 9: 距离=1.0193, 文档ID=7, 内容=RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成
排名 10: 距离=1.0250, 文档ID=2153, 内容=梦引术:玄阴经秘术之一,搜索、改动他人神识的秘术。
......
貌似对于Flat而言,没有区别。
9.9. 案例9:那我们试试HNSW
先看看不加入归一化的结果:
plain
在索引中检索到的最相关的内容为(top 100):
排名 1: 距离=0.3916, 文档ID=9, 内容=Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 2: 距离=0.4295, 文档ID=8, 内容=Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 3: 距离=0.5244, 文档ID=10, 内容=Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引
排名 4: 距离=0.5872, 文档ID=0, 内容=Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索
排名 5: 距离=0.7380, 文档ID=4, 内容=Flat索引是暴力检索,召回率100%,适合小数据量场景
排名 6: 距离=0.8123, 文档ID=3, 内容=HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景
排名 7: 距离=0.8129, 文档ID=1, 内容=IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心
排名 8: 距离=0.8311, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
排名 9: 距离=1.0193, 文档ID=7, 内容=RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成
排名 10: 距离=1.0250, 文档ID=2595, 内容=梦引术:玄阴经秘术之一,搜索、改动他人神识的秘术。
再看看加入归一化的结果:
plain
在索引中检索到的最相关的内容为(top 100):
排名 1: 距离=0.8042, 文档ID=9, 内容=Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 2: 距离=0.7853, 文档ID=8, 内容=Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW
排名 3: 距离=0.7378, 文档ID=10, 内容=Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引
排名 4: 距离=0.7064, 文档ID=0, 内容=Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索
排名 5: 距离=0.6310, 文档ID=4, 内容=Flat索引是暴力检索,召回率100%,适合小数据量场景
排名 6: 距离=0.5939, 文档ID=3, 内容=HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景
排名 7: 距离=0.5936, 文档ID=1, 内容=IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心
排名 8: 距离=0.5845, 文档ID=6, 内容=向量数据库的核心是向量检索引擎,很多底层基于Faiss实现
排名 9: 距离=0.4903, 文档ID=7, 内容=RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成
排名 10: 距离=0.4875, 文档ID=470, 内容=梦引术:玄阴经秘术之一,搜索、改动他人神识的秘术。
没有什么区别呀。
这是因为我们的场景有两个特殊性,导致归一化的 "排除长度干扰" 的优势没体现出来:
- 我们用的是同一个模型编码的同类型短句 :
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">BAAI/bge-small-zh-v1.5</font>对 "定义式短句" 的编码长度(范数)差异本来就很小 ------ 不管是 Faiss 技术短句还是修仙设定短句,向量长度都差不多,所以归一化前后的相对距离顺序几乎不变; - 我们用的是 HNSW 索引,图结构已经固定了相对距离:HNSW 在构建图时,已经根据向量的相对距离建立了连接,归一化只是改变了距离的尺度,没有改变相对顺序,所以检索结果的排序几乎不变。
什么时候归一化会有 "天差地别" 的效果?
- 场景 1:混合不同长度的文本(标题 vs 长文档)
- 比如:
- 你有两种文本:一种是 "只有几个字的标题"(比如 "Faiss 索引"),另一种是 "几千字的长文档"(比如 "Faiss 索引的详细介绍");
- 不用归一化时,长文档的向量长度会比标题长很多,L2 距离会被 "长度" 主导 ------ 哪怕标题和查询的语义更相关,也可能因为长度短,L2 距离大,被排到后面;
- 归一化后,所有向量的长度都变成 1,L2 距离只看 "方向的相似性",也就是语义的相似性,标题会排到前面。
- 比如:
- 场景 2:混合不同来源的向量(两个不同的模型)
- 比如:
- 你有两种向量:一种是用
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">BAAI/bge-small-zh-v1.5</font>编码的(范数大概 0.8-1.2),另一种是用<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">all-MiniLM-L6-v2</font>编码的(范数大概 5-10); - 不用归一化时,
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">all-MiniLM-L6-v2</font>的向量长度会比<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">bge</font>长很多,L2 距离会被 "长度" 主导 ------ 哪怕<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">bge</font>的向量和查询的语义更相关,也可能因为长度短,L2 距离大,被排到后面;
- 你有两种向量:一种是用
- 归一化后,所有向量的长度都变成 1,两种来源的向量可以公平比较。
- 比如:
10. 所有构建索引方法对比
我的电脑CPU是:Intel® Core™ i7-14700HX
我们对5种类型的检索进行速度比拼,看看速度、查询效果如何:
python
import faiss
import time
import numpy as np
from prettytable import PrettyTable
from sentence_transformers import SentenceTransformer
# ====================== 1. 加载模型和准备数据 ======================
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# 准备Faiss相关文档
docs = [
"Faiss是Meta开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索",
"IVF索引通过k-means分桶实现检索加速,需要先训练聚类中心",
"PQ乘积量化通过向量分段压缩,大幅减少内存占用,适合超大规模向量库",
"HNSW是基于分层图的索引,检索速度极快,召回率接近暴力检索,适合线上实时场景",
"Flat索引是暴力检索,召回率100%,适合小数据量场景",
"Python是一门解释型编程语言,广泛用于机器学习、数据分析领域",
"向量数据库的核心是向量检索引擎,很多底层基于Faiss实现",
"RAG检索增强生成的核心步骤是:文档向量化、向量检索、prompt拼接、大模型生成",
"Faiss的核心的索引有Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",
"Faiss的核心的索引有6个,分别是Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW",
"Flat、IVFx Flat、PQx、IVFxPQy、LSH、HNSW是Faiss的核心的索引",
]
def read_txt_file(file_path):
"""读取txt文件内容并返回文本列表"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return [line.strip() for line in f if line.strip()]
except Exception as e:
print(f"读取文件时发生错误:{e}")
return []
# 加载小说干扰数据
docs.extend(list(set(read_txt_file("小说8万字大纲.txt"))))
print(f"文档总数:{len(docs)}")
# ====================== 2. 编码向量(显式归一化) ======================
doc_vectors = model.encode(docs).astype('float32')
faiss.normalize_L2(doc_vectors) # 显式归一化,通用最佳实践
dim = doc_vectors.shape[1]
print(f"向量维度:{dim}")
# ====================== 3. 定义查询和Ground Truth(用于计算召回率) ======================
query = "Faiss的索引类型有哪些?"
query_vector = model.encode([query]).astype('float32')
faiss.normalize_L2(query_vector)
k = 10
# 先跑Flat,获取Ground Truth(标准答案的文档ID)
index_flat = faiss.index_factory(dim, "Flat", faiss.METRIC_INNER_PRODUCT)
index_flat.add(doc_vectors)
_, I_flat = index_flat.search(query_vector, k)
ground_truth_ids = set(I_flat[0])
print(f"Ground Truth(Flat的Top10文档ID):{ground_truth_ids}")
# ====================== 4. 循环对比不同索引 ======================
table = PrettyTable()
# 🌟 修改1:加内存占用列
table.field_names = ["索引类型", "构建耗时(秒)", "训练耗时(秒)", "添加耗时(秒)", "内存占用(MB)", "总耗时(秒)", "Top10召回率"]
# 索引类型列表(把逗号替换成下划线,方便保存文件)
index_types = [
("Flat", "Flat"),
("IVF100,Flat", "IVF100_Flat"),
("PQ16", "PQ16"),
("IVF100,PQ16", "IVF100_PQ16"),
("HNSW64", "HNSW64")
]
for index_factory_str, index_name in index_types:
print(f"\n正在测试索引:{index_name}...")
# 多次实验取平均(3次)
build_times = []
train_times = []
add_times = []
memory_usages = [] # 🌟 修改2:加内存列表
total_times = []
recall_scores = []
for _ in range(3):
# 1. 构建索引
build_start = time.time()
index = faiss.index_factory(dim, index_factory_str, faiss.METRIC_INNER_PRODUCT)
build_end = time.time()
build_time = build_end - build_start
build_times.append(build_time)
# 2. 训练索引(如果需要)
train_time = 0
if not index.is_trained:
train_start = time.time()
index.train(doc_vectors)
train_end = time.time()
train_time = train_end - train_start
train_times.append(train_time)
# 3. 添加向量
add_start = time.time()
index.add(doc_vectors)
add_end = time.time()
add_time = add_end - add_start
add_times.append(add_time)
# 🌟 修改3:测量索引内存占用(关键!)
serialized = faiss.serialize_index(index)
memory_usage = len(serialized) / (1024 * 1024) # 字节转MB
memory_usages.append(memory_usage)
# 4. 总耗时
total_time = build_time + train_time + add_time
total_times.append(total_time)
# 5. 设置索引的检索参数
if "IVF" in index_factory_str:
index.nprobe = 20 # IVF搜索20个桶
if "HNSW" in index_factory_str:
index.hnsw.efSearch = 64 # HNSW的候选集大小
# 6. 检索并计算召回率
_, I = index.search(query_vector, k)
retrieved_ids = set(I[0])
recall = len(retrieved_ids & ground_truth_ids) / len(ground_truth_ids)
recall_scores.append(recall)
# 取平均值
avg_build = np.mean(build_times)
avg_train = np.mean(train_times)
avg_add = np.mean(add_times)
avg_memory = np.mean(memory_usages) # 🌟 修改4:取内存平均
avg_total = np.mean(total_times)
avg_recall = np.mean(recall_scores)
# 添加到表格
table.add_row([
index_name,
f"{avg_build:.4f}",
f"{avg_train:.4f}",
f"{avg_add:.4f}",
f"{avg_memory:.2f}", # 🌟 修改5:加内存到表格
f"{avg_total:.4f}",
f"{avg_recall:.2%}"
])
# 7. 保存最后一次的检索结果
with open(f"faiss_{index_name}_results.txt", "w", encoding="utf-8") as f:
f.write(f"我们的查询是: {query}\n")
f.write(f"在索引中检索到的最相关的内容为(top {k}):\n")
for idx, (distance, doc_id) in enumerate(zip(_[0], I[0])):
f.write(f"排名 {idx+1}: 距离={distance:.4f}, 文档ID={doc_id}, 内容={docs[doc_id]}\n")
# ====================== 5. 打印结果 ======================
print("\n" + "="*80)
print("🌟 索引性能对比(3次实验取平均)")
print("="*80)
print(table)
plain
================================================================================
🌟 索引性能对比(3次实验取平均)
================================================================================
+-------------+--------------+--------------+--------------+--------------+------------+-------------+
| 索引类型 | 构建耗时(秒) | 训练耗时(秒) | 添加耗时(秒) | 内存占用(MB) | 总耗时(秒) | Top10召回率 |
+-------------+--------------+--------------+--------------+--------------+------------+-------------+
| Flat | 0.0003 | 0.0000 | 0.0017 | 6.24 | 0.0020 | 100.00% |
| IVF100_Flat | 0.0012 | 0.2017 | 0.0167 | 6.46 | 0.2196 | 100.00% |
| PQ16 | 0.0037 | 9.3675 | 0.0976 | 0.55 | 9.4688 | 90.00% |
| IVF100_PQ16 | 0.0014 | 9.5760 | 0.1109 | 0.77 | 9.6884 | 80.00% |
| HNSW64 | 0.0010 | 0.0000 | 0.0705 | 7.85 | 0.0714 | 100.00% |
+-------------+--------------+--------------+--------------+--------------+------------+-------------+
这个结果非常完整,完美验证了Faiss主流索引在「速度、内存、召回率」三个核心维度上的权衡------我们结合「数据量背景(3197条)」「新增的内存占用列」「每个索引的算法原理」,逐一解读并给出明确的适用场景:
10.1. 先明确数据量背景
我们的实验数据量是 3197条384维向量 (小数据量),这个背景是解读所有结果的前提------很多索引的「速度/内存优势」要在百万/亿级数据量下才会体现,小数据量下反而会因为「训练/构建开销」显得慢。
10.2. 逐一解读每个索引的结果
10.2.1. Flat(暴力检索)------ 基准线
| 指标 | 结果 | 核心解读 |
|---|---|---|
| 构建耗时 | 0.0003秒 | 极快------只是初始化一个空的向量数组。 |
| 训练耗时 | 0.0000秒 | 无------暴力检索不需要预训练任何结构。 |
| 添加耗时 | 0.0017秒 | 极快------直接把向量追加到数组末尾,无额外计算。 |
| 内存占用 | 6.24 MB | 基准线 ------全量存储3197条384维float32原始向量(计算公式:3197 × 384 × 4 ÷ (1024×1024) ≈ 4.7MB,实际6.24MB是因为Faiss有少量元数据开销)。 |
| 总耗时 | 0.0020秒 | 最低------小数据量下,暴力检索的开销最小。 |
| Top10召回率 | 100.00% | 满分------遍历所有向量,不会漏检。 |
10.2.1.1. 适用场景
- 小数据量(<10 万条);
- 需要 100% 召回率(比如验证其他索引的召回率时作为 Ground Truth);
- 离线场景(对检索速度要求不高)。
10.2.2. IVF100_Flat(分桶+暴力检索)------ 平衡之选
| 指标 | 结果 | 核心解读 |
|---|---|---|
| 构建耗时 | 0.0012秒 | 快------初始化IVF的分桶结构。 |
| 训练耗时 | 0.2017秒 | 有开销------用k-means把3197条向量聚成100个聚类中心。 |
| 添加耗时 | 0.0167秒 | 比Flat慢------需要先计算向量属于哪个桶,再追加。 |
| 内存占用 | 6.46 MB | 仅比Flat多0.22MB------因为没有压缩向量,只是多存了100个384维的聚类中心(几乎不占内存)。 |
| 总耗时 | 0.2196秒 | 比Flat高------小数据量下,训练的开销超过了分桶的优势。 |
| Top10召回率 | 100.00% | 满分 ------因为你设置了nprobe=20(搜索20个桶),覆盖了所有相关向量。 |
10.2.2.1. 关键补充
- 小数据量下IVF的速度优势没体现,但如果数据量到 100万条 ,IVF的检索速度会比Flat快 10-100倍;
- 内存和Flat几乎一样,因为没有压缩。
10.2.2.2. 适用场景
- 中等数据量(10万-1000万条);
- 内存足够存全量**内存足够存全量原始向量****(因为用了 Flat,没有压缩)**
- 需要平衡速度和召回率。
10.2.3. PQ16(乘积量化压缩)------ 内存杀手(小数据量训练慢,但内存优势巨大)
| 指标 | 结果 | 核心解读 |
|---|---|---|
| 构建耗时 | 0.0037秒 | 快------初始化PQ的压缩结构。 |
| 训练耗时 | 9.3675秒 | 很高------需要把384维向量切分成16个子向量,每个子向量用k-means聚成256个中心(16×256次聚类)。 |
| 添加耗时 | 0.0976秒 | 比Flat慢------需要把每个子向量量化成对应的聚类中心ID(压缩过程)。 |
| 内存占用 | 0.55 MB | 亮点!压缩了91% ------计算公式:(6.24 - 0.55) / 6.24 ≈ 91%。PQ把每个384维float32向量(1536字节)压缩成了16个uint8 ID(16字节),内存占用骤降。 |
| 总耗时 | 9.4688秒 | 很高------小数据量下,训练的开销完全盖过了压缩的优势。 |
| Top10召回率 | 90.00% | 有损失------PQ是"有损压缩",会丢失部分向量细节,导致召回率下降10%。 |
10.2.3.1. 关键补充
- 表格里没列,但这是PQ的核心价值 :如果数据量到 1亿条 ,Flat需要
1亿×384×4 ≈ 150GB,PQ16只需要1亿×16×1 ≈ 1.6GB,内存优势是数量级的; - 召回率虽然只有90%,但可以通过增加子向量数量(比如从16改成32)提升到95%+(但内存会翻倍)。
10.2.3.2. 适用场景
- 超大规模数据量(>1000万条);
- 内存严重受限;
- 能接受10%-20%的召回率损失。
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:PQ16是对向量进行了压缩,那么我们最后取原文的时候是从哪里取的呢(我的理解都是在内存中,难道返回的是压缩后的文本?)?
🥳 𝑨𝒏𝒔𝒘𝒆𝒓:这个问题问得**非常关键,戳中了 Faiss 的核心定位。 Faiss 只存「向量」,不存「原始文本」 ,Faiss 是一个「向量检索库」,不是「数据库」------ 它的唯一作用是「在向量库中快速找到和查询向量最相似的 TopK 向量的 ID」,它根本不存原始文本! **
10.2.4. IVF100_PQ16(分桶+乘积量化)------ 内存极致压缩(双重损失,仅适用于超大规模数据)
| 指标 | 结果 | 核心解读 |
|---|---|---|
| 构建耗时 | 0.0014秒 | 快------同时初始化IVF和PQ的结构。 |
| 训练耗时 | 9.5760秒 | 最高------既要训练IVF的100个聚类中心,又要训练PQ的16×256个子向量中心,训练时间叠加。 |
| 添加耗时 | 0.1109秒 | 最慢------既要分桶,又要量化。 |
| 内存占用 | 0.77 MB | 依然极小------比PQ16多0.22MB,因为多存了IVF的100个聚类中心,但依然比Flat小90%+。 |
| 总耗时 | 9.6884秒 | 最高------小数据量下,双重训练的开销最大。 |
| Top10召回率 | 80.00% | 损失最大------IVF分桶(可能漏桶)+ PQ压缩(丢失细节),双重信息损失。 |
10.2.4.1. 关键补充
- 这是内存占用最小 的索引组合:结合了IVF的分桶和PQ的压缩,1亿条数据只需要
1亿×16×1 + 100×384×4 ≈ 1.6GB; - 召回率虽然只有80%,但可以通过调大
nprobe(比如设成50)提升到90%+。
10.2.4.2. 适用场景
- 超大规模数据量(>1亿条);
- 内存极度受限;
- 能接受20%-30%的召回率损失(或通过调参弥补)。
10.2.5. HNSW64(分层可导航小世界图)------ 工业界首选(小数据量下也完美)
| 指标 | 结果 | 核心解读 |
|---|---|---|
| 构建耗时 | 0.0010秒 | 快------初始化HNSW的图结构。 |
| 训练耗时 | 0.0000秒 | 无------HNSW不需要预训练,图结构是在add时增量构建的。 |
| 添加耗时 | 0.0705秒 | 比Flat慢------需要为每个向量在图里找邻居、建连接,但比IVF/PQ的训练快很多。 |
| 内存占用 | 7.85 MB | 比Flat多1.61MB(26%) ------因为要存全量原始向量 + 图的连接信息(每个向量大概存64个邻居ID,每个ID4字节,所以多了 3197×64×4 ≈ 0.8MB,实际1.61MB是因为Faiss的图结构有额外元数据)。 |
| 总耗时 | 0.0714秒 | 很低------只比Flat高一点,远低于IVF/PQ。 |
| Top10召回率 | 100.00% | 满分 ------因为你设置了efSearch=64(搜索时的候选集大小),图的贪心搜索覆盖了所有相关向量。 |
10.2.5.1. 关键补充
- HNSW是现在工业界的绝对首选 :
- 小数据量下(3197条):速度接近Flat,召回率100%;
- 大数据量下(>100万条):检索速度比IVF还快 2-5倍,且召回率依然接近100%;
- 唯一的缺点是内存稍高,但现在内存越来越便宜(黄牛:?),这个缺点完全可以接受。
10.2.5.2. 适用场景
- 线上实时场景(对检索速度要求极高,比如毫秒级响应);
- 任何数据量(只要内存够存全量原始向量+图连接);
- 需要高召回率(接近100%)。
10.3. 总结
在3197条数据量下的「三维权衡」与推荐:
| 索引类型 | 内存占用(MB) | 总耗时(秒) | Top10召回率 | 推荐度 | 核心适用场景 |
|---|---|---|---|---|---|
| Flat | 6.24(基准) | 0.0020(最低) | 100.00% | ⭐⭐⭐⭐ | 小数据量验证、离线场景 |
| IVF100_Flat | 6.46(几乎不变) | 0.2196(稍高) | 100.00% | ⭐⭐⭐ | 中等数据量、内存足够 |
| PQ16 | 0.55(压缩91%) | 9.4688(很高) | 90.00% | ⭐⭐ | 超大规模数据量、内存受限 |
| IVF100_PQ16 | 0.77(压缩88%) | 9.6884(最高) | 80.00% | ⭐ | 超大规模数据量、内存极度受限 |
| HNSW64 | 7.85(高26%) | 0.0714(很低) | 100.00% | ⭐⭐⭐⭐⭐ | 工业界首选,任何数据量、线上实时场景 |
11. Faiss与RAG的关系
11.1. 明确概念
RAG是一套解决大模型「知识过时」和「幻觉」问题的完整架构,核心逻辑是:
"先从外部知识库中检索出和用户问题相关的内容,再把这些内容和用户的问题一起喂给大模型,让大模型基于检索到的真实内容生成答案。"
它不是一个具体的工具,而是一套包含「文档预处理、向量化、向量检索、Prompt拼接、大模型生成」的完整流程。
Faiss是Meta开源的高性能向量检索库,它的唯一核心作用是:
"在百万/亿/十亿级高维向量中,毫秒级找到和查询向量最相似的TopK向量的ID。"
它不存原始数据,只负责"快速找相似向量",是一个纯粹的"技术组件"。
11.2. Faiss在RAG中的介入时机
我们把RAG的完整流程拆解开,看Faiss在哪里介入:
| 步骤 | 做什么 | 用什么工具 | Faiss介入了吗? |
|---|---|---|---|
| 1. 文档预处理 | 把PDF/Word/笔记切分成短片段(比如每段512字) | LangChain、LLaMAIndex | ❌ |
| 2. 文档向量化 | 用语义模型把每个文档片段转成高维向量(比如384/768维) | all-MiniLM-L6-v2、bge-small-zh |
❌ |
| 3. 构建向量索引 | 把所有文档向量存到Faiss里,构建加速索引(比如HNSW/IVF) | Faiss | ✅ 核心介入! |
| 4. 用户查询向量化 | 用同一个语义模型把用户的问题转成向量 | 和步骤2一样的模型 | ❌ |
| 5. 向量检索 | 用Faiss在索引中快速找到和查询向量最相似的TopK文档ID | Faiss | ✅ 核心介入! |
| 6. 取原始文档 | 用Faiss返回的文档ID,去原始数据库(比如MySQL/PostgreSQL)里取对应的文档片段 | MySQL、PostgreSQL、Python列表 | ❌ |
| 7. Prompt拼接+大模型生成 | 把「用户问题+检索到的文档片段」拼成Prompt,喂给大模型生成答案 | GPT-4o、Qwen、Llama 3 | ❌ |
11.3. Faiss在RAG中的核心价值
RAG对「向量检索」的速度要求极高(通常需要毫秒级响应),这正是Faiss的不可替代性所在:
- 如果不用Faiss(用Flat暴力检索) :100万条384维向量,检索一次需要 0.5-1秒,用户体验很差;
- 如果用Faiss(用HNSW64索引) :同样100万条向量,检索一次只需要 0.001-0.01秒,完全满足实时响应的要求;
- 如果用Faiss(用IVF1000_PQ16索引) :1亿条向量,检索一次也只需要 0.01-0.05秒,且内存占用极小。
一句话总结:Faiss是RAG的「性能心脏」------没有Faiss的加速,RAG在海量数据下根本跑不起来。
11.4. Faiss在RAG的向量检索替代方案
虽然Faiss是RAG中最常用的向量检索组件,但它不是唯一的选择,我们可以把RAG的向量检索方案分成三类:
- 轻量级/自建场景(用Faiss)
- 适用场景:个人项目、小团队、数据量<1000万条、不想依赖云服务;
- 优势:开源、免费、轻量、性能好、完全可控;
- 劣势:需要自己维护索引、自己做数据持久化、自己处理分布式。
- 托管向量数据库(用Milvus/Pinecone/Weaviate)
- 适用场景:企业级项目、数据量>1亿条、需要分布式、不想自己维护;
- 优势:托管服务、自动扩容、支持分布式、有完整的元数据管理;
- 关键联系 :很多托管向量数据库的底层就是用Faiss实现的(比如Milvus的默认索引就是Faiss的IVF/HNSW)。
- 轻量级向量数据库(用Chroma/Qdrant)
- 适用场景:个人项目、小团队、数据量<100万条、想要比Faiss更完整的功能(比如元数据过滤);
- 优势:比Faiss多了元数据管理、数据持久化、更简单的API;
- 劣势:性能比纯Faiss稍差一点。