未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适合已有向量检索基础的读者理解 Qdrant 的 HNSW、SQ/PQ/BQ、重排、命名向量及优化器配置取舍,不是经验证的生产容量或性能配方。当前参数到召回率的对应关系、量化加速倍数及 oversampling 收益推荐仍缺少可追溯测试条件,不能直接用于选型承诺;7.5、9.4 和生产案例已明确为假设或目标,不应当作实测结果。图内存公式仅为不完整的示意估算,recommend_m 不能保证实际图内存满足预算,且其示例输出注释与当前代码计算结果不一致;部分编码容量将 GiB 数值标为 GB,使用时须区分单位。量化编码容量不等于实例总内存,Rescore 需要访问原始向量且只能重排已召回候选,保留 float32 或设置 on_disk 本身不保证精确搜索。代码片段为配置示意,省略号和 embedding 变量需替换为匹配维度的实际输入,默认布局、参数行为及 API 应结合所用服务端和客户端版本确认。本次仅作结构与正文一致性审阅,未进行外部事实核验或代码运行验证。 已取消无法确定目标的可点击引用「#二、hnsw_config 参数全解析」,保留原提及;Missing anchor: #二、hnsw_config 参数全解析 已取消无法确定目标的可点击引用「#四、on_disk 模式:低内存的生产之道」,保留原提及;Missing anchor: #四、on_disk 模式:低内存的生产之道

Qdrant 索引与量化配置深度指南

abstract 文档概述

本文深入解析 Qdrant 的索引机制与量化压缩体系。索引决定搜索的速度与精度,量化决定内存占用与计算效率。掌握这两个维度的配置,是将 Qdrant 从"能用"推向"好用"的关键跨越。

目录


一、HNSW 索引原理回顾

1.1 为什么 Qdrant 选择 HNSW

在向量数据库的索引赛道上,主流方案包括:

索引类型 代表实现 特点
精确搜索(暴力扫描) Flat Index 100% 召回,O(N) 复杂度
基于树结构 KD-Tree、Ball Tree 低维有效,高维退化
基于量化 IVF+PQ 内存高效,需训练阶段
基于图结构 HNSW 高召回+高速度,工程首选

HNSW(Hierarchical Navigable Small World)是工程实践中常用的近似最近邻(ANN)索引之一,其适用性取决于数据分布、召回目标、内存和负载。Qdrant 基于 HNSW 进行了深度定制,加入了 可过滤 HNSW(Filterable HNSW)能力,这是其核心差异化优势之一。

1.2 HNSW 分层图结构

HNSW 的本质是一个多层有向图:

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph TB
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    subgraph Layer2["第 2 层(导航层)- 稀疏长程连接"]
        A2["节点 A"] --- B2["节点 E"]
        B2 --- C2["节点 J"]
    end
    subgraph Layer1["第 1 层(中间层)- 中等密度"]
        A1["节点 A"] --- B1["节点 C"]
        B1 --- C1["节点 E"]
        C1 --- D1["节点 H"]
        D1 --- E1["节点 J"]
    end
    subgraph Layer0["第 0 层(基础层)- 密集精确连接"]
        A0["节点 A"] --- B0["节点 B"]
        B0 --- C0["节点 C"]
        C0 --- D0["节点 D"]
        D0 --- E0["节点 E"]
        E0 --- F0["节点 F"]
        F0 --- G0["节点 G"]
        G0 --- H0["节点 H"]
        H0 --- I0["节点 I"]
        I0 --- J0["节点 J"]
    end
    Layer2 --> Layer1
    Layer1 --> Layer0
    class Layer2,A2,B2,C2,Layer1,A1,B1,C1,D1,E1,Layer0,A0,B0,C0,D0,E0,F0,G0,H0,I0,J0 default

搜索过程:

  1. 从最高层的入口点开始贪心搜索
  2. 逐层下降,每层找局部最近邻
  3. 在第 0 层进行精细搜索,返回 top-K 结果

建图过程:

  • 每个新插入节点随机分配到某一层(层数越高概率越低)
  • 在每层中,通过贪心搜索找到 ef_construct 个候选邻居
  • 从候选中选择最优的 m 个节点建立双向连接

二、hnsw_config 参数全解析

2.1 参数清单

Qdrant 的 HNSW 配置通过 hnsw_config 对象传入,支持集合级别和向量级别两种粒度的配置。

from qdrant_client import QdrantClient
from qdrant_client.models import (
    VectorParams, Distance, HnswConfigDiff
)

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="my_collection",
    vectors_config=VectorParams(
        size=1536,
        distance=Distance.COSINE
    ),
    hnsw_config=HnswConfigDiff(
        m=16,                      # 每个节点最大连接数
        ef_construct=200,          # 建图时的候选邻居数
        full_scan_threshold=10000, # 全量扫描阈值,单位为向量数据大小的 KB,不是向量条数
        max_indexing_threads=4,    # 构建索引使用的线程数
        on_disk=False,             # 是否将索引存储在磁盘
        payload_m=16               # Payload 过滤索引的 m 值
    )
)

