向量检索深度解读

向量检索深度解读

向量检索深度解读:原理、相似度与距离的等价关系、索引加速全解析

当你在大模型应用里问一句话,系统是如何在亿级文档中”秒级”找到最相关的那几段的?答案藏在一个词里——向量检索。本文从原理到工程实现,一次性讲透。


一、先说清楚:什么是向量检索

1.1 一句话定义

向量检索(Vector Retrieval),是指在一段高维实数向量空间中,给定一个查询向量 qq,从预先建好的向量集合 {v1,v2,…,vn}\{v_1, v_2, \dots, v_n\} 中,快速找出与 qq “最接近”的 Top-K 个向量的过程。

1.2 为什么需要向量

传统关键词检索(BM25 等)依赖词项精确匹配:查询里有”苹果”,文档里也得有”苹果”才命中。但它解决不了语义层面的匹配:

  • 查询”智能手机耗电快怎么办”,文档写的是”手机续航差如何优化”——没有词项重叠,但语义高度相关。
  • 同义词、上下文理解、跨语言匹配,都是关键词检索的天花板。

向量检索的核心思路:用一段稠密向量 v∈Rdv \in \mathbb{R}^d(常见 d=768,1024,1536d=768, 1024, 1536 等)来”编码”一段文本的语义。语义越接近的文本,它们在向量空间中的距离就越近。于是”找相关文档”就变成了”找空间中最近的向量”。

1.3 典型流程

┌──────────┐    Embedding     ┌──────────┐    建索引        ┌──────────┐
│ 原始文本 │ ──────────────▶ │ 向量集合  │ ──────────────▶ │ 索引结构  │
└──────────┘                  └──────────┘                  └────┬─────┘
                                                                │
                          ┌──────────┐    检索 Top-K            │
                          │ 查询向量  │ ◀───────────────────────┘
                          └──────────┘
  1. 离线阶段:把所有文档切块、过 Embedding 模型,得到向量集合,并建立索引。
  2. 在线阶段:用户 Query → 同一 Embedding 模型 → 查询向量 → 在索引里找 Top-K。

看起来简单,但里面藏着三个关键问题:怎么度量”接近”?相似度和距离怎么对等?索引怎么加速? 下面逐一拆解。


二、原理篇:怎么度量”接近”

“接近”需要一个数学度量。常见有三类:相似度、距离、还有一些变形度量。我们分别看。

2.1 余弦相似度(Cosine Similarity)

最常用于文本向量。定义为两个向量夹角的余弦值:

CosSim(a,b)=a⋅b∥a∥2 ∥b∥2\text{CosSim}(a, b) = \frac{a \cdot b}{\|a\|_2 \, \|b\|_2}
  • 取值范围 [−1,1][-1, 1],越大越相似。
  • 只关注方向,不关注模长。这在语义检索中合理:一段长摘要和它的核心句,方向应该一致,模长不同没关系。
  • 工程关键:如果向量事先 L2 归一化(即 ∥v∥2=1\|v\|_2=1),则 CosSim(a,b)=a⋅b\text{CosSim}(a,b) = a \cdot b,退化成一次点积。这就是为什么几乎所有的向量库(FAISS / Milvus / Qdrant…)都默认”归一化 + 内积”等价于”余弦相似度”。

2.2 点积(Dot Product / Inner Product)

IP(a,b)=a⋅b=∑i=1daibi\text{IP}(a, b) = a \cdot b = \sum_{i=1}^{d} a_i b_i
  • 如果向量未归一化,点积同时受模长和方向影响,是一个”模长加权”的相似度。
  • 如果向量已归一化,点积 = 余弦相似度。
  • 实际应用中,很多 Embedding 模型输出就直接是归一化向量(如 OpenAI text-embedding-3-*),因此检索时直接用点积即可。

2.3 欧氏距离(L2 Distance)

L2(a,b)=∑i=1d(ai−bi)2\text{L2}(a, b) = \sqrt{\sum_{i=1}^{d}(a_i - b_i)^2}
  • 取值范围 [0,+∞)[0, +\infty),越小越接近。
  • 同时考虑方向和模长。对图像、音频等模态的向量更常用。

2.4 其他度量(了解即可)

度量适用场景特点
曼哈顿距离 L1稀疏/离散数据对异常值更鲁棒
汉明距离二值/哈希向量位级比较,超快
杰卡德距离集合/稀疏二值集合差异度量

