Skip to content

向量数据库从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 的分层、入口点、邻接边、候选队列和搜索过程。
  • 解释 MefConstructionefSearch 对内存、构建和查询的影响方向。
  • 讲清 IVF 的训练中心、倒排桶、nlistnprobe
  • 讲清 PQ 的子空间、码本、编码和近似距离。
  • 判断 HNSW、IVF、IVF-PQ、精确扫描分别适合什么规模和约束。
  • 区分 Pre-filter、Post-filter 和索引感知过滤的正确性与性能边界。
  • 解释分片、本地候选、全局归并、过采样和路由。
  • 解释写入、刷新、更新、墓碑、压缩和重建。
  • 设计容量、备份、恢复、扩容和迁移方案。
  • 使用 Recall@K、P95/P99、QPS、内存和构建时间做选型。
  • 运行本页标准库 Demo,观察 IVF 漏召回和 Post-filter 不足 K 条。

二、向量数据库保存哪些对象

一条生产向量记录不只是 vector:

json
{
  "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"
}
部分作用
主键幂等写入、更新、删除、对账
向量计算相似度
MetadataACL、版本、状态、路由和业务过滤
原文或引用展示证据;有些系统只存外部地址
Revision防止跨模型、跨维度、跨Pooling混用

向量数据库不是订单等权威业务事实的替代品。订单状态、金额和库存仍应由事务数据库维护;向量库保存的是语义检索副本。

三、一次查询完整经过什么

mermaid
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,单查询距离计算方向约为:

text
O(N × D)

一千万条、1024 维意味着一次查询要处理约百亿维度元素方向的运算,还要读取大量内存。硬件向量化、批处理和 GPU 可以加速,但数据规模和并发继续增长时,全量扫描成本仍显著。

4.1 精确扫描什么时候仍然合适

  • 数据量小。
  • Filter 后候选很少。
  • 离线评估需要真实最近邻作为 Ground Truth。
  • 批量查询可充分利用矩阵计算。
  • 业务要求极高 Recall,延迟要求较宽松。

不要把 ANN 当成任何规模下的唯一正确答案。小集合使用复杂图索引可能增加写入、内存和运维成本,却没有明显收益。

五、ANN为什么叫近似最近邻

ANN 不遍历全部向量,而是利用图、聚类、量化或树结构优先访问可能相近的区域。

mermaid
flowchart TD
    A["全部N条向量"] --> B["离线或增量构建索引"]
    B --> C["查询向量进入索引"]
    C --> D["访问少量高概率区域"]
    D --> E["形成候选集合"]
    E --> F["计算候选距离并排序"]
    F --> G["返回近似TopK"]

因为没有检查全部向量,真实最近邻可能位于未访问区域,所以 Recall 可能低于精确 KNN。ANN 参数通常形成三角取舍:

text
更高Recall
↔ 更多候选与计算
↔ 更高延迟、内存或构建成本

六、HNSW是什么结构

HNSW 是 Hierarchical Navigable Small World,分层可导航小世界图。每个向量是图节点,节点连接若干近邻;少量节点随机进入更高层。

text
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怎样构建

简化过程:

mermaid
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怎样查询

mermaid
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,先用训练样本得到若干粗聚类中心,再把每条向量分配到最近中心对应的倒排桶。

mermaid
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 段:

text
[x1,x2,x3,x4] [x5,x6,x7,x8]

每段不再保存完整 Float,而是保存最接近码字的编号:

text
[code_17, code_203]

10.1 构建过程

  1. 选择训练样本。
  2. 将向量拆成 m 个子空间。
  3. 每个子空间独立聚类形成码本。
  4. 每条向量的每段映射为最近码字 ID。
  5. 保存短编码,可选保留原向量用于精排。

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为什么最容易产生生产问题

企业查询通常带:

text
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:

mermaid
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 看,而不是只看集群平均。

十四、写入何时对查询可见

向量库写入通常经历:

mermaid
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。

十八、容量估算

原始向量:

text
N × D × bytesPerElement

总容量还要加:

text
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 后恢复精确结果。

python
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条

python
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 擅长错误码、字段名、类名和精确短语。

mermaid
flowchart TD
    A["Query"] --> B["向量ANN候选"]
    A --> C["关键词候选"]
    B --> D["按稳定ID去重"]
    C --> D
    D --> E["RRF或经过验证的分数融合"]
    E --> F["Rerank"]
    F --> G["最终证据TopK"]

向量相似度和 BM25 分数通常不在同一量纲,不能直接相加。RRF 按排名融合:

text
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 多个值,画出:

text
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 设计方向

  1. 按稳定 Chunk ID 和 embeddingRevision 建候选索引。
  2. tenantId、knowledgeBaseId 和 active 状态为可索引 Metadata。
  3. 大租户可独立分区或路由,小租户共享分区并保留逻辑 ACL。
  4. 字段名走关键词候选,语义问法走向量候选。
  5. Filter 在候选搜索前或索引遍历中生效。
  6. 各 Shard 过采样候选,Coordinator 合并后 RRF/Rerank。
  7. 新索引通过固定权限和召回集后切 Alias。
  8. 缓存 Key 包含租户、ACL、索引和 Revision。
  9. 定期对账源文档、Metadata、向量、关键词索引和缓存。

24.3 失败边界

  • 单 Shard 超时:根据产品和业务决定部分结果是否可接受;知识问答通常要标记结果不完整,不能静默当完整 TopK。
  • Filter 服务异常:Fail Closed,不退化为全库搜索。
  • Alias/Revision 不匹配:阻断查询并告警,不能跨空间“勉强返回”。
  • 删除积压:active Filter 先阻断可见性,同时修复删除传播和 Compaction。

二十五、生产故障排查Runbook

25.1 查询延迟突然升高

  1. 拆 Coordinator、网络、各 Shard、Filter、ANN、取原文和 Rerank 耗时。
  2. 看 P99 是否由单个热 Shard 拖慢。
  3. 检查数据量、efSearch/nprobe、TopK、过采样是否改变。
  4. 按 Filter 选择性分组,判断是否只在高过滤场景退化。
  5. 检查 Compaction、批量导入和索引构建是否争抢资源。
  6. 查看内存是否不足导致磁盘访问、GC 或缓存抖动。

25.2 Recall下降但延迟更快

高度怀疑搜索范围缩小:

  • efSearch 或 nprobe 下调。
  • 分片过采样减少。
  • PQ 压缩配置改变。
  • Filter 提前排除了正确文档。
  • Query 路由漏 Shard。
  • ANN 索引未完成或图质量下降。

用固定 Query 同时跑精确扫描与 ANN,比较差集并记录访问候选数。

25.3 只有带Filter查询很慢

  • Metadata 字段是否建立适合的索引。
  • Filter 是 Pre、Post 还是遍历中生效。
  • 选择性是否极高导致图邻居大量不可用。
  • 是否能按租户/知识库路由缩小集合。
  • 表达式是否包含高成本 OR、数组交集或未索引字段。
  • 不能为了提速移除 ACL;应调整数据布局和索引方案。

25.4 删除后仍能搜到旧文档

按以下副本逐一查:

text
源文档状态
→ 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一定返回数学真实TopKANN用更少访问换速度,必须测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应用工程化面试题,完整原理以本页为准。

二十八、关联知识点

二十九、学习验收清单

  • [ ] 能解释精确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 等名词。