2.2 核心参数详解

m:图的连接密度

m 控制每个节点在 HNSW 图中的最大连接数(第 0 层为 2*m,其他层为 m)。

m 值  → 更多连接 → 更高召回 → 更多内存 → 更慢构建
m 值  → 更少连接 → 更低召回 → 更少内存 → 更快构建

图连接内存的简化示意估算(未完整计入第 0 层连接容量及其他结构开销,不可作为容量上限):

HNSW 图内存 ≈ num_vectors × m × (层数均值 ≈ 1.3) × sizeof(node_id)

以 node_id 为 4 字节举例:

  • 100 万向量,m=16:≈ 100万 × 16 × 1.3 × 4 字节 ≈ 83 MB
  • 100 万向量,m=32:≈ 100万 × 32 × 1.3 × 4 字节 ≈ 167 MB

推荐范围:

场景 推荐 m 值 说明
内存严苛 8 - 12 降低图密度,牺牲部分召回
通用场景 16 Qdrant 默认值,平衡选择
高召回要求 32 - 64 召回率 >99%,内存翻倍
极高精度 64 - 128 接近精确搜索,适合小数据集

ef_construct:建图质量

ef_construct 决定构建索引时每个节点考虑的候选邻居数量。值越大,建图质量越高,但建图速度越慢。

important 约束关系

ef_construct 必须大于 m,否则选出的邻居数量不足以填满节点连接槽。 推荐关系:ef_construct >= 2 × m,通常设为 m × 10 ~ m × 20。

配置组合参考:

# 快速构建(CI/CD 测试环境)
fast_config = HnswConfigDiff(m=8, ef_construct=50)

# 平衡配置(大多数生产场景)
balanced_config = HnswConfigDiff(m=16, ef_construct=200)

# 高质量配置(要求 99%+ 召回)
quality_config = HnswConfigDiff(m=32, ef_construct=400)

# 极致质量(离线索引构建)
premium_config = HnswConfigDiff(m=64, ef_construct=800)

full_scan_threshold:全量扫描阈值

该参数以向量数据大小的 KB 为单位,为查询规划选择扫描路径提供阈值;实际选择还与分段及过滤后估计候选规模有关,不能按全集合向量条数直接判断。

full_scan_threshold:向量数据大小阈值,单位 KB
扫描或 HNSW 路径:结合分段、索引状态及过滤后估计候选规模选择

tip 实践建议

对于精度敏感场景(如金融风控),可适当提高此阈值,让更多查询走精确搜索路径,在数据量不大时避免 ANN 误差。 默认值为 20000。

max_indexing_threads:构建并发度

控制 HNSW 索引构建的并行线程数。

# 专用索引服务器:充分利用 CPU
HnswConfigDiff(max_indexing_threads=0)  # 0 = 使用所有可用核心

# 混合负载服务器:预留计算资源给查询
HnswConfigDiff(max_indexing_threads=4)  # 限制 4 个线程

warning 注意

增加索引线程会消耗更多 CPU,在写入密集期间可能影响查询延迟。

payload_m:Payload 索引的图密度

这是 Qdrant 可过滤 HNSW 特有的参数。Qdrant 为 Payload 字段维护额外的图边,payload_m 控制这些附加边的密度。

HnswConfigDiff(
    m=16,
    payload_m=16  # 通常与 m 相同,或设为 m 的一半以节省内存
)

2.3 运行时搜索参数 ef

ef(有时写作 hnsw_ef)是查询时的参数,控制搜索过程中维护的候选集大小:

from qdrant_client.models import SearchRequest, SearchParams

results = client.search(
    collection_name="my_collection",
    query_vector=[0.1, 0.2, ...],
    limit=10,
    search_params=SearchParams(
        hnsw_ef=128,    # 搜索候选集大小,越大越准确但越慢
        exact=False     # True = 强制精确搜索(忽略 HNSW)
    )
)

ef 与召回率的关系:

ef 值 预期召回率 延迟倍数
32 ~95% 1x
64 ~98% 1.5x
128 ~99% 2.5x
256 ~99.5% 4x
exact=True 100% 10-50x

三、m 值选择的工程公式

3.1 基于向量维度的经验公式

推荐 m ≈ max(4, min(64, dim / 100))

计算示例:

  • 384 维(小模型):按此经验公式,m ≈ max(4, min(64, 3.84)) = 4;实际候选值仍需测试。
  • 768 维(BERT base):m ≈ max(4, min(64, 7.68)) ≈ 8~16
  • 1536 维(text-embedding-3-small):m ≈ max(4, min(64, 15.36)) ≈ 16
  • 3072 维(text-embedding-3-large):m ≈ max(4, min(64, 30.72)) ≈ 32

3.2 基于数据规模的调整