实际工程里,99% 的场景就是 余弦 / 内积 / L2 三选一。下面专门讲清楚它们的相互关系,这是很多人最容易绕晕的地方。


三、核心答疑:相似度与距离是怎么”对等”的

读者常困惑:检索系统一会儿说”相似度越大越好”,一会儿说”距离越小越好”,一会儿又说”距离和相似度可以互相转换”,到底怎么转?我们从公式严格推导。

3.1 先厘清两个概念

  • 相似度(Similarity):越大越像。典型如余弦相似度、点积。
  • 距离(Distance):越小越像。典型如欧氏距离、曼哈顿距离。

排序方向是反的:相似度要 降序 取 Top-K,距离要 升序 取 Top-K。

3.2 关键结论:归一化下三者等价

命题:当向量 a,ba, b 都满足 ∥a∥2=∥b∥2=1\|a\|_2 = \|b\|_2 = 1(L2 归一化)时,“余弦相似度最大”、“点积最大”、“欧氏距离最小”三者给出完全相同的 Top-K 排序。

证明分两步。

第一步:余弦 = 点积。

CosSim(a,b)=a⋅b∥a∥∥b∥=a⋅b1⋅1=a⋅b\text{CosSim}(a,b) = \frac{a \cdot b}{\|a\|\|b\|} = \frac{a \cdot b}{1 \cdot 1} = a \cdot b

所以归一化后,按余弦降序 ⇔ 按点积降序,排序结果完全一致。

第二步:点积与 L2 距离的单调关系。

展开 L2 距离的平方:

∥a−b∥22=∑i=1d(ai−bi)2=∑ai2−2∑aibi+∑bi2=∥a∥2−2(a⋅b)+∥b∥2\|a - b\|_2^2 = \sum_{i=1}^{d}(a_i - b_i)^2 = \sum a_i^2 - 2\sum a_i b_i + \sum b_i^2 = \|a\|^2 - 2(a\cdot b) + \|b\|^2

归一化时 ∥a∥2=∥b∥2=1\|a\|^2 = \|b\|^2 = 1,代入:

∥a−b∥22=2−2(a⋅b)\boxed{\|a - b\|_2^2 = 2 - 2(a \cdot b)}

也就是说:L2 距离的平方 与 点积 是严格的线性关系(斜率 −2-2,截距 22)。

  • a⋅ba \cdot b 越大 ⇔ ∥a−b∥22\|a-b\|_2^2 越小 ⇔ ∥a−b∥2\|a-b\|_2 越小。

而平方函数在非负区间单调递增,所以平方前后的排序也完全一致。

综上,归一化条件下:

余弦相似度↑  ⟺  点积↑  ⟺  L2 距离↓\text{余弦相似度} \uparrow \iff \text{点积} \uparrow \iff \text{L2 距离} \downarrow

三者给出完全相同的 Top-K。

3.3 一张图记住转换关系

        余弦相似度 CosSim         (归一化时)          点积 IP
              ↑  =                                     ↑ =
              |                                        |
        (1 - Cos)/2  ←—— 单调线性 ——→  L2²  ←—— 单调线性 ——→  (归一化)
              |                                        |
            L2 距离                                  L2 距离²
        (越小越相似)                            (越小越相似)

记忆口诀:

  • 归一化前:三种度量各有侧重,结果可能不同。
  • 归一化后:三种度量数学等价,排序完全一致,只是”写法”不同。

3.4 工程上为什么要做这种转换

  1. 统一算法实现:很多 ANN 算法(如 HNSW)是基于”距离”设计的”近邻图”。如果用余弦,要么算法内部把向量归一化后当 L2 用,要么把相似度转成距离 dist=1−sim\text{dist} = 1 - \text{sim} 喂进去。有了上面的等价关系,我们就可以放心地”用 L2 跑出余弦的结果”。
  2. 省存储/算力:归一化向量只需存方向,且距离比较可省去开方(直接比平方距离)。
  3. 跨库迁移:理解了等价性,从 FAISS 的 IndexFlatIP 切到 IndexFlatL2、或从 Milvus 的 COSINE 切到 L2,归一化后结果完全一致,便于选型比较。

一句话总结:只要向量做了 L2 归一化,余弦相似度、内积、欧氏距离这三种”看起来不同”的度量,在排序意义上完全等价。这是向量检索里最容易被忽略、却最关键的一个数学事实。

