Faiss:大规模向量相似度检索

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的优势非常突出:

  1. 工业级稳定性:从2017年开源至今,经过近10年的工业打磨,被全球众多大厂(如Meta、谷歌、阿里等)广泛应用于生产环境
  2. 性能天花板高:在"纯向量检索库"赛道,它的速度和精度依然处于第一梯队
  3. 生态完善:文档齐全、社区活跃,遇到问题很容易找到解决方案
  4. 学习成本低:API设计简洁,Python接口上手极快,半天就能跑通基本流程

4. Faiss的安装指南

强烈建议安装 Faiss 1.7.3以上版本------新版本已经解决了老版本安装困难、bug多的问题,安装非常简单。根据我们的需求,可以选择CPU版或GPU版:

  • 如果我们的数据量在几万到几十万条,或者部署环境没有GPU,CPU版完全够用
  • 如果有NVIDIA显卡,且数据量在百万级以上,或者需要低延迟/高并发查询,GPU版是更好的选择

5. 从基础操作开始

我们将通过一个极简的官方示例,带你快速上手Faiss的核心流程------虽然整体步骤看似"把大象装冰箱"般简单,但每个环节都有需要注意的关键点,掌握这些能帮你少走很多弯路。

5.1. Faiss 核心流程:三步完成相似度检索

Faiss的所有检索任务,本质上都遵循这三个核心步骤:

  1. 准备向量数据:生成或加载待检索的向量库(database vectors)和查询向量(query vectors);
  2. 构建并填充索引:选择合适的索引类型,将向量库添加到索引中;
  3. 执行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返回的是「平方欧氏距离」,而不是开根号后的欧氏距离

所以我们有两种验证方法:

  1. 把Faiss返回的结果开个根号
  2. 把手动计算的结果平方
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的索引类型非常多,记不同的类名(如IndexFlatL2IndexIVFFlat)很麻烦。**强烈建议统一用 **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参数

nprobenumber 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向量时就不是随便存了,而是:

  1. 对每个新向量,计算它和100个聚类中心的距离;
  2. 把这个向量放到距离最近的那个聚类中心对应的桶里;
  3. 建立一张「倒排表」:记录每个聚类中心(桶)里都有哪些向量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的结合体,集两家之长:

  1. 先用IVF把向量分成x个桶(减少搜索范围);
  2. 再用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内部会做这些计算:

  1. 计算新向量和已有部分节点的距离;
  2. 从顶层图开始,通过贪心搜索找到新向量在每一层的"插入位置";
  3. 为新向量在每一层选择并连接最多x(即你代码里的64)个邻居节点;
  4. 维护图的连通性(如果需要,会调整旧节点的连接)。

这个过程是增量的、边加边做的 ,不需要先拿一批数据跑train(),所以FAISS认为它"不需要训练"。

三、用之前的图书馆类比总结

  • IVF :先根据旧书的主题分好100个书架(train()),再把旧书、新书放到对应的书架上(add());
  • HNSW :不用先分书架,直接边放书边给每本书搭"导航路标"(add()时直接构建图),放完书导航图也就建好了。

四、一句话总结

HNSW不需要调用FAISS的 index.train()方法is_trainedTrue),但add()时会做"分层图结构的增量构建计算"------这不是FAISS定义的"训练",是它特有的索引构建方式。
🤔 𝑸𝒖𝒆𝒔𝒕𝒊𝒐𝒏:按照HNSW的逻辑,是全量向量都会加载到内存中,并且还有顶层图的存在,所以内存占用还会更大吗?

🥳 𝑨𝒏𝒔𝒘𝒆𝒓:你的理解完全正确 !HNSW 确实是 FAISS 常用索引中内存占用最大的 ------核心原因就是它不仅要存全量原始向量 (和 Flat 一样),还要额外存复杂的分层图连接信息(这部分是 Flat 没有的)。

我们用具体的内存构成拆解+对比讲清楚:

HNSW 的内存由两部分组成

1. 原始向量存储(和 Flat 完全一样)

这部分是"必选项",HNSW 为了保证高召回率,不会对原始向量做压缩(不像 PQ 那样切分量化),必须完整存所有向量的原始数据。

  • 内存开销:nb × dim × 4 字节(float32 每个维度占 4 字节)
  • 比如你代码里的 102400 条 64 维向量:102400 × 64 × 4 ≈ 25MB
2. 分层图的边连接信息(HNSW 独有,内存大的核心原因)

