前言
上一篇我们手写了暴力版本余弦相似度与TopK检索代码。暴力检索仅适用于几千条以内的小规模向量,一旦向量规模增长到十万、百万甚至千万级别,全量遍历计算相似度会带来无法接受的延迟。
向量数据库就是为了解决海量高维向量快速相似度检索而生,是RAG知识库、多模态检索、推荐系统的核心底座。在C++后端+AI工程岗位面试中,向量数据库属于必考内容,不仅要懂原理,还要能根据业务场景做选型、识别性能瓶颈,处理持久化、分片、召回精度与检索速度之间的权衡。
本篇重点拆解FAISS底层原理,讲解Milvus分布式向量库架构,对比各类索引优劣,给出工程落地方案,附带C++调用FAISS代码、线上踩坑总结以及面试问答。
一、向量检索基础概念
1.1 什么是向量嵌入(Embedding)
文本、图片经过Embedding模型转换为固定维度浮点向量,语义相近的内容对应的向量在高维空间距离更近。RAG流程:文档切片→生成Embedding→存入向量库;用户Query→生成Query向量→向量库检索TopK相似文档→送入LLM生成答案。
1.2 距离度量方式
-
余弦相似度(Cosine):衡量向量方向夹角,不关心向量模长,文本Embedding首选;相似度越接近1,代表越相似。
-
L2欧氏距离:空间直线距离,数值越小越相似,多用于图像检索。
-
IP内积:向量点积,向量归一化后等价于余弦相似度,计算速度更快。
工程小提示:文本向量入库前,通常做L2归一化,此时IP和余弦结果完全等价,计算开销更低。
1.3 暴力检索的局限
暴力遍历:逐个计算Query向量和库内全部向量相似度,优点是100%召回,缺点是时间复杂度O(N)。向量数量到达百万级后,单次检索耗时飙升,无法用于线上高并发服务。向量索引的核心目标:牺牲一小部分召回精度,换取检索速度指数级提升。
二、FAISS深度剖析(C++原生库,RAG项目最常用)
FAISS是Meta开源、C++底层实现的向量检索库,支持CPU/GPU加速,是私有化RAG项目最主流选型。FAISS只负责内存向量索引,本身不自带持久化、分布式、元数据管理,需要业务代码自行实现落盘与分片。
2.1 FAISS主流索引类型选型
|---------------------------|-----------------------|----------------|-------------------------------------|--------------------|
| 索引类型 | 原理 | 优点 | 缺点 | 适用场景 |
| IndexFlatIP / IndexFlatL2 | 暴力全量检索 | 100%召回,无参数 | 大数据量极慢 | 小规模测试、验证效果,向量数万以内 |
| IndexIVFFlat | 倒排聚类索引,先聚类分桶,检索只搜索邻近桶 | 速度提升明显,内存占用低 | 需要训练聚类中心;nprobe控制召回,nprobe越大召回越高、越慢 | 百万级向量,通用RAG知识库,最常用 |
| IndexIVFPQ | IVF+乘积量化PQ,对向量压缩存储 | 极大节省内存,适合超大向量库 | 量化带来精度损失,调参复杂 | 千万级海量向量,内存资源紧张场景 |
2.2 FAISS核心参数:nprobe
IVF索引会把全部向量聚类划分成很多簇(centroid)。检索时,先找到和Query向量最近的nprobe个聚类桶,只在这少量桶内检索,不用遍历全部向量。
nprobe越大,检索扫描的簇越多,召回率越高,但检索耗时越长;nprobe越小速度越快,召回下降。工程上需要做压测,找到业务场景下速度与召回平衡点。
2.3 C++ FAISS极简调用示例
cpp
#include <faiss/IndexIVFFlat.h>
#include <faiss/index_io.h>
#include <vector>
#include <iostream>
int main()
{
int dim = 128; // 向量维度
int nlist = 100; // 聚类桶数量
faiss::IndexFlatL2 quantizer(dim);
faiss::IndexIVFFlat index(&quantizer, dim, nlist, faiss::METRIC_L2);
// 构造训练向量
int train_num = 2000;
std::vector<float> train_vec(train_num * dim, 0.0f);
index.train(train_num, train_vec.data());
// 插入向量
int add_num = 5000;
std::vector<float> add_vec(add_num * dim, 0.1f);
index.add(add_num, add_vec.data());
// 检索
index.nprobe = 10;
int topk = 3;
std::vector<float> query(dim,0.1f);
std::vector<float> res_dis(topk);
std::vector<faiss::idx_t> res_idx(topk);
index.search(1, query.data(), topk, res_dis.data(), res_idx.data());
for(int i=0;i<topk;i++){
std::cout << "id:" << res_idx[i] << " dist:" << res_dis[i] << std::endl;
}
// 索引持久化保存到文件
faiss::write_index(&index, "ivf.index");
return 0;
}
注意:IVF索引必须先train再add,聚类中心由训练样本生成;训练样本需要尽量覆盖向量分布,否则检索召回会很差。
2.4 FAISS工程短板
-
索引加载到内存运行,崩溃后数据丢失,需要自己做持久化写入磁盘。
-
原生不支持分布式分片,海量向量需要业务层手动分片。
-
没有内置元数据,向量ID无法直接绑定原文、文档来源,需要额外自建元数据库(MySQL/Redis)。
-
不支持高并发读写,多线程访问需要业务层加锁控制。
三、Milvus分布式向量数据库基础
Milvus是面向生产环境的分布式向量数据库,底层同样集成FAISS索引能力,在FAISS基础上补齐了分布式、持久化、元数据管理、多租户、负载均衡能力,适合企业级大规模RAG系统。
3.1 Milvus核心组件
-
RootCoord:全局元数据管理,负责集合、分片管理、DDL操作。
-
QueryCoord:查询协调器,分发检索请求、合并多分片返回结果。
-
DataCoord:数据协调器,管理数据落盘、段合并。
-
QueryNode:检索节点,加载向量索引,处理向量查询请求。
-
DataNode:数据写入节点,接收向量写入,生成数据段。
-
MinIO/S3:底层对象存储,持久化向量索引与原始数据。
3.2 数据分片与Segment机制
Milvus写入向量时,数据会写入Segment(数据段),Segment分为Growing段(内存可写入)和Sealed段(封版只读,后台构建向量索引)。后台自动执行Compaction,合并小Segment,减少检索时需要扫描的文件数量,优化查询性能。
3.3 Milvus适用场景与短板
优点:开箱即用分布式、自动分片、持久化、内置元数据、高并发读写,直接线上部署;缺点:部署组件多,资源开销大,小规模知识库使用太重,维护成本高。
四、向量库选型决策树(工程落地核心)
-
向量数量万级以内、单机私有化、追求轻量:直接FAISS + MySQL存元数据。我们前面C++私有化RAG项目优先这个方案。
-
向量百万~亿级、多机分布式、线上高并发业务:Milvus。
-
云上业务,不想运维组件:使用云厂商托管向量库。
-
嵌入式、边缘端离线场景:FAISS + 本地文件持久化。
五、检索优化工程实战要点
5.1 召回+重排(Rerank)二段检索
向量检索先粗召回:检索取Top50,快速拿到候选文档集合;再使用重排模型对候选集二次打分,筛选Top3送入LLM。可以用很小的检索开销,显著提升RAG问答准确率,工业RAG标配方案。
5.2 分片策略
单机内存放不下全部索引时,做水平分片,向量按ID哈希分到不同分片节点;查询请求广播到所有分片,每个分片返回本地TopK,业务层合并结果,再全局重排取最终TopK。分片带来网络开销,分片数量不能无限增加。
5.3 持久化与冷热分离
热数据索引常驻内存;冷数据索引保留在磁盘,按需加载。避免全量索引常驻内存导致内存打爆。
5.4 向量库常见踩坑
-
坑1:向量维度前后不一致,写入和查询Embedding维度不同,直接检索报错。
-
坑2:IVF索引训练样本太少,聚类中心不准,召回暴跌。
-
坑3:向量没有归一化,混淆IP和余弦距离,检索结果错乱。
-
坑4:海量写入不做限流,Segment疯狂分裂,查询延迟持续上涨。
-
坑5:只看检索速度,忽略召回率指标,RAG问答出现大量无关文档。
六、面试高频问答
Q1:FAISS和Milvus的区别?项目中为什么选择FAISS? A:FAISS是底层检索库,无分布式、无元数据,轻量,适合单机私有化RAG;Milvus是完整分布式向量数据库,组件多,运维成本高。我们项目是单机私有化部署,向量规模百万以内,为减少部署依赖,选用FAISS,搭配MySQL存储文档元信息,自行实现索引持久化。
Q2:IVF索引nprobe调大,会带来什么影响? A:检索扫描更多聚类中心,召回率提升;但是检索计算量变多,延迟上升、CPU开销增加,需要业务做权衡。
Q3:PQ量化的原理与副作用? A:乘积量化把高维向量拆分成多个子段,对子段聚类编码,用少量字节表示原始向量,极大压缩内存。代价是向量信息损失,召回精度下降,适合海量向量、内存紧张场景。
Q4:RAG为什么要用召回+重排二段检索? A:向量检索为速度牺牲精度,粗召回拿到大量候选;重排模型计算成本更高,但打分更精准,只在少量候选上执行,兼顾速度和检索准确度。
七、总结
-
向量数据库本质是高维向量的近似最近邻检索引擎,核心权衡点:检索速度、召回精度、内存占用。
-
FAISS轻量、C++原生,适合私有化单机RAG;Milvus是分布式向量数据库,适合大规模线上业务。
-
IVF、PQ是最核心索引,面试重点考察nprobe、训练、量化相关知识点。
-
工程不能只关注检索代码,必须配套元数据管理、持久化、分片、召回+重排策略,才能落地可用的RAG知识库。
-
向量检索是RAG系统的入口,检索质量直接决定最终LLM回答效果,是AI工程项目面试深挖重点。