3.5 补充答疑:余弦相似度是 [-1,1],为什么人脸识别产品显示 0~100%?

这是工程落地时一个非常典型的”分数映射”问题。

问题根源:余弦相似度的理论取值范围是 [−1,1][-1, 1]。但在人脸识别这类场景里,Embedding 模型输出的特征向量经过训练后,不同人脸之间的余弦相似度几乎不会出现负值——两个不同人的特征向量夹角通常落在 [0°,90°][0°, 90°] 内,对应的余弦值在 [0,1][0, 1]。于是实际有效区间就被压缩成了 [0,1][0, 1]。

但 [0,1][0, 1] 这个范围直接给用户看体验很差:一张陌生人脸比对结果”0.42”是什么意思?过不过阈值?所以产品侧会做一层分数映射,把它转成更直观的 0~100。常见做法有几类:

① 线性映射(最简单)

直接把 [0,1][0, 1] 线性拉伸到 [0,100][0, 100]:

Score=CosSim×100\text{Score} = \text{CosSim} \times 100
  • 余弦 1.0 → 100 分(完全相似)
  • 余弦 0.0 → 0 分(完全不相似)

简单粗暴,但有个问题:人脸识别中”同人”的余弦往往已经很高(比如 0.6 以上),而”非同人”普遍在 0.3 以下。线性映射后,同人显示 60 分、非同人显示 30 分,区分度看起来不强。

② 截断 + 线性映射(更常见)

只取实际有区分度的区间 [smin⁡,smax⁡][s_{\min}, s_{\max}] 做线性拉伸:

Score=CosSim−smin⁡smax⁡−smin⁡×100\text{Score} = \frac{\text{CosSim} - s_{\min}}{s_{\max} - s_{\min}} \times 100

比如取 smin⁡=0.2,smax⁡=0.95s_{\min}=0.2, s_{\max}=0.95:

  • 余弦 0.95 → 100 分
  • 余弦 0.2 → 0 分
  • 中间的 0.6 → 约 53 分

这样”同人/非同人”的分数差距被拉大,用户观感更清晰。很多商业 SDK 的相似度百分比就是这种做法,只是参数 smin⁡,smax⁡s_{\min}, s_{\max} 是各自调出来的经验值。

③ 平滑/非线性映射

为了进一步放大区分度,有的产品会套一层非线性函数,比如:

Score=11+e−k(CosSim−θ)×100\text{Score} = \frac{1}{1 + e^{-k(\text{CosSim} - \theta)}} \times 100

这是一条 Sigmoid 曲线,θ\theta 是阈值拐点,kk 控制陡峭程度。在阈值附近分数变化最剧烈,远离阈值则快速饱和到 0 或 100。好处是”过/不过”的决策感更强。

④ 直接归一化到百分比

也有产品把余弦相似度做”归一化”语义解读:认为负值在实际中不出现,于是约定 max⁡(CosSim,0)×100%\max(\text{CosSim}, 0) \times 100\%,即”相似度 = 余弦值 × 100%“。这是①的特例。

关键认知:

看到的分数背后的本质
0~100% 的相似度几乎都是余弦相似度经过映射函数变换后的展示值
不同厂商的”85分”不可直接横向比较——映射函数、阈值、训练模型都不同
真正决定”是不是同一人”不是绝对分数,而是该产品阈值(threshold) 在映射后对应的值

所以下次看到人脸 SDK 说”相似度 92%“,要明白:它底层大概率是一个 [0,1][0,1] 区间的余弦相似度,经过截断+线性拉伸(或 Sigmoid)后映射到 [0,100][0,100] 的结果。算法层面排序没变,变的只是展示刻度。

拓展一句:这种”原始分数 → 友好分数”的映射不只用于人脸。声纹识别、指纹比对、甚至 RAG 系统里给用户展示”相关度”时,都会做类似的标定。本质都是把一个抽象的相似度数值,映射到用户能直观理解的语义刻度上。


四、索引加速篇:亿级向量怎么秒级检索

到这里都是”暴力检索”(Brute Force)的范畴:把 qq 和库里每个 viv_i 算距离,排序取 Top-K。复杂度 O(n⋅d)O(n \cdot d),库大了根本跑不动。

真实系统靠的是 近似最近邻搜索(ANN, Approximate Nearest Neighbor):以”放弃一点点精度”换”几个数量级的速度提升”。下面讲最常见的几种索引加速方案。

