向量检索原理入门:从 ANN 到 HNSW

向量检索原理入门:从 ANN 到 HNSW

做 RAG、推荐系统时,百万级向量检索慢得像蜗牛?cosine 和 L2 该用哪个?面试官一问「召回率怎么算」就卡壳?这篇文章用最直观的方式,把向量检索的核心原理讲透,并标注每个考点的面试追问方向。


目录

  1. 为什么不能用暴力搜索?
  2. ANN:用近似换速度
  3. HNSW:分层导航的小世界图
  4. cosine vs L2:方向还是距离?
  5. 参数调优:召回率与延迟的权衡
  6. 面试考点速查表

1. 为什么不能用暴力搜索?

先看最朴素的做法:精确最近邻检索。给定一个查询向量,遍历库里所有向量,算一遍距离,取最近的 k 个。

python 复制代码
import numpy as np

def brute_force(query, vectors, k=10):
    """暴力扫描:O(n) 线性遍历"""
    dists = np.linalg.norm(vectors - query, axis=1)
    idx = np.argsort(dists)[:k]
    return idx, dists[idx]

问题在哪?

  • 复杂度 O(n):100 万条 × 768 维,每次查询要算 100 万次距离,延迟轻松到几十毫秒甚至上百毫秒。
  • 维度灾难(curse of dimensionality):维度越高,向量在高维空间越稀疏,所有点之间的距离趋同,精确检索的「精确」意义也在减弱。

🎯 面试考点 :面试官问「为什么不用暴力搜索」,别只答「太慢」,要说出 O(n) 线性扫描 + 维度灾难 两层原因。


2. ANN:用近似换速度

ANN(Approximate Nearest Neighbor,近似最近邻) 的核心思想:以可接受的精度损失,换取数量级的速度提升

它不保证返回一定 是真正的最近邻,只保证返回足够接近的邻居。

核心指标:召回率 Recall@k

ini 复制代码
Recall@k = ANN 返回的 k 个结果中,有多少比例是真正的 top-k 最近邻

例:精确检索返回 [A,B,C],ANN 返回 [A,C,D],则 recall@3 = 2/3

🎯 面试考点 :ANN 的「近似」不是玄学,是有量化指标的。Recall@k 与延迟的权衡 是工程落地的核心,也是贯穿全文的主线。


3. HNSW:分层导航的小世界图

HNSW(Hierarchical Navigable Small World)是目前最主流的 ANN 算法。拆开看:

3.1 NSW:单层小世界图

NSW 本身是单层 的。它的「小世界」结构借鉴了现实社交网络的六度分隔思想:

  • 每个节点只连接少数几个邻居
  • 但通过少数几步就能到达任意节点

搜索时用贪心算法导航:从入口节点出发,每次走到「比当前更接近目标」的邻居,直到无法再改进。

3.2 HNSW:加一层「跳表」

HNSW 在 NSW 之上加了分层,借鉴**跳表(Skip List)**思想:

  • 高层 :节点少、连接稀疏 → 负责快速跨越长距离
  • 底层 :节点全、连接稠密 → 负责精细定位

搜索时从最高层入口开始,逐层贪心逼近、向下钻取,最后在底层做精细检索。

🎯 面试考点NSW 是单层的,HNSW 才是分层------这是最容易混淆的硬伤。面试官一听就知道你有没有吃透。


4. cosine vs L2:方向还是距离?

4.1 数学定义

公式(竖线在表格里会被误判为分隔符,所以单独放):

text 复制代码
cosine:cos(θ) = (A·B) / (|A| × |B|)
L2:√(Σ(ai-bi)²)
cosine 相似度 L2 距离
衡量 方向一致性 绝对位置差异
范围 -1, 1,越大越相似 [0, +∞),越小越近
对模长 不敏感 敏感

4.2 归一化后:两者等价(核心考点)

若 A、B 都是单位向量(|A|=|B|=1):

css 复制代码
|A-B|² = |A|² + |B|² - 2A·B = 2 - 2cos(A,B)

所以:L2 距离越小 ⟺ cosine 相似度越大,两者排序完全一致。

但注意 :等价的前提是都归一化。不归一化时,一个向量模长很大但方向接近,cosine 高但 L2 远,排序可能完全不同。

🎯 面试考点 :这是本题最核心的考点。很多系统(如 Faiss)先归一化再用 L2,因为 L2 计算更快、效果等价。

4.3 场景怎么选?

判断标准不是「文本用 cosine、聚类用 L2」这种死记,而是问:我关心的是方向还是绝对位置?

  • 文本 embedding:关心语义方向,且模长受文本长度影响 → 默认 cosine(或归一化后 L2)
  • 聚类(K-means):基于质心欧氏距离 → 默认 L2;特征归一化后用 cosine 也等价
  • 异常检测:偏离中心的距离更直观 → L2