这部分是"额外开销",HNSW 要存每个节点在每一层的邻居连接关系 ------也就是你代码里 HNSW64 中的"64 条边"对应的存储。

  • 内存开销:主要由 M 参数(即你代码里的 x,每个节点每层的最大连接数)和层数决定
    • 层数:随数据量 nbO(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个最经典、我们每天都在接触的落地场景,瞬间就能让我们明白它的价值:

  1. 以图搜图(淘宝拍立淘、百度识图):把商品 / 图片转成 512/1024 维的特征向量,用 HNSW/IVF_PQ 建索引,用户上传图片时,检索库里最相似的图片,我们学的「索引速度 / 召回率平衡」就是这里的核心选型逻辑。
  2. RAG 检索增强生成(大模型知识库):现在最火的大模型问答场景,把 PDF / 文档 / 笔记切成片段,转成语义向量,用 Faiss 建索引;用户提问时,先检索和问题最相关的文档片段,再喂给大模型生成答案,Faiss 就是整个系统的 "大脑检索中枢"。
  3. 推荐系统召回阶段:抖音 / 淘宝的推荐,会给用户生成一个 "兴趣向量",给商品 / 内容也生成向量,用 Faiss 在亿级商品库中快速检索出和用户兴趣最匹配的几百个商品,再做精排,我们学的 IVF_PQ 就是工业界推荐系统最常用的索引。
  4. 本地语义搜索:给我们自己的几百篇笔记、文档建索引,不用记关键词,直接搜 "我之前写的关于 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 + 语义模型的核心价值:

  1. Top1(文档 6) :内容是 "向量数据库的核心是向量检索引擎,很多底层基于 Faiss 实现"
    • 虽然没有直接列索引类型,但提到了 "Faiss 作为底层引擎",和查询的 "Faiss 索引" 语义高度相关,所以距离最小。
  2. Top2(文档 0) :内容是 "Faiss 是 Meta 开源的向量检索库,支持多种索引类型,用于高维向量的快速相似性检索"
    • 直接提到了 "Faiss 支持多种索引类型",是查询的核心关键词之一,距离稍大但也非常接近。
  3. 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]}")

改动:

  1. query变了
  2. docs也变了,添加了3个正确答案
  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> 的结果相比,这次的结果明显更 "贴题":

  1. Top1(文档 0):内容是 "Faiss 是 Meta 开源的向量检索库,支持多种索引类型"------ 虽然没直接列索引,但它是查询的 "核心语境引入",语义模型认为 "先确认 Faiss 有多种索引" 是回答问题的第一步,整体语义关联最强;
  2. Top2(文档 8):直接列了 "Faiss 的核心索引有 Flat、IVFx Flat..."------ 这是你预期的 "正确答案",已经排到了第二,比之前的结果进步很大;
  3. 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 或语义模型出了问题,我们拆解清楚原因:

  1. 最主要原因:数据严重不平衡,相关向量被 "淹没"
  • 我们的数据里,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 相关的向量。
  1. 次要原因 :未做向量归一化,L2 距离受向量长度影响。我们用的是 <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">faiss.METRIC_L2</font>(欧氏距离),但没有对向量做归一化
  • 欧氏距离不仅看 "向量方向的相似性",还看 "向量长度的差异";
  • 小说文本的长度、语义模型对不同文本的编码长度可能有差异,导致小说向量和查询向量的 L2 距离看起来 "更小",但其实语义完全不相关。
  1. 辅助原因 :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?

🥳 𝑨𝒏𝒔𝒘𝒆𝒓:这个结果包含两个关键信息,完全符合我们的预判:

  1. Flat 确实能找到相关文档:证明不是 Faiss 或语义模型的问题,是数据不平衡 + 向量空间分布的问题;
  2. 只有文档 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, 内容=梦引术:玄阴经秘术之一,搜索、改动他人神识的秘术。

没有什么区别呀。

这是因为我们的场景有两个特殊性,导致归一化的 "排除长度干扰" 的优势没体现出来:

  1. 我们用的是同一个模型编码的同类型短句<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">BAAI/bge-small-zh-v1.5</font> 对 "定义式短句" 的编码长度(范数)差异本来就很小 ------ 不管是 Faiss 技术短句还是修仙设定短句,向量长度都差不多,所以归一化前后的相对距离顺序几乎不变;
  2. 我们用的是 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-v2bge-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稍差一点。

12. 参考

相关推荐
zhojiew8 小时前
在RAG系统中对FAISS,HNSW,BM25向量检索引擎选型的问题
人工智能·机器学习·faiss
深藏功yu名8 小时前
Day24:向量数据库 Chroma_FAISS 入门
数据库·人工智能·python·ai·agent·faiss·chroma
深藏功yu名11 小时前
Day24(进阶篇):向量数据库 Chroma_FAISS 深度攻坚 —— 索引优化、性能调优与生产级落地
数据库·人工智能·python·ai·agent·faiss·chroma
爱打代码的小林10 天前
基于 LangChain + 通义千问 + FAISS 构建 RAG 问答系统
langchain·大模型·faiss·rag
麦麦大数据1 个月前
M004_基于Langchain+RAG的银行智能客服系统设计与开发
typescript·langchain·flask·vue3·faiss·rag
veFuwcCVSXz1 个月前
三相开绕组永磁同步电机的容错控制
faiss
pvIaUtLZ1 个月前
单相逆变器的控制方案直接决定了系统在并网/离网模式下的动态响应。咱们今天拆解几个硬核玩法,从调制到控制层层递进,手把手看明白这背后的门道
faiss
charlee441 个月前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索
dblens 数据库管理和开发工具2 个月前
开源向量数据库比较:Chroma, Milvus, Faiss,Weaviate
数据库·开源·milvus·faiss·chroma·weaviate