4.1 暴力检索 Flat(基线)

  • 精确,但 O(n⋅d)O(n \cdot d)。
  • 库小(< 10万)时完全够用,没必要上 ANN。
  • FAISS 中对应 IndexFlatL2 / IndexFlatIP。

4.2 量化压缩:PQ / IVF-PQ

乘积量化(Product Quantization, PQ) 的思路:把 dd 维向量切成 mm 段,每段独立做 K-Means 聚成 kk 个簇心,用簇心编号代替原始向量。一个 1024 维 float32 向量原本占 4KB,PQ 后可能只占几十字节。

  • 压缩:从存原始向量 → 存”每段属于哪个簇心编号”。
  • 距离近似:查表算距离。预先算好每个簇心之间的距离表,检索时查表求和即可。
  • IVF-PQ:在 PQ 之上加 IVF(倒排+聚类)粗筛,进一步加速。经典的大规模检索方案。

代价:有量化误差,召回率下降;适合”海量 + 内存敏感”场景。

4.3 倒排索引 IVF

思路:先对全部向量做 K-Means 聚成 kk 个簇,检索时只算 qq 所在簇(及邻近几个簇)内的距离,把 nn 降到 n/kn/k 量级。

  • 优点:实现简单,可控性强(nprobe 调粗细)。
  • 缺点:簇边界附近会漏掉邻近簇里的近邻;高维时聚类效果衰减。
  • FAISS 中对应 IndexIVFFlat / IndexIVFPQ。

4.4 图索引:HNSW(当前 SOTA 之一)

HNSW(Hierarchical Navigable Small World) 是目前最常用的图索引算法,思路分层跳表 + 近邻图:

  • 构建一个”小世界图”:每个向量是节点,连到它的若干近邻。
  • 分多层:上层稀疏、跨度大用于”远距离跳跃”;下层稠密、精度高用于”局部细化”。
  • 检索时从顶层入口点出发,逐层贪心向 qq 靠近,底层做精细近邻搜索。

特点:

  • 召回率/延迟平衡极佳,百万级库毫秒级。
  • 内存占用大(要存图),不适合超大规模 + 低内存场景。
  • 关键参数:M(每个节点的连接数,影响精度和内存)、efConstruction(建图时候选数,影响建索引质量和速度)、efSearch(检索时候选数,影响召回和延迟)。
  • FAISS 对应 IndexHNSWFlat,Milvus / Qdrant 默认都支持。

4.5 各索引方案对比一览

索引类型原理召回延迟内存适用场景
Flat暴力100%高中小库、需精确
IVF-Flat聚类粗筛中高中中中等规模、可调召回
IVF-PQ聚类+量化中低极低海量、内存敏感
HNSW分层图高极低高中大规模、低延迟
** DiskANN**图+磁盘高中低超大规模、磁盘存储

4.6 选型经验法则

  • 库 < 10万:直接 Flat,省心。
  • 库 10万~1000万、延迟敏感:HNSW。
  • 库 > 1000万、内存吃紧:IVF-PQ 或 DiskANN。
  • 同时要精确 Top-K:上重排(Rerank):先 ANN 召回 Top-100,再用精确距离重排取 Top-10。这是 RAG 系统的标配。

五、把整条链路串起来:一次向量检索的全过程

以 RAG(检索增强生成)场景为例:

  1. 离线建库

    • 文档切片 → Embedding 模型 → 得到向量集合。
    • 若用余弦:向量 L2 归一化(关键,决定后续等价性)。
    • 选择索引:百万级选 HNSW,建好后落盘。
  2. 在线检索

    • 用户 Query → 同一 Embedding 模型 → 查询向量(同样归一化)。
    • 进入 HNSW,efSearch 控制”在图里探索多深”,返回 Top-100 候选。
    • 粗排:HNSW 返回的就是近似的 Top-100。
    • 精排 Rerank:对这 100 个候选用精确距离(点积/L2)重新打分排序,取 Top-5。
    • 把 Top-5 的原文片段拼进 Prompt,交给大模型生成回答。

整条链路里,“归一化”这一步决定了你能否在余弦/内积/L2 之间自由切换;“ANN 索引 + Rerank”这一组合决定了你能否在亿级数据上做到毫秒级响应。


六、进阶答疑:向量数据库怎么在不重启的情况下动态更新?