5. 参数调优:召回率与延迟的权衡

HNSW 参数分构建期查询期两组:

5.1 构建期参数(决定「图长什么样」)

参数 含义 影响
M 每个节点最大邻居数 越大图越稠密、召回率越高,但内存和构建时间越大(典型 16~64)
M_max 底层最大连接数(≈2M) 控制底层稠密程度,防止退化
ef_construction 插入时搜索的候选集大小 越大插入找邻居越准、图质量越高,但构建越慢(典型 100~200)

5.2 查询期参数(决定「怎么搜」)

参数 含义 影响
ef_search 查询时维护的候选集大小 越大召回率越高、延迟越高------召回率 vs 延迟的核心旋钮

5.3 实战:FAISS 代码

python 复制代码
import faiss

d = 768  # 向量维度
M = 16
ef_construction = 100
ef_search = 50

# 构建索引
index = faiss.IndexHNSWFlat(d, M)
index.hnsw.efConstruction = ef_construction
index.add(vectors)  # 构建期

# 查询
index.hnsw.efSearch = ef_search
D, I = index.search(query, k=10)  # 查询期

⚠️ 踩坑提醒构建期参数(M、ef_construction)线上已固定,改它们要重建索引 。线上延迟超标,只能调 ef_search ;而且 ef_search 要先离线评测 recall-延迟曲线再定值,不是拍脑袋。


6. 面试考点速查表

考点 一句话答案 加分点
为什么用 ANN? 用可接受的召回率损失换数量级速度提升 说出 O(n) 扫描 + 维度灾难
Recall@k 是什么? ANN 返回结果中真正 top-k 的比例 强调它是工程落地核心指标
HNSW 原理? 分层(跳表)+ 小世界图(贪心导航) 强调 NSW 是单层、HNSW 才分层
cosine vs L2? cosine 看方向、L2 看距离 归一化后等价:|A-B|²=2-2cos
怎么调参? 构建期管图质量、查询期管权衡 强调 ef_search 靠实测、构建期不能混调

总结:核心框架一句话背下来

scss 复制代码
向量检索 = 距离度量 + 索引算法 + 参数调优

距离度量:cosine 看方向 / L2 看距离 → 归一化后等价
索引算法:HNSW = 分层(跳表) + 小世界(贪心导航)
参数调优:构建期(M, ef_construction)管图质量
          查询期(ef_search)管召回率 vs 延迟
核心指标:Recall@k 与延迟的权衡

💡 面试加分建议

  1. 主动串知识点:比如方案设计时提到「归一化后 L2 更快」,体现体系化理解。
  2. 提备选方案:IVFPQ 更省内存但召回率略低、暴力扫描延迟不可接受------说明取舍比只说「选 HNSW」更有说服力。
  3. 强调工程思维:先离线评测、再上线,这是面试官最看重的。

🛠️ 实战背书

这篇不是纯理论------我的 RAG 项目(FastAPI + ChromaDB)底层用的正是 hnswlib 的 HNSW,文中每个参数(M、ef_construction、ef_search)都在真实问答管线上验证过。面试时讲"我用过 + 知道为什么这么调",比只会背定义值钱得多。


如果这篇文章对你有帮助,欢迎点赞收藏~ 后续可以继续更新:IVFPQ 量化原理、向量数据库(Milvus/FAISS/Qdrant)选型实战、RAG 检索优化实战。 🚀

相关推荐
Augustzero1 小时前
为什么高性能调度器都在“偷任务”?从无锁队列看懂工作窃取
c++·后端
众人皆醒我独醉1 小时前
Embedding:把词变成向量——AI 理解语义的唯一方式
人工智能·面试·llm
Leo2821 小时前
异步任务链路如何不断链:基于 OpenTelemetry 的 Trace 设计
后端
技术长镜头1 小时前
别再死记 Record、Gap、Next-Key:沿一条 SQL 看懂 InnoDB 锁
后端·mysql
晚安code1 小时前
Java四大函数式接口一篇讲透:配上Stream流式计算处理集合
后端
晴殇i1 小时前
最近在 Github 名字叫“马尾辫”,这个真的很有趣看到头像
前端·后端·开源
IT_陈寒2 小时前
Vite打包时静态资源404?加个斜杠就能解决
前端·人工智能·后端
程序员爱钓鱼2 小时前
Go 输入输出详解
后端·面试·go
程序员爱钓鱼2 小时前
Rust 生命周期详解:生命周期标注、约束与真实项目应用
后端·面试·rust