向量数据库从ANN索引到分布式检索完整原理
向量数据库负责保存向量、Metadata 和业务身份,并在给定查询向量后返回相似的 TopK 对象。真正的难点不是“能存一个浮点数组”,而是在百万、千万甚至更大规模下,同时满足召回质量、低延迟、并发、权限过滤、更新删除、扩容和恢复。
本章从精确 KNN 开始,逐步解释:
- 为什么全量比较准确但规模大时昂贵?
- ANN 为什么快,却可能漏掉数学上的真实最近邻?
- HNSW 的多层图怎样构建、查询和调参?
- IVF 怎样训练中心、分桶和用
nprobe控制候选范围? - PQ 怎样压缩向量,为什么会损失精度?
- Pre-filter、Post-filter 与 Filter-aware ANN 有什么区别?
- 分片节点怎样计算 Local TopK,Coordinator 怎样合并 Global TopK?
- 新写入何时可见,删除为什么可能先变成墓碑?
- 怎样设计真实生产压测,而不是只看单次 Demo?
Embedding 模型、Pooling 和相似度契约见 Embedding完整原理;本页集中讲向量索引和数据库运行过程。
一、学习目标
学完后,你应该能够:
- 区分精确 KNN 和 ANN,并解释速度、Recall 和资源取舍。
- 讲清 HNSW 的分层、入口点、邻接边、候选队列和搜索过程。
- 解释
M、efConstruction、efSearch对内存、构建和查询的影响方向。 - 讲清 IVF 的训练中心、倒排桶、
nlist和nprobe。 - 讲清 PQ 的子空间、码本、编码和近似距离。
- 判断 HNSW、IVF、IVF-PQ、精确扫描分别适合什么规模和约束。
- 区分 Pre-filter、Post-filter 和索引感知过滤的正确性与性能边界。
- 解释分片、本地候选、全局归并、过采样和路由。
- 解释写入、刷新、更新、墓碑、压缩和重建。
- 设计容量、备份、恢复、扩容和迁移方案。
- 使用 Recall@K、P95/P99、QPS、内存和构建时间做选型。
- 运行本页标准库 Demo,观察 IVF 漏召回和 Post-filter 不足 K 条。
二、向量数据库保存哪些对象
一条生产向量记录不只是 vector:
{
"chunkId": "lis-field-patient-id-v3",
"vector": [0.012, -0.084, 0.231],
"tenantId": "hospital-a",
"roles": ["data_admin"],
"knowledgeBaseId": "medical-assets",
"docId": "lis-fields-v3",
"contentHash": "sha256:...",
"embeddingRevision": "embed-v3|mean|l2|dim-1024",
"docStatus": "active",
"validFrom": "2026-07-01T00:00:00+08:00",
"sourceUri": "/standards/lis/fields#patient_id"
}| 部分 | 作用 |
|---|---|
| 主键 | 幂等写入、更新、删除、对账 |
| 向量 | 计算相似度 |
| Metadata | ACL、版本、状态、路由和业务过滤 |
| 原文或引用 | 展示证据;有些系统只存外部地址 |
| Revision | 防止跨模型、跨维度、跨Pooling混用 |
向量数据库不是订单等权威业务事实的替代品。订单状态、金额和库存仍应由事务数据库维护;向量库保存的是语义检索副本。
三、一次查询完整经过什么
flowchart TD
A["用户问题"] --> B["认证上下文生成租户与ACL"]
B --> C["兼容Revision生成Query向量"]
C --> D["选择集合、分区或分片"]
D --> E["执行Metadata Filter"]
E --> F["ANN检索候选"]
F --> G["各分片返回Local候选"]
G --> H["Coordinator合并Global TopK"]
H --> I["关键词融合、去重或Rerank"]
I --> J["证据阈值与最终预算"]
J --> K["返回Chunk ID、分数和Metadata"]执行顺序依产品和索引不同。尤其是 Filter 可能在 ANN 前、ANN 遍历中或 ANN 后生效,三种方式的性能、安全和结果完整性不同,必须通过产品文档和压测确认。
四、精确KNN为什么准确但可能慢
KNN 这里指查询向量与全部候选向量计算距离,再选真实 TopK。
若候选数量 N、维度 D,单查询距离计算方向约为:
O(N × D)一千万条、1024 维意味着一次查询要处理约百亿维度元素方向的运算,还要读取大量内存。硬件向量化、批处理和 GPU 可以加速,但数据规模和并发继续增长时,全量扫描成本仍显著。
4.1 精确扫描什么时候仍然合适
- 数据量小。
- Filter 后候选很少。
- 离线评估需要真实最近邻作为 Ground Truth。
- 批量查询可充分利用矩阵计算。
- 业务要求极高 Recall,延迟要求较宽松。
不要把 ANN 当成任何规模下的唯一正确答案。小集合使用复杂图索引可能增加写入、内存和运维成本,却没有明显收益。
五、ANN为什么叫近似最近邻
ANN 不遍历全部向量,而是利用图、聚类、量化或树结构优先访问可能相近的区域。
flowchart TD
A["全部N条向量"] --> B["离线或增量构建索引"]
B --> C["查询向量进入索引"]
C --> D["访问少量高概率区域"]
D --> E["形成候选集合"]
E --> F["计算候选距离并排序"]
F --> G["返回近似TopK"]因为没有检查全部向量,真实最近邻可能位于未访问区域,所以 Recall 可能低于精确 KNN。ANN 参数通常形成三角取舍:
更高Recall
↔ 更多候选与计算
↔ 更高延迟、内存或构建成本六、HNSW是什么结构
HNSW 是 Hierarchical Navigable Small World,分层可导航小世界图。每个向量是图节点,节点连接若干近邻;少量节点随机进入更高层。
Layer 2:A -------- F
\ /
Layer 1:A --- C --- F --- H
\ | / /
Layer 0:A-B-C-D-E-F-G-H-I高层节点少、边跨度大,用来快速接近目标区域;底层包含全部节点,用来精细搜索。
6.1 为什么“小世界图”能导航
如果图只有局部近邻,从很远入口逐步移动可能需要很多跳;若只有随机长边,到达局部后又不够精细。HNSW 通过分层和近邻连接结合长距离跳转与局部搜索。
6.2 HNSW不是保证最短路径的精确图算法
它使用启发式候选搜索,受入口、图质量、参数和数据分布影响,可能停在局部区域并漏掉真实近邻,因此仍是 ANN。
七、HNSW怎样构建
简化过程:
flowchart TD
A["新向量到达"] --> B["随机决定最高层级"]
B --> C["从当前全局入口的最高层开始"]
C --> D["高层贪心移动到更近节点"]
D --> E["逐层下降"]
E --> F["在目标层用efConstruction扩展候选"]
F --> G["选择有限近邻并建立边"]
G --> H["必要时裁剪邻居列表"]
H --> I["更新入口点或索引状态"]7.1 随机层级
大多数节点只在 Layer 0,少数进入高层,更高层节点更少。随机分布形成类似跳表的快速路径。具体层级概率由实现决定。
7.2 从入口点搜索插入位置
从最高层入口开始,比较当前节点邻居与新向量距离,只要找到更近邻居就移动;到某层局部较近区域后下降一层。
7.3 efConstruction
底层构建不是只保留第一个近邻,而是维护一定规模候选集合进行探索。efConstruction 越大,通常能探索更多节点并建立质量更好的图,但构建更慢、临时资源更高。
7.4 M
M 控制每个节点邻接边数量方向。更大 M 通常:
- 图连接更丰富,Recall 可能提高。
- 索引内存增加。
- 构建时距离计算和维护成本增加。
- 查询每次扩展可能检查更多邻居。
实际实现可能对第 0 层和高层使用不同最大邻居数,参数名称和约束要看产品。
7.5 邻居选择不一定只取距离最近M个
若所有边都集中在同一方向,图导航性可能变差。HNSW 实现常使用启发式多样化选择和双向边维护,具体算法不能仅用“每点连 M 个最近向量”概括。
八、HNSW怎样查询
flowchart TD
A["从最高层Entry Point开始"] --> B["比较当前节点与邻居"]
B --> C{"是否有更接近Query的邻居"}
C -- "是" --> B
C -- "否" --> D["下降一层"]
D --> E{"是否到Layer 0"}
E -- "否" --> B
E -- "是" --> F["用候选队列扩展efSearch个方向的节点"]
F --> G["维护当前最佳候选"]
G --> H["停止条件满足"]
H --> I["返回TopK"]高层通常偏贪心快速定位,Layer 0 使用更宽候选集合搜索。
8.1 efSearch
efSearch 或同类查询参数控制搜索候选宽度方向。通常:
- 值增大:访问更多节点,Recall 可能提高,延迟和 CPU 增加。
- 值减小:查询更快,但更容易漏近邻。
- 一般不能小于请求 TopK,具体限制看产品。
它不是一个跨数据集通用值。高维、聚类密度、Filter 选择性和目标 Recall 都会影响最佳点。
8.2 HNSW内存为什么高
除了原始向量,还要保存:
- 每层邻接列表。
- 节点层级和图元数据。
- 主键与映射。
- 删除标记。
- 实现所需对齐和对象开销。
因此容量不能只按 N×D×4 估算。
九、IVF怎样工作
IVF 是 Inverted File Index,先用训练样本得到若干粗聚类中心,再把每条向量分配到最近中心对应的倒排桶。
flowchart TD
A["抽样训练向量"] --> B["聚类得到nlist个中心"]
B --> C["每条数据向量寻找最近中心"]
C --> D["写入对应倒排桶"]
E["查询向量"] --> F["计算与所有中心距离"]
F --> G["选择最近的nprobe个桶"]
G --> H["只扫描这些桶内候选"]
H --> I["返回TopK"]9.1 nlist
桶或粗中心数量。太少时每桶过大,查询扫描候选多;太多时中心训练、管理成本增加,数据可能不均,nprobe 太小时更容易漏邻近桶。
9.2 nprobe
查询时搜索多少个桶:
- 增大:扫描候选更多,Recall 通常提高,延迟增加。
- 减小:更快,但边界附近的真实近邻可能在未搜索桶中。
9.3 为什么IVF要训练
粗中心要反映当前向量分布。训练样本太少、偏向某租户或与线上分布不同,会造成桶严重不均和召回退化。Embedding 模型更换后,空间变化通常需要重新训练中心和重建索引。
9.4 数据倾斜
某些桶过大,会导致查询落到这些桶时尾延迟高;某些桶过小则资源不均。应监控桶大小分布、访问热点和分片分布,而不是只看平均桶大小。
十、PQ怎样压缩向量
PQ 是 Product Quantization,通常把 D 维向量拆成 m 个子向量,每个子空间训练一个有限码本。
例如 8 维拆成 2 段:
[x1,x2,x3,x4] [x5,x6,x7,x8]每段不再保存完整 Float,而是保存最接近码字的编号:
[code_17, code_203]10.1 构建过程
- 选择训练样本。
- 将向量拆成 m 个子空间。
- 每个子空间独立聚类形成码本。
- 每条向量的每段映射为最近码字 ID。
- 保存短编码,可选保留原向量用于精排。
10.2 查询过程
常见 Asymmetric Distance Computation 方向:查询保留原始浮点向量,预先计算查询各子向量到每个码字的距离表;候选数据库向量只保存 code,通过查表求近似总距离。
10.3 为什么会损失精度
真实子向量被最近码字近似替代,量化误差会改变距离和排序。码字更多、子空间设计更细通常精度更好,但码本、编码和计算成本也变化。
10.4 IVF-PQ
先用 IVF 缩小桶范围,再用 PQ 压缩桶内向量并近似计算距离,适合大规模和内存受限场景。常配合候选过采样和原向量重排改善最终精度。
十一、HNSW、IVF、PQ怎样选
| 方案 | Recall与延迟方向 | 内存 | 写入/构建 | 适合方向 |
|---|---|---|---|---|
| 精确扫描 | Recall最高,规模大时慢 | 原向量为主 | 简单 | 小集合、Ground Truth、强Filter后少量候选 |
| HNSW | 高Recall、低查询延迟 | 图边使内存较高 | 增量构图成本明显 | 内存足、读多、追求低延迟 |
| IVF-Flat | 通过nprobe调节 | 保存原向量与桶 | 需训练中心和分桶 | 大规模、可批量构建 |
| IVF-PQ | 压缩明显、近似误差更大 | 较低 | 需训练中心与码本 | 超大规模、内存敏感 |
还要结合:
- 产品是否真正支持该索引与距离函数。
- Metadata Filter 的执行方式。
- 增量写入、更新和删除频率。
- 是否需要磁盘索引或 GPU。
- 团队备份、扩容和故障恢复能力。
十二、Filter为什么最容易产生生产问题
企业查询通常带:
tenantId = 当前租户
knowledgeBaseId = 当前知识库
docStatus = active
securityLevel <= 当前用户密级
roleTags 与当前角色匹配12.1 Pre-filter
先限定允许集合,再在集合内做向量检索。语义正确且安全边界清晰,但若索引不能高效利用 Filter,可能退化为大范围扫描或需要专门分区。
12.2 Post-filter
先全局 ANN TopN,再删除不满足 Filter 的结果。问题:
- 最终可能不足 K 条。
- 合法但全局分数稍低的候选没有进入 TopN。
- 越权候选可能已进入内存、Trace 或调试日志。
- 提高过采样只能降低不足概率,不能替代安全设计。
12.3 Filter-aware ANN
在图遍历、桶选择或候选扩展时结合 Filter,使搜索优先或只访问合法节点。不同产品支持的表达式、索引字段、选择性优化和正确性边界不同,必须压测。
12.4 高选择性和低选择性
如果 99% 数据被过滤,只剩 1%,全局图中的相邻节点多数不可用,搜索可能需要扩大候选;如果 Filter 几乎不过滤,额外判断成本较小。性能不能只用“带 Filter/不带 Filter”二分,必须按选择性曲线测试。
12.5 分区是否能替代Filter
租户或知识库可按分区路由减少搜索空间,但分区过多会增加元数据、文件、调度和小分片问题。大租户、小租户、跨租户管理查询的需求也不同,需避免每个小租户一个物理集群的过度设计。
十三、分片检索怎样得到Global TopK
假设数据分布在 3 个 Shard:
flowchart TD
A["Coordinator接收Query和Filter"] --> B["路由到Shard 1"]
A --> C["路由到Shard 2"]
A --> D["路由到Shard 3"]
B --> E["Local候选"]
C --> F["Local候选"]
D --> G["Local候选"]
E --> H["Coordinator归并相同量纲分数"]
F --> H
G --> H
H --> I["去重、可选Rerank与Global TopK"]13.1 为什么每个Shard常返回超过最终K的候选
后续可能有:
- 去重。
- Filter 或状态校验。
- 混合检索融合。
- Rerank 改变顺序。
- 同文档多 Chunk 限额。
因此常使用 shard-level overfetch,但过采样增加网络、Coordinator 内存和排序成本,需要压测。
13.2 分数必须可比较
同一 Embedding Revision、相同归一化和距离定义下,向量分数通常可归并。若不同 Shard 使用不同模型、量化或分数转换,直接比较可能错误。混合检索的 BM25 分数在不同 Shard 上也可能受局部统计影响,产品通常有自己的全局统计或归并机制。
13.3 路由
根据 tenantId、knowledgeBaseId 或业务分区路由可以减少 Fan-out。但路由键错误会漏数据;跨分区 Query 要明确是否允许和怎样鉴权。
13.4 热分片
大租户、热门知识库或不均匀哈希可能让少数 Shard CPU、内存和 QPS 过高,整体 P99 被最慢 Shard 拖住。监控必须分 Shard 看,而不是只看集群平均。
十四、写入何时对查询可见
向量库写入通常经历:
flowchart TD
A["客户端写入"] --> B["参数、维度与主键校验"]
B --> C["WAL或持久化日志"]
C --> D["内存段或写缓冲"]
D --> E["刷新为可查询Segment"]
E --> F["建立或合并ANN索引"]
F --> G["副本同步与路由更新"]
G --> H["查询可见"]具体产品可能在内存阶段就可搜索,也可能需要 refresh/load。客户端收到写成功不一定意味着所有副本和所有查询节点立即可见。需要明确:
- 写成功语义。
- Read-after-write 一致性选项。
- Refresh 周期。
- 副本延迟。
- 索引构建期间查询使用精确扫描还是暂不可见。
不要用关系数据库事务语义想当然地推断向量库。
十五、更新、删除和墓碑
15.1 更新通常是什么
向量内容改变时,很多索引难以原地修改图或量化编码,产品可能执行“新版本写入 + 旧版本删除标记”。应用应使用稳定业务主键和版本,避免随机 UUID 造成重复记录。
15.2 逻辑删除
先标记 tombstone,查询过滤掉旧记录;后台 Compaction 或索引重建再物理回收空间和图节点。
15.3 为什么删除后空间不立刻下降
- 墓碑只改变可见性。
- WAL 和快照仍保留历史。
- Segment 不可原地压缩。
- HNSW 图边和节点等待重建。
- 副本尚未完成回收。
15.4 为什么删除后仍可能搜到
- 删除只发生在原文库,向量副本未同步。
- Query 访问延迟副本。
- 缓存仍持有旧结果。
- Filter 没有强制
docStatus=active。 - 新旧索引 Alias 指向错误。
- 删除主键与 Chunk 实际主键不匹配。
删除传播要在源文档、Metadata、向量索引、关键词索引和缓存之间对账。
十六、Segment、Compaction和重建
很多向量库将数据组织为不可变或近似不可变 Segment:
- 新写进入活跃 Segment。
- Segment 达阈值后封存并构建索引。
- 删除形成墓碑。
- 小 Segment 和高墓碑 Segment 通过 Compaction 合并。
Compaction 会消耗 CPU、磁盘 IO、内存和网络,可能与查询争抢资源。需要限速、错峰和监控队列。HNSW 墓碑过多会降低有效图质量或增加遍历浪费,可能需要重建。
十七、高可用、副本和故障恢复
17.1 副本解决什么
- 节点故障时仍可读。
- 分担读流量。
- 发布和维护时减少中断。
副本不是备份:误删除、错误 Embedding Revision 或逻辑污染会同步到副本。
17.2 备份需要包含什么
- 原始或可重建的 Chunk 快照。
- Metadata 和主键。
- Embedding Revision 与索引配置。
- 向量数据或可重新生成向量的授权数据。
- Alias、集合、分区和版本清单。
- 评估集和发布 Manifest。
17.3 恢复演练
不能只验证“备份任务成功”。应在隔离环境恢复并检查:
- 向量计数和维度。
- active/deleted 数量。
- ACL 样本。
- 固定 Query Recall。
- Alias 和 Revision。
- 恢复耗时是否满足 RTO。
- 可接受数据丢失范围是否满足 RPO。
十八、容量估算
原始向量:
N × D × bytesPerElement总容量还要加:
ANN索引
+ Metadata索引
+ 主键映射
+ WAL
+ 活跃写缓冲
+ 墓碑与Compaction空间
+ 副本
+ 快照与备份
+ 文件系统和运行安全余量18.1 HNSW图边粗略方向
只做教学估算时,每个底层节点约 M 条或更多实现相关边,每条边有邻居 ID 和结构开销;高层还有额外边。不能只用 N×M×4 当真实总值,但可以认识到 M 增大将近似线性推高图内存方向。
18.2 不能按平均值规划
需要考虑:
- 最大租户向量数。
- 维度和数据类型。
- Metadata 变长。
- 增量写入峰值。
- Compaction 临时双份空间。
- 索引重建期间新旧版本并存。
- 副本和跨区域备份。
十九、可运行Demo:精确KNN与简化IVF
下面只使用 Python 标准库。它使用预先给定的两个粗中心,演示 Query 位于分桶边界时,nprobe=1 可能漏掉真实最近邻,增加到 2 后恢复精确结果。
import math
from dataclasses import dataclass
Vector = tuple[float, ...]
@dataclass(frozen=True)
class Item:
item_id: str
vector: Vector
def squared_l2(a: Vector, b: Vector) -> float:
if len(a) != len(b):
raise ValueError("向量维度不同")
return sum((x - y) ** 2 for x, y in zip(a, b))
def exact_knn(query: Vector, items: list[Item], k: int) -> list[tuple[str, float]]:
return sorted(
((item.item_id, squared_l2(query, item.vector)) for item in items),
key=lambda pair: pair[1],
)[:k]
def build_ivf(items: list[Item], centroids: list[Vector]) -> list[list[Item]]:
buckets = [[] for _ in centroids]
for item in items:
nearest = min(
range(len(centroids)),
key=lambda index: squared_l2(item.vector, centroids[index]),
)
buckets[nearest].append(item)
return buckets
def ivf_search(
query: Vector,
centroids: list[Vector],
buckets: list[list[Item]],
nprobe: int,
k: int,
) -> tuple[list[tuple[str, float]], int]:
if not 1 <= nprobe <= len(centroids):
raise ValueError("nprobe超出范围")
selected = sorted(
range(len(centroids)),
key=lambda index: squared_l2(query, centroids[index]),
)[:nprobe]
candidates = [item for index in selected for item in buckets[index]]
return exact_knn(query, candidates, k), len(candidates)
if __name__ == "__main__":
centroids = [(0.0, 0.0), (10.0, 0.0)]
items = [
Item("left-0", (0.0, 0.0)),
Item("left-4", (4.0, 0.0)),
# 5.1更接近右中心,所以进入右桶;但它离Query=4.9最近。
Item("right-5.1", (5.1, 0.0)),
Item("right-10", (10.0, 0.0)),
]
query = (4.9, 0.0)
buckets = build_ivf(items, centroids)
exact = exact_knn(query, items, k=1)
probe_1, visited_1 = ivf_search(query, centroids, buckets, nprobe=1, k=1)
probe_2, visited_2 = ivf_search(query, centroids, buckets, nprobe=2, k=1)
assert exact[0][0] == "right-5.1"
assert probe_1[0][0] == "left-4" # 漏掉真实最近邻
assert probe_2 == exact # 扫两个桶后恢复
assert visited_1 < visited_2
print("exact:", exact)
print("nprobe=1:", probe_1, "visited=", visited_1)
print("nprobe=2:", probe_2, "visited=", visited_2)真实 IVF 的中心通过训练获得,数据量和维度也远大于此。这个 Demo 只证明 nprobe 控制访问范围,访问更少候选可能漏召回。
二十、可运行Demo:Post-filter为什么不足K条
from dataclasses import dataclass
@dataclass(frozen=True)
class Candidate:
item_id: str
tenant_id: str
score: float
def post_filter(
candidates: list[Candidate], tenant_id: str, global_top_n: int, k: int
) -> list[Candidate]:
global_candidates = sorted(candidates, key=lambda item: item.score, reverse=True)[:global_top_n]
return [item for item in global_candidates if item.tenant_id == tenant_id][:k]
def pre_filter(candidates: list[Candidate], tenant_id: str, k: int) -> list[Candidate]:
allowed = [item for item in candidates if item.tenant_id == tenant_id]
return sorted(allowed, key=lambda item: item.score, reverse=True)[:k]
if __name__ == "__main__":
data = [
Candidate("b-1", "hospital-b", 0.99),
Candidate("b-2", "hospital-b", 0.98),
Candidate("a-1", "hospital-a", 0.97),
Candidate("a-2", "hospital-a", 0.96),
Candidate("a-3", "hospital-a", 0.95),
]
post = post_filter(data, "hospital-a", global_top_n=2, k=2)
pre = pre_filter(data, "hospital-a", k=2)
assert post == []
assert [item.item_id for item in pre] == ["a-1", "a-2"]
print("post-filter results:", post)
print("pre-filter results:", [item.item_id for item in pre])该 Demo 还没有模拟越权内容进入日志的安全风险,只演示结果不足。真实系统必须让 tenantId 和 ACL 来自服务端认证上下文,并确认向量库 Filter 的实际执行方式。
二十一、混合检索和分数融合
向量召回擅长同义语义,关键词/BM25 擅长错误码、字段名、类名和精确短语。
flowchart TD
A["Query"] --> B["向量ANN候选"]
A --> C["关键词候选"]
B --> D["按稳定ID去重"]
C --> D
D --> E["RRF或经过验证的分数融合"]
E --> F["Rerank"]
F --> G["最终证据TopK"]向量相似度和 BM25 分数通常不在同一量纲,不能直接相加。RRF 按排名融合:
RRFScore(doc) = Σ 1 / (constant + rank)Rerank 对较少候选做更精细的 Query-Document 联合判断。候选太少时 Rerank 无法找回未召回文档,候选太多会增加延迟和成本。
二十二、生产压测怎么设计
22.1 Ground Truth
从完整数据或可接受的精确扫描得到真实近邻,或者使用人工标注的相关 Chunk。没有 Ground Truth,只测延迟无法知道 ANN 是否把结果做错。
22.2 核心变量
| 变量 | 需要覆盖 |
|---|---|
| 数据量 | 当前、半年后、容量上限 |
| 维度 | 实际Embedding维度 |
| Filter | 无过滤、1%、10%、50%选择性等 |
| TopK | 业务实际K和过采样K |
| 并发 | 稳态、峰值和突发 |
| 写入 | 无写、增量写、批量导入、Compaction |
| 索引参数 | M、efSearch、nprobe、量化配置 |
| 数据分布 | 不同租户、语言和热点 |
22.3 指标
- Recall@K、MRR、nDCG。
- 平均、P50、P95、P99 延迟。
- QPS 和错误率。
- CPU、内存、磁盘 IO、网络。
- 每查询访问候选数。
- 索引构建时间和峰值资源。
- 写入到可见延迟。
- 删除传播延迟。
- Compaction 队列和墓碑比例。
- 故障恢复和扩容期间性能。
22.4 参数曲线而不是单点
例如测试 efSearch 多个值,画出:
efSearch → Recall@10
efSearch → P95/P99
efSearch → CPU/QPS选满足质量门禁后的最低资源点,而不是盲目把参数调到最大。
二十三、选型维度
| 维度 | 要问的问题 |
|---|---|
| 索引 | 支持哪些距离、HNSW、IVF、PQ或磁盘索引 |
| Filter | 表达式、字段索引、选择性退化和安全边界 |
| 混合检索 | 关键词与向量是否统一查询和融合 |
| 一致性 | 写成功、可见性、副本读取语义 |
| 生命周期 | 更新、删除、墓碑、Compaction、重建 |
| 扩展 | 分片、路由、再平衡、热点治理 |
| 可靠性 | WAL、副本、快照、恢复、RPO/RTO |
| 运维 | 指标、Trace、慢查询、容量和升级 |
| 成本 | 内存、磁盘、节点、网络、托管费用 |
| 生态 | Java/Python SDK、框架集成和版本兼容 |
产品方向示例:
- pgvector:团队已有 PostgreSQL、中小规模、希望统一事务与运维时可评估;仍需测试索引和 Filter。
- Elasticsearch/OpenSearch 向量能力:已有全文检索和混合搜索体系时可评估。
- Milvus 等专业向量数据库:大规模和独立向量平台需求时评估分布式能力与运维。
- FAISS:高性能算法库,不等于自带权限、WAL、分布式高可用的数据库。
- 轻量本地方案:适合学习和原型,不能凭 Demo 推断生产可靠性。
不做脱离版本的绝对产品排名。最终选择必须由真实数据、团队能力、SLO 和恢复要求证明。
二十四、商业场景:多租户医疗知识库
24.1 需求
- 数千万 Chunk 方向增长。
patient_id等精确字段和自然语言同义问法。- 医院、部门、角色和密级隔离。
- 文档每日增量,删除和权限变化需及时生效。
- P95 检索预算 150ms,仅为示例,真实值按系统 SLO。
24.2 设计方向
- 按稳定 Chunk ID 和 embeddingRevision 建候选索引。
- tenantId、knowledgeBaseId 和 active 状态为可索引 Metadata。
- 大租户可独立分区或路由,小租户共享分区并保留逻辑 ACL。
- 字段名走关键词候选,语义问法走向量候选。
- Filter 在候选搜索前或索引遍历中生效。
- 各 Shard 过采样候选,Coordinator 合并后 RRF/Rerank。
- 新索引通过固定权限和召回集后切 Alias。
- 缓存 Key 包含租户、ACL、索引和 Revision。
- 定期对账源文档、Metadata、向量、关键词索引和缓存。
24.3 失败边界
- 单 Shard 超时:根据产品和业务决定部分结果是否可接受;知识问答通常要标记结果不完整,不能静默当完整 TopK。
- Filter 服务异常:Fail Closed,不退化为全库搜索。
- Alias/Revision 不匹配:阻断查询并告警,不能跨空间“勉强返回”。
- 删除积压:active Filter 先阻断可见性,同时修复删除传播和 Compaction。
二十五、生产故障排查Runbook
25.1 查询延迟突然升高
- 拆 Coordinator、网络、各 Shard、Filter、ANN、取原文和 Rerank 耗时。
- 看 P99 是否由单个热 Shard 拖慢。
- 检查数据量、efSearch/nprobe、TopK、过采样是否改变。
- 按 Filter 选择性分组,判断是否只在高过滤场景退化。
- 检查 Compaction、批量导入和索引构建是否争抢资源。
- 查看内存是否不足导致磁盘访问、GC 或缓存抖动。
25.2 Recall下降但延迟更快
高度怀疑搜索范围缩小:
- efSearch 或 nprobe 下调。
- 分片过采样减少。
- PQ 压缩配置改变。
- Filter 提前排除了正确文档。
- Query 路由漏 Shard。
- ANN 索引未完成或图质量下降。
用固定 Query 同时跑精确扫描与 ANN,比较差集并记录访问候选数。
25.3 只有带Filter查询很慢
- Metadata 字段是否建立适合的索引。
- Filter 是 Pre、Post 还是遍历中生效。
- 选择性是否极高导致图邻居大量不可用。
- 是否能按租户/知识库路由缩小集合。
- 表达式是否包含高成本 OR、数组交集或未索引字段。
- 不能为了提速移除 ACL;应调整数据布局和索引方案。
25.4 删除后仍能搜到旧文档
按以下副本逐一查:
源文档状态
→ Metadata active状态
→ 向量主键与墓碑
→ 关键词索引
→ Alias实际版本
→ 副本延迟
→ 应用与结果缓存记录删除事件 ID 和各系统处理时间,建立删除传播 SLO 与对账任务。
25.5 集群扩容后P99反而上升
- 数据再平衡和索引复制正在占用 IO/网络。
- 新节点缓存冷。
- 分片数增加导致 Coordinator Fan-out 增大。
- 小分片过多。
- 路由或副本选择不均。
- 新旧节点硬件或版本不一致。
扩容要分阶段迁移和预热,观察 Rebalance 完成后的稳态,不应只看节点数量。
25.6 写入成功但查询不到
- 写入是否到了正确集合、分区、Revision。
- 写成功是 WAL 接收还是查询可见。
- Segment 是否 refresh/load。
- ANN 索引是否完成。
- Query Filter 是否排除新记录。
- 副本和路由表是否更新。
- 查询是否访问旧 Alias 或缓存。
二十六、常见误区与后果
| 误区 | 正确理解 |
|---|---|
| ANN一定返回数学真实TopK | ANN用更少访问换速度,必须测Recall |
| HNSW就是每点连M个最近邻 | 还有分层、入口、候选搜索和启发式邻居选择 |
| efSearch越大越好 | Recall可能提高,但延迟、CPU和QPS受影响 |
| IVF不需要训练 | 粗中心质量和数据分布决定桶与Recall |
| PQ是无损压缩 | 码字近似会产生量化误差 |
| Post-filter等于权限安全 | 结果可能不足,越权候选已被访问 |
| 副本就是备份 | 逻辑删除和污染会同步到副本 |
| 删除成功空间立刻下降 | 墓碑等待Compaction或重建 |
| 加节点一定更快 | Rebalance、Fan-out、小分片和冷缓存可能恶化P99 |
| 官方百万QPS可直接套用 | 数据、维度、Filter、Recall和硬件条件不同 |
二十七、面试标准回答
27.1 向量数据库为什么查询快
它通常使用 ANN 索引避免对全部 N 条向量做精确比较。HNSW 通过分层近邻图导航到相似区域;IVF 先定位最近粗聚类桶,只扫描 nprobe 个桶;PQ 用码本压缩向量并近似计算距离。访问候选减少换来低延迟,但可能漏真实近邻,所以必须同时评估 Recall。
27.2 HNSW查询过程是什么
从最高层入口点开始,沿更接近 Query 的邻居贪心移动,局部收敛后逐层下降;到 Layer 0 后维护 efSearch 规模方向的候选和最佳集合,继续扩展邻居直到停止,再返回 TopK。M 影响图连接和内存,efConstruction 影响构图质量与成本,efSearch 影响查询 Recall 与延迟。
27.3 IVF的nlist和nprobe是什么
nlist 是粗聚类中心或倒排桶数量,建库时每条向量分配到最近桶;nprobe 是查询时搜索的最近桶数量。增大 nprobe 通常提高 Recall 但扫描更多候选、延迟更高。中心训练样本和桶数据倾斜也会影响效果。
27.4 PQ为什么省空间又损失精度
PQ 将向量拆成多个子空间,每段只保存最近码字编号,查询通过码字距离表估算总距离,避免保存和计算完整 Float 向量。真实子向量被码字近似,量化误差会改变距离和排序,因此常配合候选过采样和原向量精排。
27.5 为什么Post-filter可能不足K条
它先从全库 ANN 取 TopN,再删除不满足租户或权限的候选。若高分候选多数越权,过滤后可能不足 K,而合法但全局排名稍低的文档从未进入候选。权限应尽量作为 Pre-filter 或索引感知 Filter,并由服务端认证上下文生成。
27.6 分布式向量查询怎样得到全局TopK
Coordinator 将 Query 和 Filter 路由到相关 Shard,各 Shard 执行 Local ANN 并返回过采样候选;Coordinator 在分数可比较前提下归并、去重,按需做混合融合和 Rerank,再得到 Global TopK。P99 常被最慢 Shard、Fan-out 和热点拖累。
更多简洁答案见 AI应用工程化面试题,完整原理以本页为准。
二十八、关联知识点
- Embedding训练、Pooling与检索:理解向量空间、距离和Revision。
- RAG完整管道:学习候选索引、Alias、混合召回和对账。
- RAG数据治理:学习权限、版本、删除和审计。
- Elasticsearch:查看倒排索引、分片与向量能力的具体体系。
- Spring AI RAG:查看 VectorStore Filter 的 Java 工程实现。
二十九、学习验收清单
- [ ] 能解释精确KNN的复杂度与适用场景。
- [ ] 能画出HNSW分层图的构建和查询过程。
- [ ] 能说明M、efConstruction、efSearch的影响方向。
- [ ] 能画出IVF中心训练、分桶和nprobe查询。
- [ ] 能解释PQ子空间、码本、编码和量化误差。
- [ ] 能比较精确扫描、HNSW、IVF-Flat和IVF-PQ。
- [ ] 能区分Pre-filter、Post-filter与Filter-aware ANN。
- [ ] 能解释分片Local候选到Global TopK的归并。
- [ ] 能解释写成功、refresh、索引完成和查询可见的区别。
- [ ] 能解释墓碑、Compaction和删除后空间不下降。
- [ ] 能设计副本、备份、RPO/RTO和恢复演练。
- [ ] 能使用Ground Truth画Recall与P99参数曲线。
- [ ] 能运行IVF Demo并解释nprobe=1为什么漏召回。
- [ ] 能运行Filter Demo并解释为什么最终不足K条。
达到这些标准后,才算理解向量数据库,而不是只会背 HNSW、IVF 和 Milvus 等名词。
