「milvus-course-ai.zip」
链接:https://pan.quark.cn/s/00f3d411bb6d
github:https://github.com/yuanmomoya/milvus
学习目标
学完本章后,你应该能够:
- 理解 HNSW 的分层图结构和贪心搜索算法。
- 掌握 M、efConstruction、ef 三个参数的含义和调优。
- 分析 HNSW 的内存开销并做容量规划。
- 在 Milvus 中创建 HNSW 索引并进行性能调优。
- 判断 HNSW 适用和不适用的场景。
HNSW 核心思想
HNSW(Hierarchical Navigable Small World)是一种基于图的 ANN 索引。核心思想:构建多层图,高层稀疏用于快速跳跃,底层密集用于精细搜索。
类比:从北京找到上海某条街道------先坐飞机到上海(高层),再坐地铁到区(中层),再步行到街道(底层)。
图结构详解
分层设计
#mermaid-svg-PmXGvlgfPWbWCHjh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PmXGvlgfPWbWCHjh .error-icon{fill:#552222;}#mermaid-svg-PmXGvlgfPWbWCHjh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PmXGvlgfPWbWCHjh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PmXGvlgfPWbWCHjh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PmXGvlgfPWbWCHjh .marker.cross{stroke:#333333;}#mermaid-svg-PmXGvlgfPWbWCHjh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PmXGvlgfPWbWCHjh p{margin:0;}#mermaid-svg-PmXGvlgfPWbWCHjh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PmXGvlgfPWbWCHjh .cluster-label text{fill:#333;}#mermaid-svg-PmXGvlgfPWbWCHjh .cluster-label span{color:#333;}#mermaid-svg-PmXGvlgfPWbWCHjh .cluster-label span p{background-color:transparent;}#mermaid-svg-PmXGvlgfPWbWCHjh .label text,#mermaid-svg-PmXGvlgfPWbWCHjh span{fill:#333;color:#333;}#mermaid-svg-PmXGvlgfPWbWCHjh .node rect,#mermaid-svg-PmXGvlgfPWbWCHjh .node circle,#mermaid-svg-PmXGvlgfPWbWCHjh .node ellipse,#mermaid-svg-PmXGvlgfPWbWCHjh .node polygon,#mermaid-svg-PmXGvlgfPWbWCHjh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PmXGvlgfPWbWCHjh .rough-node .label text,#mermaid-svg-PmXGvlgfPWbWCHjh .node .label text,#mermaid-svg-PmXGvlgfPWbWCHjh .image-shape .label,#mermaid-svg-PmXGvlgfPWbWCHjh .icon-shape .label{text-anchor:middle;}#mermaid-svg-PmXGvlgfPWbWCHjh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PmXGvlgfPWbWCHjh .rough-node .label,#mermaid-svg-PmXGvlgfPWbWCHjh .node .label,#mermaid-svg-PmXGvlgfPWbWCHjh .image-shape .label,#mermaid-svg-PmXGvlgfPWbWCHjh .icon-shape .label{text-align:center;}#mermaid-svg-PmXGvlgfPWbWCHjh .node.clickable{cursor:pointer;}#mermaid-svg-PmXGvlgfPWbWCHjh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PmXGvlgfPWbWCHjh .arrowheadPath{fill:#333333;}#mermaid-svg-PmXGvlgfPWbWCHjh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PmXGvlgfPWbWCHjh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PmXGvlgfPWbWCHjh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PmXGvlgfPWbWCHjh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PmXGvlgfPWbWCHjh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PmXGvlgfPWbWCHjh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PmXGvlgfPWbWCHjh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PmXGvlgfPWbWCHjh .cluster text{fill:#333;}#mermaid-svg-PmXGvlgfPWbWCHjh .cluster span{color:#333;}#mermaid-svg-PmXGvlgfPWbWCHjh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PmXGvlgfPWbWCHjh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PmXGvlgfPWbWCHjh rect.text{fill:none;stroke-width:0;}#mermaid-svg-PmXGvlgfPWbWCHjh .icon-shape,#mermaid-svg-PmXGvlgfPWbWCHjh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PmXGvlgfPWbWCHjh .icon-shape p,#mermaid-svg-PmXGvlgfPWbWCHjh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PmXGvlgfPWbWCHjh .icon-shape .label rect,#mermaid-svg-PmXGvlgfPWbWCHjh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PmXGvlgfPWbWCHjh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PmXGvlgfPWbWCHjh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PmXGvlgfPWbWCHjh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Layer 0 - 底层(所有节点)
1
2
3
4
5
6
7
8
9
10
Layer 1 - 中层
N1
N2
N3
N4
N5
N6
N7
N8
Layer 2 - 高层(少量节点)
N1
N3
N5
N8
Layer 3 - 最高层(极少节点)
N1
N5
层级分配规则:每个节点被分配到的最高层级由概率决定:
P(节点出现在第 l 层) = (1/mL)^l
其中 mL = 1/ln(M)
大多数节点只在 Layer 0,少数节点出现在高层,形成"跳表"式的层级结构。
每层的连接
- Layer 0 :每个节点最多
2×M个邻居(密集连接) - Layer > 0 :每个节点最多
M个邻居(稀疏连接)
构建算法
#mermaid-svg-Lp4CRLxNCudt60CA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Lp4CRLxNCudt60CA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Lp4CRLxNCudt60CA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Lp4CRLxNCudt60CA .error-icon{fill:#552222;}#mermaid-svg-Lp4CRLxNCudt60CA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Lp4CRLxNCudt60CA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Lp4CRLxNCudt60CA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Lp4CRLxNCudt60CA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Lp4CRLxNCudt60CA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Lp4CRLxNCudt60CA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Lp4CRLxNCudt60CA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Lp4CRLxNCudt60CA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Lp4CRLxNCudt60CA .marker.cross{stroke:#333333;}#mermaid-svg-Lp4CRLxNCudt60CA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Lp4CRLxNCudt60CA p{margin:0;}#mermaid-svg-Lp4CRLxNCudt60CA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Lp4CRLxNCudt60CA .cluster-label text{fill:#333;}#mermaid-svg-Lp4CRLxNCudt60CA .cluster-label span{color:#333;}#mermaid-svg-Lp4CRLxNCudt60CA .cluster-label span p{background-color:transparent;}#mermaid-svg-Lp4CRLxNCudt60CA .label text,#mermaid-svg-Lp4CRLxNCudt60CA span{fill:#333;color:#333;}#mermaid-svg-Lp4CRLxNCudt60CA .node rect,#mermaid-svg-Lp4CRLxNCudt60CA .node circle,#mermaid-svg-Lp4CRLxNCudt60CA .node ellipse,#mermaid-svg-Lp4CRLxNCudt60CA .node polygon,#mermaid-svg-Lp4CRLxNCudt60CA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Lp4CRLxNCudt60CA .rough-node .label text,#mermaid-svg-Lp4CRLxNCudt60CA .node .label text,#mermaid-svg-Lp4CRLxNCudt60CA .image-shape .label,#mermaid-svg-Lp4CRLxNCudt60CA .icon-shape .label{text-anchor:middle;}#mermaid-svg-Lp4CRLxNCudt60CA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Lp4CRLxNCudt60CA .rough-node .label,#mermaid-svg-Lp4CRLxNCudt60CA .node .label,#mermaid-svg-Lp4CRLxNCudt60CA .image-shape .label,#mermaid-svg-Lp4CRLxNCudt60CA .icon-shape .label{text-align:center;}#mermaid-svg-Lp4CRLxNCudt60CA .node.clickable{cursor:pointer;}#mermaid-svg-Lp4CRLxNCudt60CA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Lp4CRLxNCudt60CA .arrowheadPath{fill:#333333;}#mermaid-svg-Lp4CRLxNCudt60CA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Lp4CRLxNCudt60CA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Lp4CRLxNCudt60CA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Lp4CRLxNCudt60CA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Lp4CRLxNCudt60CA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Lp4CRLxNCudt60CA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Lp4CRLxNCudt60CA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Lp4CRLxNCudt60CA .cluster text{fill:#333;}#mermaid-svg-Lp4CRLxNCudt60CA .cluster span{color:#333;}#mermaid-svg-Lp4CRLxNCudt60CA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Lp4CRLxNCudt60CA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Lp4CRLxNCudt60CA rect.text{fill:none;stroke-width:0;}#mermaid-svg-Lp4CRLxNCudt60CA .icon-shape,#mermaid-svg-Lp4CRLxNCudt60CA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Lp4CRLxNCudt60CA .icon-shape p,#mermaid-svg-Lp4CRLxNCudt60CA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Lp4CRLxNCudt60CA .icon-shape .label rect,#mermaid-svg-Lp4CRLxNCudt60CA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Lp4CRLxNCudt60CA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Lp4CRLxNCudt60CA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Lp4CRLxNCudt60CA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
新节点 q
确定最高层级 l
从最高层入口开始
在每层贪心搜索最近节点
当前层 > l?
只导航不连接
下降一层
搜索 efConstruction 个候选
选择最近的 M 个作为邻居
双向建立连接
到达 Layer 0?
下降一层,重复
完成
efConstruction 的作用
efConstruction 控制构建时搜索候选集的大小:
- 越大 → 找到的邻居质量越好 → 图结构越优 → 搜索时召回越高
- 越大 → 构建越慢
搜索算法
#mermaid-svg-xEEAkdd9AG1OwPqN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xEEAkdd9AG1OwPqN .error-icon{fill:#552222;}#mermaid-svg-xEEAkdd9AG1OwPqN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xEEAkdd9AG1OwPqN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xEEAkdd9AG1OwPqN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xEEAkdd9AG1OwPqN .marker.cross{stroke:#333333;}#mermaid-svg-xEEAkdd9AG1OwPqN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xEEAkdd9AG1OwPqN p{margin:0;}#mermaid-svg-xEEAkdd9AG1OwPqN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xEEAkdd9AG1OwPqN .cluster-label text{fill:#333;}#mermaid-svg-xEEAkdd9AG1OwPqN .cluster-label span{color:#333;}#mermaid-svg-xEEAkdd9AG1OwPqN .cluster-label span p{background-color:transparent;}#mermaid-svg-xEEAkdd9AG1OwPqN .label text,#mermaid-svg-xEEAkdd9AG1OwPqN span{fill:#333;color:#333;}#mermaid-svg-xEEAkdd9AG1OwPqN .node rect,#mermaid-svg-xEEAkdd9AG1OwPqN .node circle,#mermaid-svg-xEEAkdd9AG1OwPqN .node ellipse,#mermaid-svg-xEEAkdd9AG1OwPqN .node polygon,#mermaid-svg-xEEAkdd9AG1OwPqN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xEEAkdd9AG1OwPqN .rough-node .label text,#mermaid-svg-xEEAkdd9AG1OwPqN .node .label text,#mermaid-svg-xEEAkdd9AG1OwPqN .image-shape .label,#mermaid-svg-xEEAkdd9AG1OwPqN .icon-shape .label{text-anchor:middle;}#mermaid-svg-xEEAkdd9AG1OwPqN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xEEAkdd9AG1OwPqN .rough-node .label,#mermaid-svg-xEEAkdd9AG1OwPqN .node .label,#mermaid-svg-xEEAkdd9AG1OwPqN .image-shape .label,#mermaid-svg-xEEAkdd9AG1OwPqN .icon-shape .label{text-align:center;}#mermaid-svg-xEEAkdd9AG1OwPqN .node.clickable{cursor:pointer;}#mermaid-svg-xEEAkdd9AG1OwPqN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xEEAkdd9AG1OwPqN .arrowheadPath{fill:#333333;}#mermaid-svg-xEEAkdd9AG1OwPqN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xEEAkdd9AG1OwPqN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xEEAkdd9AG1OwPqN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xEEAkdd9AG1OwPqN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xEEAkdd9AG1OwPqN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xEEAkdd9AG1OwPqN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xEEAkdd9AG1OwPqN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xEEAkdd9AG1OwPqN .cluster text{fill:#333;}#mermaid-svg-xEEAkdd9AG1OwPqN .cluster span{color:#333;}#mermaid-svg-xEEAkdd9AG1OwPqN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xEEAkdd9AG1OwPqN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xEEAkdd9AG1OwPqN rect.text{fill:none;stroke-width:0;}#mermaid-svg-xEEAkdd9AG1OwPqN .icon-shape,#mermaid-svg-xEEAkdd9AG1OwPqN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xEEAkdd9AG1OwPqN .icon-shape p,#mermaid-svg-xEEAkdd9AG1OwPqN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xEEAkdd9AG1OwPqN .icon-shape .label rect,#mermaid-svg-xEEAkdd9AG1OwPqN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xEEAkdd9AG1OwPqN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xEEAkdd9AG1OwPqN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xEEAkdd9AG1OwPqN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
查询向量 q
从最高层入口节点开始
贪心搜索:移动到更近的邻居
还能找到更近的?
到达 Layer 0?
下降一层
从当前最近节点继续
在 Layer 0 扩展搜索
维护 ef 大小的候选集
从候选集取 TopK
ef 参数的作用
ef 是搜索时在 Layer 0 维护的候选集大小:
ef >= limit(必须,否则结果不够)- ef 越大 → 探索范围越广 → 召回越高 → 延迟越大
参数详解与调优
三大参数
| 参数 | 阶段 | 含义 | 范围 | 影响 |
|---|---|---|---|---|
M |
构建 | 每个节点的邻居数上限 | 4-64 | 图密度、内存、召回 |
efConstruction |
构建 | 构建时候选集大小 | 64-512 | 图质量、构建速度 |
ef |
搜索 | 搜索时候选集大小 | limit ~ 512 | 召回率、搜索延迟 |
M 的影响
渲染错误: Mermaid 渲染失败: Lexical error on line 2. Unrecognized text. ...rt LR subgraph M=8 A1[内存低\n图 ----------------------^
| M | 内存开销(每节点) | 适用场景 |
|---|---|---|
| 8 | 128 字节 | 内存紧张,可接受较低召回 |
| 16 | 256 字节 | 默认推荐,平衡点 |
| 32 | 512 字节 | 高召回要求 |
| 48-64 | 768-1024 字节 | 极高召回,内存充足 |
efConstruction 的影响
| efConstruction | 构建速度 | 图质量 | 建议 |
|---|---|---|---|
| 64-100 | 快 | 一般 | 快速原型 |
| 128-200 | 中 | 好 | 生产默认 |
| 256-400 | 慢 | 很好 | 高精度要求 |
| 500+ | 很慢 | 边际收益递减 | 通常不必要 |
经验 :efConstruction >= 2 × M 通常足够。
ef 的影响
| ef | 召回率 | 延迟 | 说明 |
|---|---|---|---|
| = limit | 最低可接受 | 最快 | 候选集刚好够 |
| 2 × limit | 较好 | 快 | 常用起点 |
| 64-128 | 高 | 中 | 推荐范围 |
| 256+ | 很高 | 慢 | 高精度场景 |
约束 :ef >= limit,否则返回结果数量不足。
内存估算
HNSW 的内存由两部分组成:原始向量 + 图结构。
计算公式
总内存 = 原始向量内存 + 图结构内存
原始向量 = N × dim × 4 字节(float32)
图结构 ≈ N × M × 2 × 2 × 4 字节
= N × M × 16 字节
(Layer 0 有 2M 个邻居,高层有 M 个,平均约 2M;每个邻居存 ID + offset)
实际估算表
| 数据量 | 维度 | M | 向量内存 | 图内存 | 总计 |
|---|---|---|---|---|---|
| 100 万 | 512 | 16 | 1.91 GB | 0.24 GB | ~2.15 GB |
| 100 万 | 768 | 16 | 2.87 GB | 0.24 GB | ~3.11 GB |
| 500 万 | 768 | 16 | 14.3 GB | 1.19 GB | ~15.5 GB |
| 1000 万 | 768 | 16 | 28.6 GB | 2.38 GB | ~31.0 GB |
| 1000 万 | 768 | 32 | 28.6 GB | 4.77 GB | ~33.4 GB |
容量规划建议
python
def estimate_hnsw_memory_gb(n: int, dim: int, m: int) -> dict:
"""估算 HNSW 内存需求"""
vector_bytes = n * dim * 4
graph_bytes = n * m * 16
total_bytes = vector_bytes + graph_bytes
return {
"vectors_gb": vector_bytes / (1024**3),
"graph_gb": graph_bytes / (1024**3),
"total_gb": total_bytes / (1024**3),
"recommended_ram_gb": total_bytes / (1024**3) * 1.3, # 留 30% 余量
}
# 示例
print(estimate_hnsw_memory_gb(n=5_000_000, dim=768, m=16))
# {'vectors_gb': 14.3, 'graph_gb': 1.19, 'total_gb': 15.5, 'recommended_ram_gb': 20.1}
完整实战代码
python
from pymilvus import DataType, MilvusClient
import numpy as np
import time
client = MilvusClient(uri="http://localhost:19530")
COLLECTION = "hnsw_demo"
DIM = 768
N = 100_000
# 创建 Collection + HNSW 索引
if client.has_collection(COLLECTION):
client.drop_collection(COLLECTION)
schema = MilvusClient.create_schema(auto_id=False)
schema.add_field(field_name="id", datatype=DataType.VARCHAR, is_primary=True, max_length=16)
schema.add_field(field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=DIM)
index_params = MilvusClient.prepare_index_params()
index_params.add_index(
field_name="embedding",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 200},
)
client.create_collection(collection_name=COLLECTION, schema=schema, index_params=index_params)
# 写入数据
batch_size = 5000
for i in range(0, N, batch_size):
vectors = np.random.randn(batch_size, DIM).astype("float32")
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
vectors = (vectors / norms).tolist()
data = [{"id": str(i + j), "embedding": vectors[j]} for j in range(batch_size)]
client.upsert(collection_name=COLLECTION, data=data)
client.load_collection(COLLECTION)
print(f"写入 {N} 条,索引构建完成")
# ef 参数对比
query = np.random.randn(DIM).astype("float32")
query = (query / np.linalg.norm(query)).tolist()
print("\n--- ef 参数对比 ---")
for ef in [16, 32, 64, 128, 256]:
latencies = []
for _ in range(50):
start = time.perf_counter()
client.search(
collection_name=COLLECTION,
data=[query],
anns_field="embedding",
search_params={"metric_type": "COSINE", "params": {"ef": ef}},
limit=10,
)
latencies.append((time.perf_counter() - start) * 1000)
p50 = np.percentile(latencies, 50)
p95 = np.percentile(latencies, 95)
print(f"ef={ef:3d} P50={p50:.2f}ms P95={p95:.2f}ms")
HNSW 的优势与局限
优势
- 搜索速度快:图导航复杂度 O(log N),比 IVF 的线性扫描快
- 召回率高:ef 足够大时接近 100%
- 天然支持增量:新节点直接插入图中,无需重建
- 无需训练:不像 IVF 需要 KMeans 训练
局限
- 内存高:图结构额外占用 ~15-25% 内存
- 构建慢:逐条插入建图,比 IVF 的 KMeans 慢
- 不支持 GPU 加速:图遍历难以并行化
- 删除效率低:标记删除后图结构不会自动修复
何时不用 HNSW
- 数据量 > 5000 万且内存不足 → 用 IVF_PQ 或 DISKANN
- 需要极致压缩 → 用 PQ 系列
- 数据频繁大批量删除 → 定期重建索引
HNSW vs IVF 性能对比
以 100 万条 768 维向量为例(同一硬件):
| 指标 | HNSW (M=16, ef=128) | IVF_FLAT (nlist=1024, nprobe=64) |
|---|---|---|
| 内存 | ~3.1 GB | ~2.9 GB |
| 构建时间 | ~45s | ~15s |
| P50 延迟 | 2.5ms | 9.8ms |
| P95 延迟 | 4.2ms | 14.3ms |
| Recall@10 | 97% | 95% |
HNSW 在延迟上有明显优势,代价是更高的内存和构建时间。
生产调优建议
场景一:低延迟优先(在线搜索)
python
# 构建参数
params={"M": 16, "efConstruction": 200}
# 搜索参数
search_params={"params": {"ef": 64}} # ef 不要太大
场景二:高召回优先(离线评测)
python
# 构建参数
params={"M": 32, "efConstruction": 400}
# 搜索参数
search_params={"params": {"ef": 256}}
场景三:内存受限
python
# 降低 M 减少图结构内存
params={"M": 8, "efConstruction": 128}
# 或者考虑开启 mmap
# milvus.yaml: queryNode.mmap.enabled: true
常见错误
| 现象 | 原因 | 修复 |
|---|---|---|
| 搜索结果数量 < limit | ef < limit | 设置 ef >= limit |
| 召回率不够高 | ef 或 M 太小 | 增大 ef(优先)或重建索引增大 M |
| 构建索引超时 | efConstruction 太大 + 数据量大 | 降低 efConstruction 或增加资源 |
| 内存超出预期 | M 太大 | 降低 M 或用 mmap |
| 增量写入后召回下降 | 新数据的邻居连接质量不如批量构建 | 定期重建索引 |
面试题
-
HNSW 为什么要分层?只用一层图行不行?
单层图搜索需要从随机入口开始,路径长。分层后高层稀疏图提供"快速跳跃"能力,大幅减少搜索步数。类似跳表对链表的加速。
-
M 增大为什么能提高召回但增加内存?
M 越大,每个节点连接的邻居越多,图越密集,搜索时能探索到更多候选。但每个邻居指针都需要存储,内存线性增长。
-
ef 和 efConstruction 的关系是什么?
efConstruction 决定图的质量上限(离线),ef 决定搜索的精度(在线)。ef 再大也无法弥补 efConstruction 太小导致的图质量差。通常 ef <= efConstruction。
-
HNSW 支持删除吗?删除后会怎样?
Milvus 中支持标记删除。但删除后图中的边不会自动移除,可能导致搜索路径经过已删除节点。大量删除后建议重建索引。
-
为什么 HNSW 不适合 GPU 加速?
HNSW 搜索是图遍历,每一步依赖上一步的结果(贪心导航),难以并行化。IVF 的列表扫描是独立的,更适合 GPU 的 SIMD 并行。
练习题
-
M 参数实验:固定 50 万条数据和 efConstruction=200,分别用 M=8、16、32、48 建索引。记录构建时间、内存占用。搜索时固定 ef=128,对比召回率。
-
ef 调优曲线:固定 M=16 的索引,ef 从 16 到 512 逐步增大,记录 P50/P95 延迟和 Recall@10。画出 ef-recall 和 ef-latency 双轴图。
-
efConstruction 影响:分别用 efConstruction=64、128、256、400 建索引(M=16),搜索时统一 ef=128。对比召回率差异,验证"efConstruction 决定图质量上限"。
-
增量写入影响:先用 50 万条数据建索引,再增量写入 50 万条。对比增量前后的搜索召回率,验证是否有退化。
小结
HNSW 是当前最流行的向量索引,适合大多数中等规模(< 2000 万条)且内存充足的场景。三个参数的调优优先级:先调 ef(在线,立即生效),再调 M 和 efConstruction(需要重建索引)。记住内存公式,做好容量规划,HNSW 就是最省心的选择。