前面讲的索引(HNSW、IVF-PQ 等)一旦”建好”,看起来像是静态的。但实际业务里向量是持续新增的——新文档入库、新用户注册人脸、新商品上架。向量数据库(如 Milvus、Qdrant、Weaviate)是怎么做到一边在线检索、一边实时写入新向量,还不用重启服务的?

这背后是一套”分层存储 + 增量合并 + 读写分离”的机制。下面以 Milvus 为例拆解。

6.1 核心矛盾:索引不好改,但数据要常更

先理解为什么”动态更新”是个难题:

  • HNSW 的图结构是”贪心构建”出来的,每加一个节点要重新连边、维护分层。可以增量加,但删除很麻烦——图里一个节点连着很多邻居,硬删会破坏图的连通性。
  • IVF-PQ 的簇心是离线 K-Means 算好的,新数据来了不一定落进合适的簇,重算 K-Means 代价巨大。
  • PQ 量化表 也是预计算的,数据变了表就得重算。

结论:成熟的 ANN 索引几乎都是”适合批量建、不适合频繁改”的。但业务又要求实时写入。怎么破?

6.2 解法:Growing Segment + Sealed Segment + 后台合并

Milvus(以及大多数向量数据库)采用的是类似 LSM-Tree 的思路,把”写”和”查”解耦:

       写入路径                              合并/落盘路径
┌──────────┐  新向量    ┌────────────┐  攒够  ┌──────────────┐
│ 客户端   │ ────────▶ │ Growing    │ ────▶ │ Sealed       │
│ insert() │           │ Segment    │       │ Segment      │
└──────────┘           │ (内存/可写) │       │ (落盘/只读)   │
                       └──────┬─────┘       └──────┬───────┘
                              │                     │
                              │   检索时同时查两处    │
                              ▼                     ▼
                       ┌─────────────────────────────────┐
                       │     Query: 合并 Growing + Sealed │
                       └─────────────────────────────────┘

① Growing Segment(增长段,可写)

新写入的向量先进入一个内存中的 Growing Segment。它还没有建成正式 ANN 索引,就是一个可追加的列表(本质上是 Flat 暴力结构)。写入极快,直接 append。

  • 检索时,Growing 段里用暴力计算(Flat)参与 Top-K。
  • 因为这段数据量小(比如攒到一定条数或一定时间),暴力算也很快,不影响整体延迟。

② Sealed Segment(封闭段,只读 + 索引)

当 Growing Segment 攒够量(比如达到 maxInsertBatchSize 或触发 Compaction),后台任务会把它”封口”:

  • 对这批数据正式建 ANN 索引(HNSW / IVF-PQ)。
  • 连同索引一起落盘,变成不可变(immutable)的 Sealed Segment。
  • 此后该段走索引加速检索,不再可写。

③ 检索时合并两段结果

一次查询同时扫描:

  • 所有 Sealed Segment → 走 ANN 索引,快。
  • 当前 Growing Segment → 走 Flat 暴力,因为数据少也快。

各自拿到 Top-K 候选后,做一次归并排序取全局 Top-K 返回。对用户完全透明,感觉就是在查一个统一的库。

这就是”不重启也能更新”的核心:写入只碰 Growing 段,索引只建在 Sealed 段上,两者分离,互不阻塞。

6.3 删除怎么办:不真删,先标记

HNSW 图里直接摘节点会破坏连通性,所以向量数据库普遍用标记删除(Soft Delete):

  • 删除时只在元数据里打一个 tombstone(墓碑)标记。
  • 索引结构原封不动,检索时拿到结果后再过滤掉带墓碑的向量。
  • 物理清理放到后台 Compaction 阶段:重建该 Segment 时跳过被标记的向量,新 Segment 里它们就自然消失了。

代价:被删向量在物理清理前仍占内存、仍可能被 ANN 召回(只是最后被过滤)。所以删除有”延迟生效”的特性。

6.4 更新 = 删 + 插

向量本身不可变(改了向量值,索引里的位置就不对了),所以”更新一条向量”实际操作是:

  1. 删旧向量(打墓碑)。
  2. 插入新向量(进 Growing 段)。

6.5 Compaction:后台整理碎片

随着 Growing→Sealed 的不断转换,会产生很多小 Segment(碎片),影响检索效率。Milvus 会定期做 Compaction:

  • 把多个小 Sealed Segment 合并成大 Segment。
  • 重建合并后的索引。
  • 丢弃墓碑标记的向量。
  • 全程在后台进行,不影响在线读写。

