技术栈定位:C++ 内核实现 + Python/C++ 双语言接口,Meta FAIR(Facebook AI Research)开源,BSD 许可。
1. 背景:为什么需要专门的向量检索
1.1 高维向量检索的业务场景
深度学习把非结构化数据(文本、图片、语音、传感器波形)编码为高维稠密向量(embedding),业务上最常见的需求变成「给一个查询向量,找出库里最相似的 K 个向量」:
- 语义检索 / RAG:把文档切块做 embedding,用户提问时检索最相关的文档片段喂给大模型;
- 以图搜图 / 人脸识别:图片 embedding 入库,新图查最相似;
- 推荐系统:用户向量与物品向量内积排序;
- 异常检测(工业数采):把设备运行窗口(温度/振动/主轴负载等时序片段)编码成向量,检索历史相似工况,辅助工艺追溯与故障类比;
- 代码/缺陷去重:相似代码片段检索。
1.2 朴素方案为什么不行
最朴素的做法是「全量扫描 + 逐条计算距离」:对 n 条 d 维向量,一次查询要做 n×d 次浮点运算。n=1000 万、d=768 时,单次查询约 76.8 亿次运算,普通 CPU 单线程要数秒,且内存里存不下裸数据(1000 万×768×4B ≈ 30GB)。这就是精确最近邻(KNN)在大规模下的不可扩展性。
工业界真正需要的是ANN(Approximate Nearest Neighbor,近似最近邻):牺牲少量召回率,换取数量级的速度与内存收益。FAISS 正是这套问题的标准答案之一。
1.3 FAISS 定位与生态位置
- 内存索引库:FAISS 只管「向量索引 + 检索」,不管持久化、分布式、事务——它把数据整体加载进内存(或 GPU 显存)构建索引;
- 库而非服务:直接链接进你的 C++ 程序或 import 进 Python 进程,没有网络层;
- 与向量数据库的区别:Milvus / Qdrant / Chroma / pgvector 等向量数据库内部大量使用 FAISS / HNSW 类索引,但额外提供了服务化、持久化、过滤、多租户、分布式能力。FAISS 适合「数据规模可控、要极致性能、自己掌控链路」的场景。
2. 核心概念:索引、度量与召回
2.1 距离度量(Metric)
FAISS 最常用的三种度量:
| 度量 | 常量 | 含义 | 适用 |
|---|---|---|---|
| 欧氏距离平方 | faiss.METRIC_L2(默认) | ‖x−y‖² | 向量空间各向同性时 |
| 内积 | faiss.METRIC_INNER_PRODUCT | x·y,越大越相似 | 先 normalize_L2 归一化后再用内积 = 余弦相似度,推荐系统/文本检索最常用 |
| 汉明距离 | METRIC_Hamming | 二进制向量逐位异或 | 二进制索引(IndexBinary*),人脸/生物特征 |
陷阱预告:很多人把「余弦相似度」直接当距离传给 FAISS,但 FAISS 的 search 返回的是「距离」且按升序排序取前 k。用内积时未归一化会导致「模长大的向量恒占优」;用 L2 时未归一化会让「模长差异主导相似度」。正确姿势:faiss.normalize_L2(vectors) 后配 IndexFlatIP。
2.2 精确 vs 近似
| 类型 | 索引 | 速度 | 内存 | 召回率 |
|---|---|---|---|---|
| 精确(暴力) | IndexFlatL2 / IndexFlatIP | O(n·d)/查询,n 大时不可用 | 高(原始向量) | 100% |
| 倒排(IVF) | IndexIVFFlat | 快,随 nprobe 调 | 高(原始向量) | 85%~99% |
| 压缩(PQ) | IndexIVFPQ | 快 | 低(压缩到 1/8~1/32) | 70%~95% |
| 图(HNSW) | IndexHNSWFlat | 极快 | 中高(图边开销) | 95%~99.9% |
| 哈希(LSH) | IndexLSH | 快 | 低 | 中低 |
2.3 一个比喻理解 IVF + PQ
- IVF(Inverted File,倒排文件):先用 K-means 把全库聚成 nlist 个簇,检索时只探测 nprobe 个最近的簇(分区),相当于「先粗筛再细算」;
- PQ(Product Quantization,乘积量化):把 d 维向量切成 m 段子向量,每段用 k 个码字(质心)编码,用 1 个字节(k=256)代表一段,整条向量变成 m 字节的短码——把「存向量」变成「存压缩码」,内存降到原来的 1/8~1/32;
- 两者组合 IVF + PQ 就是十亿级检索的经典配置。
3. 安装与快速上手
# Python(推荐先装 faiss-cpu,GPU 机器再装 faiss-gpu) pip install faiss-cpu # C++(vcpkg) vcpkg install faiss最小闭环(Python):
import faiss import numpy as np d = 64 # 向量维度 n = 10000 # 库内向量数 np.random.seed(42) xb = np.random.rand(n, d).astype('float32') # 库向量,必须 float32! xb[:, 0] += np.arange(n) / 1000.0 # 制造一点区分度 index = faiss.IndexFlatL2(d) # 精确 L2 索引 index.add(xb) # 入库 print(index.ntotal) # 10000 xq = np.random.rand(5, d).astype('float32') # 5 条查询 k = 4 # 取前 4 近邻 D, I = index.search(xq, k) # D: 距离, I: 索引号,形状均为 (5, 4) print(I) print(D)search 返回两个 (nq, k) 的矩阵:I[i][j] 是第 i 条查询的第 j 近邻在库中的序号,D[i][j] 是对应距离。维度顺序是 [查询数][k],和很多人直觉的 [k][查询数] 相反。
4. 索引类型全景与 API 说明
4.1 索引家族总表
| 索引类 | 构造参数 | 特点 | 需要 train |
|---|---|---|---|
| IndexFlatL2(d) | 维度 | 精确暴力 L2 | 否 |
| IndexFlatIP(d) | 维度 | 精确暴力内积(配归一化做余弦) | 否 |
| IndexIVFFlat(quantizer, d, nlist) | 粗量化器、维度、簇数 | 倒排+原始向量 | 是 |
| IndexIVFPQ(quantizer, d, nlist, m, nbits) | +子向量段数、码位 | 倒排+乘积量化 | 是 |
| IndexHNSWFlat(d, M) | 维度、边数 | 图索引,高召回 | 否 |
| IndexHNSW( d, M, metric_type) | 否 | ||
| IndexIDMap(index) | 包装任意索引 | 外部 int64 ID | 随被包装索引 |
| IndexIDMap2(index) | 支持 remove_ids | 随被包装索引 | |
| IndexLSH(d, nbits) | 维度、哈希位数 | LSH 哈希 | 否 |
| IndexBinaryFlat(d) | 二进制维度(bit 数) | 汉明精确 | 否 |
| IndexBinaryIVF(quantizer, d, nlist) | 汉明倒排 | 是 | |
| GpuIndexFlatL2 / GpuIndexIVFFlat / GpuIndexIVFPQ | GPU 版本 | 显存索引 | 视类型 |
4.2 索引工厂:一行代码配索引
faiss.index_factory(d, description_string) 是 FAISS 最优雅的设计,用字符串描述索引结构,避免手写嵌套构造:
# 常见组合 faiss.index_factory(d, "Flat") # IndexFlatL2 faiss.index_factory(d, "Flat", faiss.METRIC_INNER_PRODUCT) # IndexFlatIP faiss.index_factory(d, "IVF256,Flat") # nlist=256 的 IVF faiss.index_factory(d, "IVF256,PQ64") # IVF + PQ,64 段 faiss.index_factory(d, "HNSW64,Flat") # HNSW,M=64 faiss.index_factory(d, "OPQ32_64,IVF256,PQ64") # 先 OPQ 预变换再 IVF+PQ(高精度标配) faiss.index_factory(d, "IVF256,Flat,IDMap") # 包上 ID 映射- PQ64:把向量切成 64 段,每段 8bit(默认 nbits=8),向量压缩为 64 字节;
- OPQ32_64:Optimized Product Quantization,先做可学习的正交旋转再切段,能显著提升 PQ 精度(PQ 切段在原始维度上的相关性会让码字效率下降,OPQ 先解相关);
- 字符串语法还支持 IMI2x10(多索引)、SQ8(标量量化)等。
4.3 核心方法速查(Python 绑定)
| 方法 | 说明 |
|---|---|
| index.add(xb) | 批量入库,xb 形状 (n, d) float32 C 连续 |
| index.add_with_ids(xb, ids) | 配合 IndexIDMap,指定外部 ID(int64) |
| index.search(xq, k) | 批量查询,返回 (D, I) |
| index.train(xb) | 训练聚类/量化器,IVF/PQ 类 add 前必须 train |
| index.is_trained | 是否已训练,add 前检查它 |
| index.ntotal | 库内向量总数 |
| index.d | 维度 |
| index.nprobe | IVF 探测簇数(IVF 特有属性),越大召回越高 |
| index.reconstruct(i) | 还原第 i 条原始向量(Flat 类直接读,PQ 类是近似还原) |
| index.reconstruct_n(i0, n) | 还原一批 |
| index.remove_ids(sel) | IndexIDMap2 支持按 ID 删除;faiss.IDSelectorBatch 构造选择器 |
| index.reset() | 清空索引 |
| faiss.write_index(index, "file.index") | 保存到磁盘 |
| faiss.read_index("file.index") | 加载 |
| faiss.normalize_L2(x) | 原地 L2 归一化(配合 IP 索引) |
| faiss.omp_set_num_threads(t) | 设置 OpenMP 线程数 |
| faiss.vector_to_array(index.reconstruct_n(0, n)) | 把重建结果转回 numpy |
5. 详细使用说明
5.1 IVF 检索:倒排 + nprobe 调参
import faiss, numpy as np d, n, nlist = 128, 100_000, 100 np.random.seed(7) xb = np.random.rand(n, d).astype('float32') quantizer = faiss.IndexFlatL2(d) # 粗量化器:对簇心做精确检索 index = faiss.IndexIVFFlat(quantizer, d, nlist) assert not index.is_trained index.train(xb) # 必须先 train(K-means 聚 nlist 个簇心) assert index.is_trained index.add(xb) index.nprobe = 8 # 查询时探测 8 个簇 xq = np.random.rand(10, d).astype('float32') D, I = index.search(xq, 5) print("召回率参考:nprobe 越大越接近暴力结果")train 的数据与 add 的数据不必相同,但应来自同一分布——train 只用于学簇心与码本,不进入索引。训练集大小建议 ≥ nlist × 39(FAISS 官方经验值:每个簇至少 39 个点,否则 K-means 不稳定)。
5.2 IVF+PQ:十亿级内存方案
d, n, nlist, m = 128, 500_000, 512, 32 xb = np.random.rand(n, d).astype('float32') quantizer = faiss.IndexFlatL2(d) index = faiss.index_factory(d, "IVF512,PQ32") # 等价于 IndexIVFPQ(quantizer, d, nlist, m=32, nbits=8) index.train(xb) index.add(xb) index.nprobe = 16 D, I = index.search(xb[:10], 5)PQ32 把 128 维向量压成 32 字节,内存仅为原始 float32 的 1/16;配合 nlist=512,单机可支撑千万~亿级。代价是距离是近似估计(用码本距离表查表),召回率低于 IVF-Flat。
5.3 HNSW:高召回 + 低延迟
index = faiss.IndexHNSWFlat(d, 32) # M=32:每节点最多 32 条边 index.hnsw.efConstruction = 200 # 建图搜索宽度,越大图质量越高 index.add(xb) index.hnsw.efSearch = 64 # 查询搜索宽度,越大召回越高、越慢 D, I = index.search(xq, 10)- M 越大:图连通性越好、召回越高,但内存与建图时间上升(边要存邻居 ID + 距离,M=32 时每条向量额外约 M×2×8B 的图开销);
- efConstruction / efSearch 是运行时搜索宽度,两者与召回率正相关、与速度负相关;
- HNSW 无需 train,add 即用,是「中等规模 + 高召回 + 低延迟」的最佳默认选择。
5.4 外部 ID 映射与删除
index = faiss.IndexIDMap(faiss.IndexFlatL2(d)) ids = np.arange(1000, 1000 + n).astype('int64') # 业务 ID(如设备点位 ID) index.add_with_ids(xb, ids) D, I = index.search(xq, 3) # 返回的 I 里是业务 ID 而非 0..n-1 序号- IndexIDMap 的 ID 必须是int64;
- 只支持 add_with_ids,不支持 remove;要删除用 IndexIDMap2 + remove_ids(selector);
- 注意:IDMap 包装的是精确索引时,I 返回的是映射后的 ID;但某些组合下返回值语义要实测确认(尤其 IVFPQ 变体),建议写单元测试固化。
5.5 余弦相似度:归一化 + 内积
faiss.normalize_L2(xb) # 原地归一化 faiss.normalize_L2(xq) index = faiss.IndexFlatIP(d) # 或 index_factory(d, "Flat", METRIC_INNER_PRODUCT) index.add(xb) D, I = index.search(xq, 10) # D 现在是余弦相似度(升序,负数在后)归一化后 L2 距离 = 2 - 2×cos,所以「归一化 + IP」与「归一化 + L2」排序结果一致,但 IP 实现更快。务必先归一化再建索引,否则模长大的向量霸榜。
5.6 GPU 加速
import faiss res = faiss.StandardGpuResources() # 显存资源池,全局复用 index_cpu = faiss.index_factory(d, "IVF1024,Flat") index_cpu.train(xb); index_cpu.add(xb) index_gpu = faiss.index_cpu_to_gpu(res, 0, index_cpu) # 0 号卡 D, I = index_gpu.search(xq, 10) # 显式构造 GPU 索引 gpu_index = faiss.GpuIndexIVFFlat(res, d, nlist, faiss.METRIC_L2)- 必须训练完再迁移 GPU(GPU 索引的 train 走 CPU 也行,但常见做法是先 CPU train);
- StandardGpuResources 建议创建一次全局复用,反复创建会反复分配显存;
- 批量查询(search 一次传几千条)才能吃满 GPU,单条查询反而有传输开销。
5.7 RAG 实战:本地 LLM + FAISS 检索增强
与llama闭环的完整链路(伪代码+要点):
# 1. 文档切块 + embedding(llama.cpp embedding 模型,如 nomic-embed-text) # 得到 doc_chunks: list[str], doc_vecs: (n, 384) float32 # 2. 建库 import faiss faiss.normalize_L2(doc_vecs) index = faiss.IndexIDMap(faiss.IndexFlatIP(384)) ids = np.arange(len(doc_chunks)).astype('int64') index.add_with_ids(doc_vecs, ids) faiss.write_index(index, "doc_index.faiss") # 3. 查询时 index = faiss.read_index("doc_index.faiss") q_vec = embed(query).astype('float32') faiss.normalize_L2(q_vec) D, I = index.search(q_vec, 5) # 取最相关 5 个片段 context = "\n".join(doc_chunks[i] for i in I[0]) # 4. 拼 prompt 交给 llama.cpp / OpenAI 兼容 API 生成回答要点:
- embedding 模型与查询侧必须同一模型,跨模型向量空间不对齐,检索直接失效;
- 增量更新:业务上「新增文档」频繁时,可定期全量重建(FAISS 不支持增量训练聚类码本),或采用「主索引 + 增量小索引」双索引合并结果;
- 中等规模(<1000 万)直接用 IndexFlatIP 最稳,别为了炫技上 IVF。
5.8 工业数采场景:工艺相似工况检索
把 CNC 设备运行窗口(如最近 60s 的主轴负载、三轴坐标、进给率组成的 60×5=300 维窗口向量)入库:
# 窗口向量化:滑动窗口 → 归一化 → 入库 windows = build_sliding_windows(ts_data, window=60, dims=5) # (N, 300) faiss.normalize_L2(windows) index = faiss.IndexHNSWFlat(300, 32) index.hnsw.efConstruction = 200 index.add(windows) # 新窗口查询:找历史相似工况 → 关联当时是否报警/刀具寿命剩余 cur = latest_window(ts_data).astype('float32') faiss.normalize_L2(cur) D, I = index.search(cur, 20) # I 中的历史窗口 ID → 关联工艺参数、报警记录,做类比诊断与 Kafka/TDengine 链路组合:TDengine 存原始时序,FAISS 存「窗口向量快照」,异常窗口出现时用向量检索历史相似工况辅助根因分析。
6. 底层实现剖析(知其所以然)
6.1 IVF 的两阶段检索
- 训练(train):对训练集跑 K-means(FAISS 使用 Clustering 类,内含多次 k-means++ 初始化 + Lloyd 迭代),得到 nlist 个簇心;
- 入库(add):每条向量算与所有簇心的 L2 距离,归入最近簇,写入对应倒排列表;
- 查询(search):查询向量先算与 nlist 个簇心的距离,取前 nprobe 个簇;只在这 nprobe 个倒排列表内做精确扫描。复杂度 O(nprobe × n/nlist × d)。
nprobe = nlist 时退化为全量扫描,召回 100%。
6.2 PQ 的距离查表(ADC)
乘积量化把 d 维切成 m 段,每段独立 K-means 聚 k=2^nbits 个码字。查询时预先计算查询向量每段与对应码字表的距离(m×k 大小的距离表,查表 O(m) 得到近似距离),全程避免高维浮点运算——这就是 PQ 快且省内存的本质。IndexIVFPQ 检索 = IVF 粗筛(候选簇)→ PQ 查表精排。
6.3 HNSW 的层级图
HNSW(Hierarchical Navigable Small World)维护多层图:底层包含所有节点,上层是稀疏的「跳表」结构。查询从最高层出发,每层贪心走到局部最近,再进入下一层细化。efSearch 控制每层搜索的候选队列宽度。FAISS 的 IndexHNSWFlat 底层用邻接表存边(hnsw.neighbor),距离随边存储避免重算。
6.4 线程与 SIMD
- FAISS 用 OpenMP 做查询并行:index.search 对批量查询按 nq 切分并行;
- 距离计算对 SSE/AVX2/AVX512 做了手写 SIMD 优化(fvec_L2sqr 系列),所以要求输入 float32 且 C 连续——numpy 默认就是,但非连续切片(如 xb[::2])会触发拷贝或报错。
7. 性能调优清单
- 度量选择:文本/语义场景「归一化 + IP」;几何/物理量场景 L2;
- 索引选型阶梯:<10 万用 Flat → <1000 万用 HNSW 或 IVF-Flat → 更大用 IVF+PQ / OPQ+IVF+PQ;
- nprobe 与召回:从 nlist 的 1/16 起步,用验证集测召回率-延迟曲线,别盲调;
- 训练集:≥ nlist×39 条,与线上分布一致;OPQ 训练集可以更大;
- 批量查询:search 一次传 ≥256 条,摊销调度与内存分配开销;
- 线程数:faiss.omp_set_num_threads 与物理核匹配;Python 侧 GIL 在 search 内部会释放(FAISS C++ 执行),多线程查询可用 ThreadPoolExecutor;
- 内存:大索引用 IVF+P Q 或 SQ8(标量量化,1 字节/维);留意 IndexHNSW 图边内存,M 从 16→64 内存近似线性涨;
- 持久化:write_index 是同步阻塞全量序列化,大索引考虑后台线程保存,或按天分片索引;
- GPU:批量 > 1000 条再上 GPU;显存不足用 GpuIndexIVFPQ;
- 预热:服务启动后先跑一次空查询,避免首查延迟(页缓存/显存未就绪)。
8. 常错点与坑(26 条)
| 坑 | 现象 | 正确姿势 | |
|---|---|---|---|
| 1 | 未 train 就 add | Error: 'add' called on an untrained index | IVF/PQ 类先 train 再检查 is_trained |
| 2 | dtype 不是 float32 | 报 TypeError/ValueError 或静默错结果 | 一律 astype('float32');float64 不支持 |
| 3 | 维度不一致 | add/search 报维度错误 | index.d 与输入 xb.shape[1] 一致 |
| 4 | 非 C 连续内存 | 报 Array must be contiguous | np.ascontiguousarray(x) |
| 5 | 余弦相似度直接当距离 | 检索结果被模长大向量霸榜 | 先 normalize_L2 再建 IP 索引 |
| 6 | search 返回值维度顺序搞反 | 取错行列 | D/I 形状是 (nq, k) |
| 7 | 用默认 ID 当业务 ID | 删除/回查错乱 | 业务 ID 用 IndexIDMap + add_with_ids |
| 8 | IndexIDMap 想删数据 | remove_ids 报错/不支持 | 用 IndexIDMap2;Flat 底层本就不支持删除 |
| 9 | nprobe 忘设置 | 召回率暴跌且不自知 | 建完索引立即 index.nprobe = ... |
| 10 | 训练集太小 | K-means 不稳定,检索质量差 | 训练集 ≥ nlist×39,且分布与线上一致 |
| 11 | train 和 add 用不同分布数据 | 码本失配,检索退化 | 训练集应代表真实数据分布 |
| 12 | PQ 段数 m 不能整除维度 | 构造报错 | PQ 要求 d % m == 0;OPQ 可放宽 |
| 13 | PQ 后 reconstruct 当原向量用 | 还原结果精度损失 | 需要精确向量时用 Flat 索引或另行存储 |
| 14 | HNSW 的 M/ef 参数乱设 | 内存暴涨或召回极低 | M 从 16~32 起步;efSearch 随数据量增大 |
| 15 | GPU 索引直接 add 未训练索引 | 报错 | 先 CPU train 再 index_cpu_to_gpu |
| 16 | 反复创建 StandardGpuResources | 显存反复分配/泄漏 | 全局创建一次复用 |
| 17 | 单条查询上 GPU | 比 CPU 还慢 | GPU 适合批量 ≥256 条 |
| 18 | write_index 后换版本读 | 加载报错或崩溃 | 同版本序列化;跨版本用 index_factory 重建 + add |
| 19 | 空索引 search | 报 Error: empty index 或返回空 | 查询前判 ntotal == 0 |
| 20 | 多线程并发 add | 数据竞争、索引损坏 | add 串行;search 只读可并发 |
| 21 | ids 传 int32 | 截断/异常 | ID 必须是 int64 |
| 22 | 忘写 index_factory 的 metric 参数 | 想用余弦却建了 L2 | 余弦先归一化 + 显式 METRIC_INNER_PRODUCT |
| 23 | embedding 模型不一致 | RAG 检索结果完全无意义 | 建库与查询用同一模型、同一归一化流程 |
| 24 | 训练与查询混用未归一化/已归一化数据 | 相似度尺度不一致 | 建库与查询走同一预处理管线 |
| 25 | search 结果不校验 | 数据量小看不出问题,上线崩 | 用验证集算召回率(recall@k)再上 |
| 26 | 依赖 faiss 内部随机 | 每次 train 结果略不同 | 需要复现时固定 np.random.seed + 聚类种子设置 |
9. 与向量数据库/其他 ANN 库选型对比
| 方案 | 形态 | 优点 | 局限 | 适合 |
|---|---|---|---|---|
| FAISS | 内存库 | 最快、最灵活、C++/Python 双接口 | 无持久化/服务化,需自管 | 数据规模可控、链路自控、嵌入现有服务 |
| Milvus | 分布式服务 | 持久化、过滤、云原生、多副本 | 部署重、有网络与运维成本 | 大规模生产 RAG、多租户 |
| Qdrant | 服务 | Rust 实现、过滤强、快 | 生态较新 | 中大规模语义检索 |
| Chroma | 嵌入式 | 轻量、本地开发友好 | 性能上限低 | 原型/小项目 RAG |
| pgvector | PostgreSQL 扩展 | 与关系数据同库、SQL 过滤 | 性能不如专用索引 | 已有 PG 且量不大 |
| hnswlib | 内存库 | HNSW 极致简单 | 只有 HNSW 一种结构 | 快速集成单场景 |
| ScaNN / NGT / DiskANN | 备选 | 各自强项 | 生态/语言绑定差异 | 特殊硬件与规模诉求 |
选型建议:单机千万级以内、追求性能与可控 → FAISS;需要服务化、持久化、过滤 → 向量数据库;工业数采这类「数据在本地、链路自建」的场景,FAISS 嵌入采集网关/分析服务最合适。
10. 总结:五要点速记
- 精确检索用 IndexFlatL2/IP,超过几十万条再谈 ANN;
- 语义相似 = 归一化 + 内积,这是 RAG 场景的黄金组合;
- 中等规模选 HNSW(免 train、召回高),大规模选 IVF+PQ(省内存),更高精度加 OPQ;
- IVF/PQ 必须先 train,训练集 ≥ nlist×39 且与线上同分布;
- FAISS 是内存索引库不是数据库,持久化、增量、分布式都要自己设计(写盘分片 + 定期重建 + 双索引合并)。
FAQ 速查表
Q1:FAISS 和向量数据库是什么关系?FAISS 是底层索引引擎,多数向量数据库内部使用 FAISS/HNSW 类索引;数据库额外提供持久化、过滤、服务化、分布式。
Q2:faiss-cpu 和 faiss-gpu 能一起装吗?一般二选一;GPU 版自带 CPU 回退。装 faiss-gpu 前确认 CUDA 版本匹配(pip install faiss-gpu-cu12 等)。
Q3:nprobe 和 nlist 怎么配?nlist 通常取 4√n 量级(如 100 万条取 1024~4096);nprobe 从 nlist/16 起步,按召回率-延迟曲线调。
Q4:为什么我的余弦检索结果不对?大概率没归一化就用了 L2/IP。归一化 + IP 才是余弦,且建库与查询必须同一管线。
Q5:索引能追加数据吗?Flat/HNSW 可以持续 add;IVF/PQ 的码本在 train 后固定,追加不影响检索但码本不随新数据更新,规模翻倍后建议全量重建。
Q6:返回的 ID 能当主键用吗?默认 ID 是 0..n-1 的入库序号,删除或业务关联不可靠;业务主键用 IndexIDMap + add_with_ids。
Q7:内存不够怎么办?降维(PCA)、PQ/SQ 压缩、分片索引、或换向量数据库。
Q8:如何评估检索质量?用带标签的验证集算 recall@k(前 k 中命中真实近邻的比例),对比暴力检索结果。
Q9:C++ 里怎么用?与 Python 绑定 API 一一对应:faiss::IndexFlatL2、IndexIVFFlat、index_factory、write_index/read_index,头文件 <faiss/IndexFlat.h> 等;vcpkg 安装后链接 faiss 库。
Q10:批量查询为什么时快时慢?FAISS 对批量查询并行;单条查询无法并行且固定开销占比高,建议攒批 ≥256 条再查。