def recommend_m(num_vectors: int, vector_dim: int, memory_budget_gb: float) -> int:
    """
    根据数据规模和内存预算推荐 m 值

    Args:
        num_vectors: 向量总数
        vector_dim: 向量维度
        memory_budget_gb: HNSW 图结构可用内存(GB)

    Returns:
        推荐的 m 值
    """
    # 向量本身占用的内存(float32)
    vector_memory_gb = num_vectors * vector_dim * 4 / (1024**3)

    # 可分配给图结构的内存
    graph_budget_bytes = memory_budget_gb * (1024**3)

    # m = graph_budget / (num_vectors * 1.3 * 4)
    # 1.3 为层数系数,4 为 node_id 字节数
    max_m = graph_budget_bytes / (num_vectors * 1.3 * 4)

    # 基于维度的基础推荐
    dim_based_m = max(4, min(64, vector_dim // 100))

    # 取两者中的较小值
    recommended_m = min(int(max_m), dim_based_m)

    # 对齐到常用值
    common_values = [4, 8, 12, 16, 24, 32, 48, 64]
    eligible_values = [x for x in common_values if x <= recommended_m]
    if not eligible_values:
        raise ValueError("按当前粗略估算,预算不足以支持最小候选 m=4")
    return max(eligible_values)

# 使用示例
m = recommend_m(
    num_vectors=1_000_000,
    vector_dim=1536,
    memory_budget_gb=2.0  # 为图结构分配 2GB
)
print(f"推荐 m 值:{m}")  # 输出:推荐 m 值:16

3.3 基于召回率目标的公式

目标召回 95%:ef_construct ≈ m × 5,  ef_search ≈ m × 2
目标召回 99%:ef_construct ≈ m × 10, ef_search ≈ m × 4
目标召回 99.9%:ef_construct ≈ m × 20, ef_search ≈ m × 8

四、on_disk 模式:低内存的生产之道

4.1 内存布局问题

默认情况下,Qdrant 的向量数据和 HNSW 图都存储在内存映射文件(mmap)中。操作系统会根据访问频率智能管理哪些页面驻留在 RAM 中。

但在数据量极大时(例如 1 亿个 1536 维 float32 向量的原始编码为 614.4 GB(约 572.2 GiB)),完整加载到内存是不现实的。

4.2 on_disk 模式原理

client.create_collection(
    collection_name="large_collection",
    vectors_config=VectorParams(
        size=1536,
        distance=Distance.COSINE,
        on_disk=True  # 向量数据存磁盘
    ),
    hnsw_config=HnswConfigDiff(
        m=16,
        ef_construct=200,
        on_disk=True  # HNSW 图也存磁盘
    )
)

两个 on_disk 的区别:

配置位置 作用 影响
VectorParams.on_disk=True 向量原始数据存磁盘 内存减少 80%+,查询需磁盘 I/O
HnswConfigDiff.on_disk=True HNSW 图结构存磁盘 图遍历需磁盘 I/O,延迟增加

4.3 on_disk 的性能影响

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph LR
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    A["查询请求"] --> B{"HNSW 图<br/>是否在内存?"}
    B -->|"on_disk=False<br/>图在内存"| C["图遍历:纳秒级"]
    B -->|"on_disk=True<br/>图在磁盘"| D["SSD I/O:微秒级<br/>HDD I/O:毫秒级"]
    C --> E{"向量数据<br/>是否在内存?"}
    D --> E
    E -->|"in memory"| F["距离计算:纳秒级"]
    E -->|"on_disk"| G["加载向量:微秒级"]
    F --> H["返回结果"]
    G --> H
    class B,E decision
    class A,C,D,F,G,H default

4.4 量化 + on_disk 的黄金组合

在大规模场景下,最佳策略是量化压缩 + on_disk:

client.create_collection(
    collection_name="massive_collection",
    vectors_config=VectorParams(
        size=1536,
        distance=Distance.COSINE,
        on_disk=True  # 原始向量存磁盘(用于 rescore)
    ),
    hnsw_config=HnswConfigDiff(
        m=16,
        ef_construct=100,
        on_disk=True
    ),
    quantization_config=ScalarQuantizationConfig(
        scalar=ScalarQuantization(
            type=ScalarType.INT8,
            always_ram=True  # 量化后的向量常驻内存
        )
    )
)

效果:

  • 假设有 1 亿个 1536 维向量,原始 float32 编码为 614.4 GB(约 572.2 GiB),存磁盘
  • 量化后 int8 编码为 153.6 GB(约 143.1 GiB),常驻 RAM,用于粗排;以上均不含图结构等开销
  • HNSW 图存磁盘,热点页面由 OS 缓存
  • Rescore 时按需从磁盘读取原始向量

五、Scalar Quantization 标量量化

5.1 原理:float32 → int8

标量量化(SQ)是最直观的压缩方式:将每个维度的 float32 值映射到 int8 范围(-128 到 127)。

映射公式:

int8_value = round((float_value - min_val) / (max_val - min_val) × 255) - 128

还原公式:
float_approx = (int8_value + 128) / 255 × (max_val - min_val) + min_val

其中 min_val 和 max_val 从训练数据中统计得出。

5.2 内存压缩效果

原始 float32:4 字节/维度
量化 int8:   1 字节/维度

压缩比:4x

对于 100 万个 1536 维向量:

  • 原始:100万 × 1536 × 4 = 5.86 GB
  • SQ后:100万 × 1536 × 1 = 1.47 GB

5.3 配置方式

from qdrant_client.models import (
    ScalarQuantizationConfig, ScalarQuantization, ScalarType
)

# 在创建集合时配置
client.create_collection(
    collection_name="sq_collection",
    vectors_config=VectorParams(size=768, distance=Distance.COSINE),
    quantization_config=ScalarQuantizationConfig(
        scalar=ScalarQuantization(
            type=ScalarType.INT8,        # 当前仅支持 INT8
            quantile=0.99,               # 用于确定映射范围的分位数
            always_ram=True              # 量化向量是否始终驻留内存
        )
    )
)

# 或对已有集合更新量化配置
client.update_collection(
    collection_name="existing_collection",
    quantization_config=ScalarQuantizationConfig(
        scalar=ScalarQuantization(
            type=ScalarType.INT8,
            quantile=0.99,
            always_ram=False
        )
    )
)

5.4 quantile 参数详解

quantile 用于处理离群值。若直接使用向量的 min/max 范围,少数极端值会导致大多数值被压缩到很窄的范围,损失精度。

# quantile=0.99 的含义:
# 用第 0.5% ~ 99.5% 分位数范围进行映射
# 超出范围的值被截断到边界

# quantile=0.95:更激进截断,精度稍低但对离群值更鲁棒
# quantile=1.0:使用完整 min/max,最大保留精度

召回率 vs quantile 的关系:

quantile 召回率(@top10) 说明
0.95 ~98% 离群值截断较多
0.99 ~99% 推荐默认值
1.00 ~99.5% 完整范围,精度最高

5.5 SIMD 加速原理

标量量化的另一个优势是 int8 运算的 SIMD 加速:

CPU SIMD 寄存器宽度:256 bit(AVX2)或 512 bit(AVX-512)

float32 计算:256 bit / 32 bit = 一次处理 8 个维度
int8 计算:  256 bit / 8 bit  = 一次处理 32 个维度

速度提升:4x(理论上限)

Qdrant 内部使用 Rust 实现,充分利用 SIMD 指令集,int8 距离计算比 float32 快约 2-4 倍。


六、Product Quantization 乘积量化

6.1 原理:分段压缩

乘积量化(PQ)将高维向量分割成多个子空间,对每个子空间独立进行 k-means 聚类,用聚类中心的索引替代原始数据。

原始向量 d=1536 维 → 分成 m=96 段,每段 16 维
每段独立聚类:256 个中心(8 bit 编码)
存储:96 段 × 1 字节 = 96 字节

原始大小:1536 × 4 = 6144 字节
压缩后:96 字节
压缩比:64x

6.2 PQ 训练阶段

PQ 需要预先训练,Qdrant 在后台自动完成:

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph TD
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    A["采样训练数据<br/>(部分向量子集)"] --> B["将向量分成 m 个子段"]
    B --> C["对每段运行 k-means<br/>获得 256 个聚类中心"]
    C --> D["生成码本<br/>Codebook"]
    D --> E["对所有向量编码<br/>每维度→聚类中心索引"]
    E --> F["存储压缩后的编码"]
    class A,B,C,D,E,F default

6.3 配置方式

from qdrant_client.models import (
    ProductQuantizationConfig, ProductQuantization, CompressionRatio
)

client.create_collection(
    collection_name="pq_collection",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
    quantization_config=ProductQuantizationConfig(
        product=ProductQuantization(
            compression=CompressionRatio.X16,  # 压缩比选项
            always_ram=True
        )
    )
)

压缩比选项:

CompressionRatio 压缩倍数 子段数(1536维) 每段维度
X4 4x 1536 段 1 维
X8 8x 768 段 2 维
X16 16x 384 段 4 维
X32 32x 192 段 8 维
X64 64x 96 段 16 维

warning PQ 的局限性

  • 不支持 SIMD 加速:PQ 距离计算(查表法)对 SIMD 不友好,比 SQ 更慢
  • 需要训练数据:初始数据不足时,码本质量差
  • 召回率损失更大:相比 SQ,相同压缩比下召回率更低
  • 推荐场景:超大数据集(亿级),内存严重受限,能接受较低召回率

6.4 ADC(非对称距离计算)

PQ 使用非对称距离计算提高精度:

对称计算(低精度):
  距离(压缩A, 压缩B) = 码本查表

非对称计算(高精度,Qdrant 使用):
  距离(原始Query, 压缩向量) = 对每个子段,计算 Query子段 与 各聚类中心的距离

ADC 允许查询向量保持 float32 精度,只压缩存储的向量,显著减少精度损失。


七、Binary Quantization 二进制量化

7.1 原理:最激进的压缩

二进制量化(BQ)将每个浮点维度压缩为 1 bit:

量化规则:
  float_value >= 0 → 1
  float_value < 0  → 0

1536 维 float32 向量:
  原始:1536 × 4 = 6144 字节 = 6 KB
  BQ后:1536 / 8 = 192 字节

压缩比:32x
内存节省:96.9%

7.2 距离计算的神奇加速

BQ 的真正威力在于汉明距离(Hamming Distance)计算的极速运行:

float32 余弦相似度:1536 次乘法 + 1535 次加法 + 2 次开方
汉明距离:        1536 bit XOR + 1536 bit POPCOUNT

CPU 原生指令:
  POPCNT 指令:一次处理 64 bit,计算 64 维的汉明距离
  1536 维 = 24 次 POPCNT 指令

理论加速比:约 40x(相比 float32 余弦相似度)

7.3 配置方式

from qdrant_client.models import (
    BinaryQuantizationConfig, BinaryQuantization
)

client.create_collection(
    collection_name="bq_collection",
    vectors_config=VectorParams(
        size=1536,
        distance=Distance.COSINE  # BQ 最适合余弦相似度
    ),
    quantization_config=BinaryQuantizationConfig(
        binary=BinaryQuantization(
            always_ram=True  # 推荐始终驻留内存
        )
    )
)

7.4 BQ 的适用条件

important BQ 不是万能的

BQ 的召回率高度依赖模型输出的向量分布。以下条件下 BQ 效果好:

适合 BQ 的场景:

  • OpenAI text-embedding-ada-002、text-embedding-3-*:这些模型输出的向量经过 L2 归一化,维度分布对称,BQ 召回率可达 95%+
  • 高维向量(≥1024 维):高维下信息更分散,量化损失相对更小
  • 余弦相似度度量:BQ 对余弦相似度的近似质量优于欧式距离

不适合 BQ 的场景:

  • 低维向量(< 512 维)
  • 向量分布极不均匀的模型
  • 使用欧氏距离(L2)度量
  • 对召回率要求极高(>99.5%)

7.5 不同模型的 BQ 召回率假设示例

以下数值暂作为未开启 Rescore 时的模型召回率假设示例保留;本文未提供可追溯的官方基准来源及测试条件,不将其作为官方实测结果:

模型 向量维度 BQ 召回率(@top10)
text-embedding-3-large 3072 ~98%
text-embedding-3-small 1536 ~96%
text-embedding-ada-002 1536 ~95%
all-MiniLM-L6-v2 384 ~85%
GTE-base 768 ~90%

八、三种量化方案横向对比

8.1 综合对比表

维度 Scalar (SQ) Product (PQ) Binary (BQ)
压缩比 4x 4x ~ 64x 32x
召回率损失 极低(<1%) 中等(2-5%) 低-中(取决于模型)
计算速度 2-4x 加速 1-2x 加速 最高 40x 加速
内存节省 75% 75%~98% 96.9%
需要训练 否(自动统计) 是(k-means) 否
SIMD 支持 是(int8) 否 是(位运算)
适用维度 任意 高维(>512) 高维(>1024)
适用场景 通用首选 超大规模存储 OpenAI 等主流模型
工程复杂度 低 中 低

8.2 内存占用对比(1M 向量,1536 维)

原始 float32:    5.86 GB  ████████████████████████ 100%
SQ int8:        1.47 GB  ██████                    25%
BQ binary:      0.18 GB  █                         3%
PQ X16:         约 0.36 GiB(仅编码)               6.25%
PQ X64:         约 0.089 GiB(仅编码)              1.5625%

8.3 选择决策树

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    A["选择量化方案"] --> B{"内存预算如何?"}
    B -->|"充足(<4x 压缩足够)"| C["Scalar Quantization<br/>简单可靠,首选"]
    B -->|"紧张(需要 >4x)"| D{"向量维度?"}
    D -->|">= 1024 维"| E{"模型是 OpenAI/主流模型?"}
    D -->|"< 1024 维"| F["Product Quantization<br/>高压缩,注意召回损失"]
    E -->|"是"| G["Binary Quantization<br/>40x 加速,32x 压缩"]
    E -->|"否"| H{"内存极端紧张?"}
    H -->|"是"| F
    H -->|"否"| C
    class B,D,E,H decision
    class A,C,F,G default

九、Rescore 重排机制

9.1 量化召回损失的解决方案

量化不可避免地引入精度损失。Qdrant 通过 Rescore(重排)机制在保留量化速度优势的同时,大幅恢复召回率:

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph LR
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    A["查询向量"] --> B["BQ/SQ/PQ 粗排<br/>(量化向量)<br/>快速找出候选集 top-K×N"]
    B --> C["候选集扩大<br/>(如取 top 100)"]
    C --> D["原始 float32 向量<br/>精排 top-K"]
    D --> E["最终结果<br/>top-10"]
    class A,B,C,D,E default

核心思想:用量化向量快速缩小候选范围,用原始向量精确排名。

9.2 配置 Rescore

from qdrant_client.models import SearchParams, QuantizationSearchParams

results = client.search(
    collection_name="bq_collection",
    query_vector=[0.1, 0.2, ...],
    limit=10,
    search_params=SearchParams(
        quantization=QuantizationSearchParams(
            ignore=False,       # 不忽略量化(使用量化加速)
            rescore=True,       # 开启重排(精度恢复)
            oversampling=3.0    # 过采样倍数:先取 10×3=30 个候选
        )
    )
)

9.3 oversampling 参数

oversampling 是 Rescore 最关键的参数,控制粗排候选集的扩大倍数:

最终需要 limit=10 个结果
oversampling=2.0 → 粗排取 20 个候选 → 精排后返回 10
oversampling=3.0 → 粗排取 30 个候选 → 精排后返回 10
oversampling=5.0 → 粗排取 50 个候选 → 精排后返回 10

oversampling 对召回率的提升(Binary Quantization,OpenAI 1536维):

oversampling 召回率(@top10) 延迟增加
1.0(不扩大候选集;是否重排由 rescore 决定) ~95%(示例假设,非实测结论) 0%(示例基线)
2.0 ~98% +20%
3.0 ~99% +40%
5.0 ~99.5% +80%
10.0 ~99.9% +150%

tip 推荐配置

对于 BQ:oversampling=2.0 到 3.0 是甜点区,用 40% 的延迟换取 4% 的召回提升。 对于 SQ:oversampling=1.5 通常足够,因为 SQ 精度损失极小。

9.4 量化 + Rescore 的完整性能对比

以下按 100 万个 1536 维向量、搜索 top-10 保留为未验证的假设对比:延迟和召回率不是实测结论,内存列主要表示向量编码容量而非实例总内存。Rescore 仍需访问原始向量,表中 SQ 行未指定其存储位置;float32 存储本身不保证 100% 召回,100% 仅可作为相同条件下精确搜索基线的假设。

方案 延迟 内存 召回率 适用场景
原始 float32 50ms 5.86GB 100% 精度要求极高
SQ + Rescore 25ms 1.47GB 99.5% 通用生产推荐
BQ(无Rescore) 5ms 0.18GB 95% 大规模粗排
BQ + Rescore(3x) 8ms 0.18GB(+5.86GB磁盘) 99% 大规模精准搜索

十、Named Vectors 独立索引策略

10.1 多向量集合的场景

现代 RAG 和多模态应用中,同一个文档可能需要存储多种向量:

from qdrant_client.models import VectorsConfig

# 多向量配置:每种向量独立的 HNSW 参数
client.create_collection(
    collection_name="multimodal_collection",
    vectors_config={
        # 文本向量:高维,需要高质量索引
        "text": VectorParams(
            size=1536,
            distance=Distance.COSINE,
            hnsw_config=HnswConfigDiff(
                m=32,
                ef_construct=400,
                on_disk=False
            )
        ),
        # 图像向量:中维,平衡配置
        "image": VectorParams(
            size=512,
            distance=Distance.COSINE,
            hnsw_config=HnswConfigDiff(
                m=16,
                ef_construct=200,
                on_disk=True  # 将该命名向量的 HNSW 图存磁盘;不设置原始向量的存储位置
            )
        ),
        # 摘要向量:小维,低内存配置
        "summary": VectorParams(
            size=384,
            distance=Distance.COSINE,
            hnsw_config=HnswConfigDiff(
                m=8,
                ef_construct=100
            )
        )
    }
)

10.2 为不同向量配置不同量化

client.create_collection(
    collection_name="hybrid_quantization",
    vectors_config={
        "dense": VectorParams(
            size=1536,
            distance=Distance.COSINE,
            quantization_config=BinaryQuantizationConfig(
                binary=BinaryQuantization(always_ram=True)
            )
        ),
        "colbert": VectorParams(
            size=128,
            distance=Distance.COSINE,
            quantization_config=ScalarQuantizationConfig(
                scalar=ScalarQuantization(
                    type=ScalarType.INT8,
                    always_ram=True
                )
            )
        )
    }
)

10.3 搜索时指定向量

# 仅使用文本向量搜索
text_results = client.search(
    collection_name="multimodal_collection",
    query_vector=("text", text_embedding),
    limit=10
)

# 使用图像向量搜索
image_results = client.search(
    collection_name="multimodal_collection",
    query_vector=("image", image_embedding),
    limit=10
)

# 混合搜索(多向量结果通过 RRF 倒数排名融合)
from qdrant_client.models import Prefetch, FusionQuery, Fusion

hybrid_results = client.query_points(
    collection_name="multimodal_collection",
    prefetch=[
        Prefetch(
            query=text_embedding,
            using="text",
            limit=50
        ),
        Prefetch(
            query=image_embedding,
            using="image",
            limit=50
        )
    ],
    query=FusionQuery(fusion=Fusion.RRF),  # 倒数排名融合
    limit=10
)

十一、稀疏向量索引

11.1 稀疏向量的应用场景

稀疏向量(Sparse Vector)主要用于词汇匹配(BM25、SPLADE 等模型输出),与稠密向量结合实现混合搜索:

from qdrant_client.models import SparseVectorParams, SparseIndexParams

client.create_collection(
    collection_name="hybrid_search",
    vectors_config={
        "dense": VectorParams(
            size=1536,
            distance=Distance.COSINE
        )
    },
    sparse_vectors_config={
        "sparse": SparseVectorParams(
            index=SparseIndexParams(
                on_disk=False  # 稀疏索引是否存磁盘
            )
        )
    }
)

11.2 稀疏向量的索引机制

稀疏向量不使用 HNSW,而是基于倒排索引(Inverted Index):

词汇 "machine"  → [点ID 1, 点ID 5, 点ID 23, ...]
词汇 "learning" → [点ID 1, 点ID 7, 点ID 45, ...]
词汇 "python"   → [点ID 3, 点ID 7, 点ID 89, ...]

查询时取并集,计算点积得分,内存效率极高。

11.3 插入稀疏向量

from qdrant_client.models import PointStruct, SparseVector

client.upsert(
    collection_name="hybrid_search",
    points=[
        PointStruct(
            id=1,
            vector={
                # 稠密向量
                "dense": [0.1, 0.2, 0.3, ...],
                # 稀疏向量:只存非零维度
                "sparse": SparseVector(
                    indices=[42, 156, 891, 1204],
                    values=[0.85, 0.63, 0.41, 0.29]
                )
            },
            payload={"title": "Machine Learning with Python"}
        )
    ]
)

十二、索引构建生命周期

12.1 Segment 分段机制

Qdrant 不会对每次插入立即重建索引,而是采用**分段(Segment)**管理策略:

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
stateDiagram-v2
    [*] --> Memtable : 新数据写入内存缓冲区
    Memtable --> Plain_Segment : 数据量达到阈值
    Plain_Segment --> Indexed_Segment : 后台优化器触发
    Indexed_Segment --> Merged_Segment : 多个 Segment 合并
    Merged_Segment --> [*] : 保持最优状态

    note right of Memtable
        支持即时搜索
        无索引(暴力扫描)
    end note

    note right of Plain_Segment
        数据持久化
        仍然暴力扫描
    end note

    note right of Indexed_Segment
        HNSW 索引已建立
        高速 ANN 搜索
    end note

12.2 触发索引构建的条件

# 在 collection 的优化器配置中控制
from qdrant_client.models import OptimizersConfigDiff

client.update_collection(
    collection_name="my_collection",
    optimizer_config=OptimizersConfigDiff(
        # 触发索引构建的向量数据大小阈值(KB),不是向量条数
        indexing_threshold=20000,

        # 将向量存储转换为内存映射存储的大小阈值(KB)
        memmap_threshold=200000,

        # 已删除向量的比例超过此值时触发清理
        deleted_threshold=0.2,

        # 单个 Segment 的最大向量数据大小(KB),不是 Segment 个数
        max_segment_size=100000,

        # 最大优化任务并行数
        max_optimization_threads=2,

        # 数据刷写间隔(秒),不是数据积压阈值
        flush_interval_sec=5
    )
)

12.3 轮询优化器状态(不强制触发优化)

# 轮询优化器状态;状态为 ok 不等同于所有段已完成索引优化
import time

while True:
    collection_info = client.get_collection("my_collection")
    optimizer_status = collection_info.optimizer_status

    if str(optimizer_status) == "ok":
        print("优化器状态为 ok;仍需结合集合状态与索引进度判断完成情况")
        break

    print(f"优化中:{optimizer_status}")
    time.sleep(2)

十三、调优决策树

13.1 场景分类与推荐配置

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
    classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
    classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
    classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
    classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
    classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
    A["确定优化目标"] --> B{"主要瓶颈是什么?"}

    B -->|"查询延迟太高"| C{"数据量级?"}
    B -->|"内存不足"| D{"可接受召回损失?"}
    B -->|"索引构建太慢"| E["降低 ef_construct<br/>增加 max_indexing_threads"]
    B -->|"召回率不够"| F["提高 ef_construct<br/>提高 m<br/>提高搜索时 ef"]

    C -->|"< 100万"| G["提高 m 到 32<br/>ef_construct=400<br/>关闭量化"]
    C -->|"100万 ~ 1000万"| H["SQ int8 + Rescore<br/>m=16, ef_construct=200"]
    C -->|"> 1000万"| I["BQ + Rescore(3x)<br/>on_disk=True<br/>always_ram=True量化"]

    D -->|"可接受 5% 损失"| J["Binary Quantization<br/>32x 压缩"]
    D -->|"只接受 1% 损失"| K["Scalar Quantization<br/>4x 压缩"]
    D -->|"不接受损失"| L["on_disk=True 存原始<br/>接受延迟增加"]
    class B,C,D decision
    class A,E,F,G,H,I,J,K,L default

13.2 关键配置参数速查

目标 关键参数 推荐值
最高召回率 m, ef_construct, ef m=32, ef_c=400, ef=256
最低延迟 m, ef,开启 BQ m=8, ef=32, BQ
最小内存 on_disk, 量化 BQ + on_disk=True
最快构建 ef_construct, threads ef_c=50, threads=16
过滤友好 payload_m payload_m = m

十四、生产环境配置案例

14.1 案例一:RAG 知识库(通用场景)

示例假设与目标:500 万文档,1536 维,以查询 P99 延迟 < 50ms 为目标;本文未提供达标测试结果。

client.create_collection(
    collection_name="rag_knowledge_base",
    vectors_config=VectorParams(
        size=1536,
        distance=Distance.COSINE,
        on_disk=False,  # 内存充足,不用磁盘
        quantization_config=ScalarQuantizationConfig(
            scalar=ScalarQuantization(
                type=ScalarType.INT8,
                quantile=0.99,
                always_ram=True
            )
        )
    ),
    hnsw_config=HnswConfigDiff(
        m=16,
        ef_construct=200,
        full_scan_threshold=10000,
        on_disk=False
    ),
    optimizer_config=OptimizersConfigDiff(
        indexing_threshold=20000,
        max_optimization_threads=4
    )
)

# 搜索时配置
results = client.search(
    collection_name="rag_knowledge_base",
    query_vector=query_embedding,
    limit=5,
    search_params=SearchParams(
        hnsw_ef=128,
        quantization=QuantizationSearchParams(
            rescore=True,
            oversampling=2.0
        )
    )
)

编码容量估算与待验证性能假设:

  • SQ 编码:500万 × 1536 × 1字节 = 7.68 GB(约 7.15 GiB);原始 float32 向量为 30.72 GB(约 28.61 GiB)。本配置原始向量 on_disk=False,SQ 编码大小不是实例总内存,还需计入原始向量、HNSW 图及运行开销。
  • 召回率假设:~99.5%,需通过实际数据测试确认。
  • 查询延迟假设:10-30ms,未给出硬件、并发和测量条件,不能作为性能承诺。

14.2 案例二:大规模图像搜索

示例假设与目标:1 亿图像,512 维 CLIP 向量,以实例内存 < 32GB 为目标;需评估该向量分布的 BQ 召回损失、图缓存和重排 I/O,本文未提供达标测试结果。

client.create_collection(
    collection_name="image_search",
    vectors_config=VectorParams(
        size=512,
        distance=Distance.COSINE,
        on_disk=True,  # 向量存磁盘(1亿 × 512 × 4B = 204.8 GB,约 190.73 GiB)
        quantization_config=BinaryQuantizationConfig(
            binary=BinaryQuantization(
                always_ram=True  # BQ 编码为 204.8 GB/32=6.4 GB(约 5.96 GiB);不含图、缓存及运行开销
            )
        )
    ),
    hnsw_config=HnswConfigDiff(
        m=16,
        ef_construct=100,
        on_disk=True  # 图结构也存磁盘
    )
)

# 搜索时使用 Rescore 提升精度
results = client.search(
    collection_name="image_search",
    query_vector=image_embedding,
    limit=20,
    search_params=SearchParams(
        hnsw_ef=64,
        quantization=QuantizationSearchParams(
            rescore=True,
            oversampling=3.0  # 取 60 个候选,精排返回 20
        )
    )
)

14.3 案例三:实时推荐系统

示例假设与目标:1000 万商品向量,256 维,写入频繁,以查询 P99 < 20ms 为目标;本文未提供达标测试结果。

client.create_collection(
    collection_name="recommendation",
    vectors_config=VectorParams(
        size=256,
        distance=Distance.DOT,
        on_disk=False
    ),
    hnsw_config=HnswConfigDiff(
        m=12,
        ef_construct=100,
        full_scan_threshold=5000,
        max_indexing_threads=2  # 预留 CPU 给查询
    ),
    optimizer_config=OptimizersConfigDiff(
        indexing_threshold=10000,
        flush_interval_sec=2,   # 设置刷写间隔为 2 秒;不保证查询可见性或数据新鲜度
        max_optimization_threads=1  # 最小化优化对查询的干扰
    )
)

总结

Qdrant 的索引与量化系统体现了工程实践中精度与效率的精妙平衡:

  1. HNSW 是基础:理解 m 和 ef_construct 的关系,根据数据规模和内存预算做出合理选择
  2. 量化是杠杆:SQ 是通用首选,BQ 在大规模 OpenAI 向量场景下有压倒性优势,PQ 是内存极端受限的最后手段
  3. Rescore 是保险:开启 Rescore + 适度 oversampling,在量化带来速度和内存优势的同时,将精度损失降到可接受范围
  4. on_disk 是扩展:向量和图结构均支持磁盘存储,配合 SSD 可实现数十亿级别的向量搜索

相关文档

#向量数据库 #Qdrant #HNSW #量化压缩 #索引优化 #RAG #性能调优