这就是为什么向量库跑久了需要”compact”一下——和 LSM-Tree 数据库(如 RocksDB、HBase)的 compaction 思想一脉相承。

6.6 一张表总结动态更新机制

操作怎么做对检索影响生效时机
插入进 Growing Segment(Flat 追加)立即参与检索(暴力算)实时
删除打墓碑标记,结果过滤召回后被过滤实时可见
更新删旧 + 插新同上实时
封口建索引Growing→Sealed,建 ANN该段切到索引加速后台异步
Compaction合并小段、清理墓碑提升检索效率后台定时

6.7 工程上的几个认知点

  1. “写进去立即能查到”是有代价的:刚写入的向量走 Flat 暴力检索,Growing 段太大时会拖慢查询。所以有 maxInsertBatchSize 类参数控制”何时封口建索引”。
  2. “删除不是立即物理消失”:墓碑在 Compaction 前一直占空间,高频率删改场景要注意监控 Segment 数量和内存。
  3. “索引质量在动态场景下会波动”:每个 Sealed Segment 的索引是独立建的,段越多、越小,整体召回质量越接近各段召回的”平均”。Compaction 合并后召回通常会回升。这也是为什么生产环境要定期 compact。
  4. Milvus 2.x 的存算分离更进一步:Query Node(检索)和 Data Node(写入/建索引)分开部署,Sealed Segment 存在共享对象存储上,Query Node 按需加载。写和查彻底解耦,扩缩容互不影响。

6.8 补充答疑:检索时是把所有向量都加载到内存吗?

这是实际使用 Milvus 时最高频的疑问之一。答案是:不一定,取决于加载策略,但绝不是”无脑全装”。

背景:为什么关心内存

向量很”吃”内存。1000 万条 1024 维 float32 向量,原始数据就是 1000万×1024×4字节≈38GB1000\text{万} \times 1024 \times 4\text{字节} \approx 38\text{GB}。加上 ANN 索引结构(HNSW 的图边、IVF 的簇心表等),总占用可能更高。如果每次查询都把全量向量塞进内存,机器根本扛不住,也不经济。所以 Milvus 有一套精细的加载/释放(Load/Release)机制。

数据存储的三层位置

┌──────────────────────────────────────────────────┐
│  Query Node 内存(检索走这里,最快)              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
│  │ Segment A│  │ Segment B│  │ Segment C│  已加载 │
│  └──────────┘  └──────────┘  └──────────┘        │
└──────────────────────────────────────────────────┘
        ▲ load               ▼ release
┌──────────────────────────────────────────────────┐
│  本地磁盘 / 共享对象存储(S3/MinIO,持久化)       │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
│  │ Segment A│  │ Segment D│  │ Segment E│  未加载 │
│  └──────────┘  └──────────┘  └──────────┘        │
└──────────────────────────────────────────────────┘
  • 对象存储(S3/MinIO/本地磁盘):所有 Sealed Segment 的原始数据和索引都持久化在这里。这是”全量”的真实存放地,不在内存。
  • Query Node 内存:只有被 load 进来的 Segment(原始向量 + 索引结构)才驻留在这里,检索直接读内存。
  • Growing Segment:在 Data Node 的内存里,因为还没落盘。

三种加载策略

① 全量加载(load 全 Collection)

collection.load()

调用后,Milvus 把这个 Collection 下所有 Sealed Segment 的向量 + 索引都加载到 Query Node 内存。此时检索全部走内存,最快,但内存占用 = 全量数据大小。

  • 适合:数据量能装进内存的场景。
  • 这就是”是不是全装到内存”的答案之一——如果你主动 load 了整个 Collection,确实就是全装了,但这是你选的,不是默认行为。

② 部分加载 / 按需加载

Milvus 2.3+ 引入了 DiskANN 索引 和部分加载能力:

  • 用 DiskANN 时,只有图的”上层导航结构”驻留内存(很小),原始向量和下层图存在磁盘上,检索时按需从磁盘读取相关页(pre-fault 到 page cache)。内存占用大幅降低。
  • 也可以只 load 某些 Segment、release 不常用的,做到”热数据在内存、冷数据在磁盘”。

③ Release(释放)

collection.release()

主动把某个 Collection 从内存里卸载,Segment 数据仍在对象存储上。适合”这批数据暂时不查了,腾内存给别的 Collection”。

那”查询时”具体怎么走

一次检索查询的内存访问路径:

  1. 查询路由到 Query Node。
  2. 已 load 的 Sealed Segment:向量 + 索引在内存里,直接走 ANN 检索。
  3. Growing Segment:在 Data Node 内存里,走 Flat 暴力(数据量小)。
  4. 未 load 的 Segment:查不到(除非用 DiskANN 走磁盘,但会慢)。
  5. 各段结果归并 → 全局 Top-K。

所以:“查询时把所有向量都加载到内存吗?“——只有你 load 了的部分在内存,没 load 的根本不参与查询。这是一把”显式开关”,使用者要为”加载多少”负责。

工程上的实操建议

场景建议策略理由
数据量 < 内存collection.load() 全量加载检索最快,操作最简单
数据量 > 内存、有热点部分 load + DiskANN热数据内存、冷数据磁盘,省内存
多 Collection 共存、轮流用查时 load、不查时 release内存复用,避免 OOM
超大规模、低延迟要求扩 Query Node 节点(横向扩容)内存总量不够就加机器

关键认知

  1. “加载”是你显式触发的,不是查询时自动发生的。load 是”准备阶段”,查询是”使用阶段”。没 load 的 Collection 直接查会报错或查不到。
  2. 内存占用 ≈ 向量原始大小 + 索引结构大小。HNSW 的图边会额外占内存,PQ 量化后只占原始数据的几十分之一。
  3. 存算分离架构下,load 是把数据从对象存储”拉”到 Query Node 内存,多副本 load 还能分散到多个节点,单机内存不够就加 Query Node 数量。
  4. Milvus 没有”查询时临时全量读磁盘”的设计(不像传统数据库按页 cache)。要么 load 进内存,要么用 DiskANN 走磁盘索引。这是因为向量检索是”扫全库比距离”的密集计算,临时 IO 撑不起延迟要求。

一句话总结:Milvus 检索时不是无脑把所有向量塞进内存,而是由使用者通过 load/release 控制”哪些 Segment 进内存”。进内存的走索引快检索,没进的不参与查询;数据太大装不下时,用 DiskANN 让索引分块驻留磁盘 + 按需读取,或横向扩 Query Node 节点。内存管理是向量数据库运维的核心命题。


七、常见误区 & 避坑指南

  1. “我用的是余弦,所以不用归一化” —— 错。余弦相似度本身归一化在分母里,但不归一化时,内积 ≠ 余弦,很多索引库默认是内积,结果会不一样。要么显式选 COSINE,要么自己归一化后选 IP。
  2. “HNSW 召回高,所以一定准” —— 不一定。HNSW 是近似算法,efSearch 太小会漏召,需要在召回率和延迟间权衡。
  3. “距离小就等于相似度高” —— 只在同一度量体系下成立。拿 L2 距离和未归一化的点积比,排序可能完全不同。
  4. “索引建好就不用调了” —— 数据分布会漂移,定期重建/增量更新索引是必要的。
  5. “Embedding 模型随便换” —— 换模型 = 向量空间变了,老索引作废,必须全量重建。

八、总结

向量检索的核心问题,可以浓缩成几句话:

  1. 原理:把语义编码成高维向量,“找相关”变成”找空间最近邻”。
  2. 相似度与距离的等价:向量 L2 归一化后,余弦相似度 = 内积,且与 L2 距离呈严格线性单调关系,排序完全等价。这是工程上自由切换度量的数学基础。而产品里看到的 0~100% 相似度,几乎都是底层余弦值经过映射函数变换后的展示刻度。
  3. 索引加速:暴力检索扛不住规模,靠 ANN(IVF / PQ / HNSW / DiskANN 等)在”精度换速度”之间做权衡,配合 Rerank 精排保证最终质量。
  4. 动态更新:向量数据库靠”Growing 可写段 + Sealed 只读索引段 + 后台 Compaction”的三段式架构,把”索引不好改”转化为”攒批重建”,实现不停机实时增删改。

理解了这几点,再去看 FAISS / Milvus / Qdrant 的文档和参数,就会顺畅很多。


想深入实践?建议路径:先用 FAISS 跑通 IndexFlatL2 → 再试 IndexIVFPQ → 最后上 IndexHNSWFlat,亲手调一次 efSearch,感受召回率与延迟的此消彼长。理论就到此为止,剩下的都是工程手感。

评论