未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合建立Qdrant数据模型认知,并参考多向量检索、配置调优、备份和模型升级的操作结构,不是可顺序执行的完整脚本;示例中的省略向量、models导入、业务函数及对应集合需自行准备。当前仍缺少正文指向的Inline Storage优化章节和三种量化方式的详细配置材料;HNSW扫描建议混用了点数经验与KB阈值,并将阈值设为0描述为无条件使用HNSW,不能直接作为查询规划依据。Dot更快、Bool索引最快等选型结论缺少比较条件与来源;分片规划中的每节点数量未区分逻辑分片与副本,容量估算也不能代替实测。快照跨版本兼容性、全量快照恢复接口及部署适用范围尚未核验,生产恢复前需按目标版本和拓扑确认并演练;file://为服务端可访问路径,集群备份须覆盖实际分片。wait=True和别名原子切换不单独保证全副本可见或零停机,升级与回滚仍需同步查询模型、追平数据并保留可用旧集合。 已取消无法确定目标的可点击引用「#向量配置 vectors_config」,保留原提及;Missing anchor: #向量配置 vectors_config 已取消无法确定目标的可点击引用「#HNSW 配置 hnsw_config」,保留原提及;Missing anchor: #HNSW 配置 hnsw_config 已取消无法确定目标的可点击引用「#优化器配置 optimizer_config」,保留原提及;Missing anchor: #优化器配置 optimizer_config 已取消无法确定目标的可点击引用「#WAL 配置 wal_config」,保留原提及;Missing anchor: #WAL 配置 wal_config 已取消无法确定目标的可点击引用「#量化配置 quantization_config」,保留原提及;Missing anchor: #量化配置 quantization_config 已取消无法确定目标的可点击引用「#逻辑组合:must / should / must_not」,保留原提及;Missing anchor: #逻辑组合:must / should / must_not 已取消无法确定目标的可点击引用「#Inline Storage 优化」,保留原提及;Missing anchor: #Inline Storage 优化 已取消无法确定目标的可点击引用「向量数据库/Qdrant/02-Qdrant架构深度解析」,保留原提及;Missing link target: 向量数据库/Qdrant/02-Qdrant架构深度解析 已取消无法确定目标的可点击引用「向量数据库/Qdrant/00-Qdrant学习路线图」,保留原提及;Missing link target: 向量数据库/Qdrant/00-Qdrant学习路线图 已取消无法确定目标的可点击引用「向量数据库/通用知识/01-向量搜索原理与ANN算法」,保留原提及;Missing link target: 向量数据库/通用知识/01-向量搜索原理与ANN算法 已取消无法确定目标的可点击引用「向量数据库/Milvus/02-Milvus架构深度解析」,保留原提及;Missing link target: 向量数据库/Milvus/02-Milvus架构深度解析
Qdrant 核心概念详解
abstract 文档概述
本文系统地梳理 Qdrant 的核心数据模型与操作概念,包括 Collection、Point、Vector、Payload、Filter、Snapshot 和 Alias 七大模块。通过详尽的配置说明、类型对比和实战代码示例,帮助工程师建立对 Qdrant 数据模型的完整认知。
目录
- 概念体系总览
- Collection 集合
- Collection 的本质
- #向量配置 vectors_config
- #HNSW 配置 hnsw_config
- #优化器配置 optimizer_config
- #WAL 配置 wal_config
- #量化配置 quantization_config
- Collection 操作 API
- Point 点
- Vector 向量
- Payload 元数据
- Payload 索引
- Filter 过滤器
- #逻辑组合:must / should / must_not
- Match 精确匹配条件
- Range 范围条件
- Geo 地理条件
- IsNull / IsEmpty 空值条件
- Nested 嵌套对象条件
- HasId 点 ID 条件
- Filter 性能优化策略
- Snapshot 快照
- Alias 别名
- 数据模型设计最佳实践
概念体系总览
在开始深入每个概念之前,先建立整体的概念地图:
%%{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 QDRANT["Qdrant 实例"]
subgraph COL["Collection(集合)"]
subgraph POINT["Point(点)"]
ID["ID<br/>唯一标识符"]
VEC["Vector(向量)<br/>Dense / Sparse / Multi"]
PAY["Payload(元数据)<br/>JSON 键值对"]
end
CONFIG["Collection 配置<br/>vectors_config<br/>hnsw_config<br/>optimizer_config<br/>wal_config"]
IDX["Payload 索引<br/>Keyword / Integer / Float<br/>Geo / Full-text / Bool"]
end
ALIAS["Alias(别名)<br/>指向 Collection 的逻辑名"]
SNAP["Snapshot(快照)<br/>Collection 的持久化备份"]
end
FILTER["Filter(过滤器)<br/>must / should / must_not<br/>Match / Range / Geo / IsNull"] -->|"查询时应用"| COL
ALIAS -->|"指向"| COL
SNAP -->|"备份自"| COL
IDX -->|"加速"| FILTER
class QDRANT,COL,POINT,ID,VEC,PAY,CONFIG,IDX,ALIAS,SNAP,FILTER default
Qdrant 与关系数据库的类比:
| Qdrant 概念 | 关系数据库类比 | 关键区别 |
|---|---|---|
| Collection | Table(表) | 以向量为中心,无固定列 Schema |
| Point | Row(行) | 包含向量,不仅仅是结构化数据 |
| Payload | Columns(列) | 动态 JSON,无需预定义结构 |
| Payload 索引 | Column Index | 仅对查询频繁字段创建 |
| Filter | WHERE 子句 | 与向量搜索深度融合 |
| Alias | View(视图) | 轻量级名称映射,而非物化视图 |
| Snapshot | Database Backup | 二进制级别的数据快照 |
Collection 集合
Collection 的本质
Collection 是 Qdrant 中数据组织的最高层级,类似于关系数据库中的"表",但具有以下特点:
- 向量中心:每个 Collection 必须声明向量的维度和距离度量
- 动态 Payload:不像传统表需要预定义所有列,Payload 是自由形式的 JSON
- 物理隔离:不同 Collection 的数据完全隔离,不同 Collection 的向量维度可以不同
- 配置丰富:向量索引、优化器、WAL、量化等参数均在 Collection 级别配置
tip 何时创建多个 Collection?
- 不同业务域(如"商品向量"和"用户向量")
- 不同向量维度(不同 embedding 模型的输出)
- 不同的索引参数需求
- 不同的数据访问模式(高频查询 vs 批量分析)
向量配置 vectors_config
vectors_config 是 Collection 的核心配置,定义了存储何种向量以及如何组织。
单一向量配置(简单场景):
PUT /collections/products
{
"vectors": {
"size": 1536,
"distance": "Cosine",
"on_disk": false,
"datatype": "float32"
}
}
多命名向量配置(高级场景):
PUT /collections/products_multi
{
"vectors": {
"text_embedding": {
"size": 1536,
"distance": "Cosine",
"on_disk": true
},
"image_embedding": {
"size": 768,
"distance": "Dot",
"on_disk": false
},
"price_vector": {
"size": 128,
"distance": "Euclid",
"on_disk": false
}
},
"sparse_vectors": {
"bm25_sparse": {
"index": {
"full_scan_threshold": 5000
}
}
}
}
向量配置参数详解:
| 参数 | 类型 | 可选值 | 说明 |
|---|---|---|---|
size |
integer | 任意正整数 | 向量维度,必须与 embedding 模型输出一致 |
distance |
string | Cosine / Dot / Euclid / Manhattan | 距离度量类型 |
on_disk |
boolean | true / false | 向量是否存储在磁盘(否则常驻内存) |
datatype |
string | float32 / float16 / uint8 | 向量元素的数值类型 |
multivector_config |
object | - | 多向量配置,见 MultiVector 章节 |
hnsw_config |
object | - | 该向量专属的 HNSW 配置(覆盖 Collection 级别) |
quantization_config |
object | - | 该向量专属的量化配置 |
datatype 的存储影响:
| datatype | 每维度占用 | 1536 维向量占用 | 精度 |
|---|---|---|---|
| float32(默认) | 4 bytes | 6,144 bytes | 最高(IEEE 754 单精度) |
| float16 | 2 bytes | 3,072 bytes | 高(约 3 位小数精度) |
| uint8 | 1 byte | 1,536 bytes | 中(预量化,需归一化到 0-255) |
note float16 vs Scalar Quantization
datatype: float16 是在写入阶段就以半精度存储,不可逆。而 Scalar Quantization 是在向量存储后,额外构建一个 int8 的量化副本,原始 float32 数据仍然保留(用于 Rescore)。两者各有适用场景。
HNSW 配置 hnsw_config
hnsw_config 控制向量索引的构建参数和存储方式,直接影响搜索精度与性能。
PUT /collections/my_collection
{
"hnsw_config": {
"m": 16,
"ef_construct": 100,
"full_scan_threshold": 10000,
"max_indexing_threads": 0,
"on_disk": false,
"payload_m": 16,
"inline_storage": false
}
}
参数深度说明:
m(默认:16)
HNSW 图中每个节点在每一层的最大出边数量。这是影响索引质量最重要的参数:
m=8: 内存使用少,图质量一般,适合内存受限场景
m=16: 默认值,在大多数场景下平衡良好
m=32: 更高的召回率,但内存使用约增加 1 倍,构建时间更长
m=64: 极高召回率,用于对精度要求极高的场景
HNSW 图内存估算(近似):
memory_per_point ≈ m * 2 * 8 bytes(考虑双向边)
1M 点,m=16:约 256 MB 仅用于图结构(不含向量数据)
ef_construct(默认:100)
构建索引时的候选队列大小,决定索引质量:
ef_construct=50: 快速构建,索引质量一般
ef_construct=100: 默认,良好的质量与速度平衡
ef_construct=200: 可用于测试更高索引质量;构建时间约2倍仅作为待测试假设,不代表实际结果
ef_construct=500: 接近最优索引,适合离线批量建索引场景
注意:ef_construct 是构建阶段参数,不直接设置搜索候选数;它改变索引质量,也可能间接影响搜索性能
但更好的索引质量 = 相同 ef 下更高的召回率
full_scan_threshold(默认:10000)
full_scan_threshold 按向量数据量(KB)计,而非按点数计;查询规划器可据候选向量数据量选择全量扫描,不能将10000直接解释为1万个点。
设计逻辑:
HNSW 有固定的图构建开销和内存占用
对于小数据集(<1万点),暴力扫描反而更快
因为 HNSW 图的层级遍历有固定的常数开销
实践建议:
生产环境通常保持默认值(10000)
如果数据集确定很大且召回率优先,可设为 0(始终使用 HNSW)
on_disk(默认:false)
将 HNSW 图结构存储在磁盘而非内存:
on_disk=false(默认):
图结构全部在内存,图遍历无 IO 开销
内存使用:向量数据 + HNSW 图;图内存取决于层级、连接数和实现,本文的简化估算不能替代实际测量
on_disk=true:
图结构通过 memmap 映射,访问时按需从磁盘加载
冷启动慢,但热数据后性能仍可接受
配合 inline_storage=true 效果更好
inline_storage(默认:false)
将量化向量内嵌到 HNSW 图文件中(见 #Inline Storage 优化 章节):
适用条件:
1. on_disk=true(磁盘模式,IO 次数是瓶颈)
2. 已配置量化(有量化向量可以嵌入)
不适用:
纯内存模式(on_disk=false),因为内存中无需减少 IO
优化器配置 optimizer_config
Optimizer 是 Qdrant 的后台工作线程,负责将可追加段(Appendable Segment)转换为优化的不可变段,并清理碎片。
PUT /collections/my_collection
{
"optimizers_config": {
"deleted_threshold": 0.2,
"vacuum_min_vector_number": 1000,
"default_segment_number": 0,
"max_segment_size": 200000,
"memmap_threshold": 200000,
"indexing_threshold": 20000,
"flush_interval_sec": 5,
"max_optimization_threads": 1
}
}
参数详解:
| 参数 | 默认值 | 说明 |
|---|---|---|
deleted_threshold |
0.2 | 触发 Vacuum 的删除比例阈值(20% 已删除则触发清理) |
vacuum_min_vector_number |
1000 | 触发 Vacuum 的最小向量数量(避免过于频繁的小段清理) |
default_segment_number |
0(自动) | 默认 Segment 数量,0 表示根据 CPU 核数自动确定 |
max_segment_size |
200000 | 单个 Segment 的向量数据量上限配置,单位为KB,不是点数;具体默认值以部署配置为准 |
memmap_threshold |
200000 | 使用memmap存储向量的数据量阈值,单位为KB,不是点数;具体默认值以部署配置为准 |
indexing_threshold |
20000 | 触发向量索引构建的数据量阈值,单位为KB,不是点数;具体默认值以部署配置为准 |
flush_interval_sec |
5 | 强制将内存 Segment 数据刷到磁盘的间隔(秒) |
max_optimization_threads |
1 | 并发优化任务数(增大可加速索引构建,但消耗更多 CPU) |
优化器的工作流程:
新点写入 → 落入 Appendable Segment(RAM 中)
↓ 达到 indexing_threshold(按向量数据量KB计)
Indexing Optimizer 触发
↓ 构建 HNSW 索引
转换为 Immutable Segment
↓ 多个小段积累
Merge Optimizer 触发
↓ 合并小段为大段
超过 max_segment_size
↓
分裂为两个新 Immutable Segment
↓ 大量删除操作
Vacuum Optimizer 触发
↓
重建 Segment,物理删除已标记数据
warning 高写入场景的调优
如果写入速度远快于 Optimizer 处理速度,会导致大量 Appendable Segment 堆积,查询时需要扫描更多 Segment,性能下降。此时可考虑:
- 增大
max_optimization_threads - 暂时提高
indexing_threshold(减少触发频率) - 批量写入后手动触发优化
WAL 配置 wal_config
WAL(Write-Ahead Log)配置控制持久化行为:
PUT /collections/my_collection
{
"wal_config": {
"wal_capacity_mb": 32,
"wal_segments_ahead": 0
}
}
| 参数 | 默认值 | 说明 |
|---|---|---|
wal_capacity_mb |
32 | 单个 WAL 段的最大大小(MB) |
wal_segments_ahead |
0 | 提前预分配的 WAL 段数(减少 IO 抖动) |
WAL 大小的权衡:
较小的 wal_capacity_mb(如 8MB):
+ WAL 段切换更频繁,旧段可以更早清理
+ 单次崩溃恢复需要重放的数据更少
- 频繁分配新 WAL 段,可能产生 IO 开销
较大的 wal_capacity_mb(如 128MB):
+ 减少 WAL 段的分配频率
+ 高写入场景下 IO 效率更高
- 崩溃恢复时间可能更长
量化配置 quantization_config
量化配置在 Collection 级别设置默认量化策略,可被向量级别的配置覆盖:
PUT /collections/my_collection
{
"quantization_config": {
"scalar": {
"type": "int8",
"quantile": 0.99,
"always_ram": true
}
}
}
三种量化方式的配置结构,详见 架构深度解析 中的量化章节。
Collection 操作 API
创建 Collection:
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, HnswConfigDiff
client = QdrantClient(host="localhost", port=6333)
client.create_collection(
collection_name="my_products",
vectors_config=VectorParams(
size=1536,
distance=Distance.COSINE,
on_disk=True,
),
hnsw_config=HnswConfigDiff(
m=16,
ef_construct=100,
on_disk=True,
inline_storage=False, # 本示例未配置量化,不启用量化向量内嵌
),
)
查看 Collection 信息:
GET /collections/my_products
删除 Collection:
DELETE /collections/my_products
更新 Collection 配置(在线变更):
from qdrant_client.models import OptimizersConfigDiff
client.update_collection(
collection_name="my_products",
optimizer_config=OptimizersConfigDiff(
indexing_threshold=50000,
max_optimization_threads=2,
),
)
tip 在线配置更新的注意事项
hnsw_config 的 m 和 ef_construct 参数更新后,只对新建的 Segment 生效。已有的 Segment 需要重建才能使用新配置。可以通过触发 Vacuum 操作来强制重建。
Point 点
Point 的组成结构
Point 是 Qdrant 的基本数据单元,类似于关系数据库中的一行记录,但包含向量信息:
{
"id": "a3c4e7f1-9b2d-4e8a-b123-456789abcdef",
"vector": [0.1, 0.2, 0.3, ...],
"payload": {
"title": "iPhone 15 Pro",
"price": 999.99,
"category": "electronics",
"in_stock": true,
"tags": ["smartphone", "apple", "5G"],
"location": {
"lon": 116.397428,
"lat": 39.90923
}
}
}
Point 的三个组成部分:
%%{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
subgraph POINT["Point"]
ID["ID<br/>全局唯一标识<br/>uint64 或 UUID"]
VEC["Vector(s)<br/>一个或多个向量<br/>Dense / Sparse / Multi"]
PAY["Payload<br/>任意 JSON 元数据<br/>无 Schema 限制"]
end
SEARCH["向量搜索"] -->|"计算相似度"| VEC
FILTER["结构化过滤"] -->|"匹配条件"| PAY
RETRIEVE["按 ID 查询"] -->|"精确定位"| ID
class POINT,ID,VEC,PAY,SEARCH,FILTER,RETRIEVE default
Point ID 设计
Qdrant 支持无符号整数和 UUID 两种 Point ID;无需在创建 Collection 时固定 ID 类型,同一 Collection 内可以使用这两种类型。
类型1:unsigned integer(无符号整数)
{
"id": 42,
"vector": [...]
}
- 范围:0 到 18,446,744,073,709,551,615(uint64)
- 优点:内存效率最高,整数比较最快
- 缺点:需要自行管理 ID 分配,避免冲突
- 适用场景:有现成整数主键的业务(如数据库自增 ID)
类型2:UUID(通用唯一标识符)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"vector": [...]
}
- 格式:符合 RFC 4122 的标准 UUID 字符串
- 优点:全球唯一,无需中央协调分配
- 缺点:相比整数 ID 占用更多内存(16 bytes vs 8 bytes)
- 适用场景:分布式系统、微服务架构、需要外部系统生成 ID
ID 选择策略:
推荐使用整数 ID 当:
- 存在现成的业务主键(数据库 ID、订单号等)
- 追求极致的内存效率
- 单节点或可集中管理 ID 分配
推荐使用 UUID 当:
- 多个系统/服务并发写入,无法集中分配 ID
- 需要在不同 Collection 间引用同一实体
- 使用客户端生成 ID(离线批量导入场景)
Point 的版本控制
Qdrant 为每个 Point 维护一个版本号(Version),这是实现多副本一致性的基础:
版本号语义:
- 每次对某个 Point 的写操作(upsert/update/delete),版本号自动递增
- 版本号单调递增,不可回退
- 跨副本的写入冲突以版本号较高者为准(Last-Write-Wins)
版本号的实际用途:
1. WAL 恢复:确保每个操作只被应用一次
2. 副本同步:Follower 判断自己是否落后于 Leader
3. 冲突解决:并发写入同一 Point 时,以最新版本为准
批量操作与性能
Qdrant 的 upsert 操作支持批量写入,单次请求可包含大量 Point:
from qdrant_client.models import PointStruct
# 批量写入(推荐:每批 100-1000 个点)
client.upsert(
collection_name="my_products",
points=[
PointStruct(
id=i,
vector=[0.1] * 1536, # 仅用于演示批量写入结构;实际应使用模型生成的1536维向量
payload={"name": f"product_{i}", "price": i * 10.0}
)
for i in range(1000)
],
wait=True, # 等待操作应用到 Segment(同步模式)
)
wait 参数的语义:
wait=True(同步模式):
等待 WAL 写入 AND Segment 应用完成才返回
等待写入操作应用完成;后续查询的可见性还取决于副本状态、读取一致性及相关配置,不能据此保证任意下一次查询立即看到新数据
写入延迟较高
wait=False(异步模式):
WAL 写入后立即返回
Segment 应用在后台进行
写入延迟极低,但短时间内查询可能看不到新数据
生产场景建议:
- 批量导入:wait=False(提高吞吐量)
- 实时写入:wait=True(等待操作应用完成;读取可见性仍需结合副本状态与一致性配置)
批量写入的性能优化:
| 策略 | 说明 | 效果 |
|---|---|---|
| 批次大小 100-1000 | 平衡 HTTP 开销与处理时间 | 假设性性能参考:相对单点写入提升5-20倍;本文未提供实测条件,不代表实际收益 |
| gRPC 代替 REST | 减少序列化开销 | 假设性性能参考:提升约1.2-1.5倍;实际效果取决于请求规模、序列化与网络条件 |
| 并发多批次 | 多线程并发发送 | 可将随CPU核数扩展作为测试假设,但吞吐量还受服务端资源、网络、磁盘及并发竞争限制 |
| wait=False | 减少等待 Segment 应用 | 延迟降低,吞吐量提升 |
Vector 向量
Dense Vector 稠密向量
稠密向量(Dense Vector)是最常见的向量类型,每个维度都有一个浮点值:
{
"id": 1,
"vector": [0.1, -0.3, 0.5, 0.2, -0.1, ...]
}
稠密向量的特点:
维度:通常 128 ~ 4096 维(取决于 embedding 模型)
- OpenAI text-embedding-3-small: 1536 维
- OpenAI text-embedding-3-large: 3072 维
- CLIP ViT-B/32: 512 维
- Sentence-BERT: 768 维
- E5-large: 1024 维
所有维度均有值(非零密度 ≈ 100%)
适用:语义搜索、图像搜索、推荐系统的大多数场景
Sparse Vector 稀疏向量
稀疏向量(Sparse Vector)只存储非零维度的索引和值,适合高维稀疏数据:
{
"id": 1,
"vector": {
"bm25_sparse": {
"indices": [1234, 5678, 9012],
"values": [0.8, 0.5, 0.3]
}
}
}
稀疏向量的结构:
indices:非零维度的索引数组(uint32,范围 0 ~ 4,294,967,295)
values:对应维度的值数组(float32)
约束:
1. indices 和 values 长度必须相同
2. indices 中不能有重复值
3. 两个数组顺序要一一对应
示例(BM25 关键词权重):
"机器学习" → 词汇表 ID = 1234,TF-IDF = 0.8
"深度学习" → 词汇表 ID = 5678,TF-IDF = 0.5
"神经网络" → 词汇表 ID = 9012,TF-IDF = 0.3
稀疏向量的索引(稀疏倒排索引):
Qdrant 为稀疏向量维护一个倒排索引(Inverted Index),这与传统的全文检索引擎类似,但在向量层面实现:
索引结构:
词汇 ID 1234 → [点 ID: 1, 5, 23, 456, ...]
词汇 ID 5678 → [点 ID: 1, 7, 89, ...]
词汇 ID 9012 → [点 ID: 1, 3, 67, ...]
查询时:
1. 找出查询向量的所有非零维度
2. 从倒排索引中获取每个维度对应的候选点列表
3. 计算候选点与查询向量的点积(dot product)
4. 返回分数最高的 Top-K 点
稀疏向量的核心用途:BM25 混合搜索
# 混合搜索:Dense(语义)+ Sparse(关键词)
client.query_points(
collection_name="documents",
prefetch=[
# 语义检索(Dense Vector)
models.Prefetch(
query=[0.1, 0.2, ...], # dense embedding
using="text_dense",
limit=50,
),
# 关键词检索(Sparse Vector)
models.Prefetch(
query=models.SparseVector(
indices=[1234, 5678],
values=[0.8, 0.5],
),
using="bm25_sparse",
limit=50,
),
],
query=models.FusionQuery(fusion=models.Fusion.RRF), # RRF 融合排序
limit=10,
)
info 稀疏向量的限制
- 稀疏向量必须命名(不能是 Collection 的"默认向量")
- 稀疏向量必须与稠密向量有不同的名称
- 稀疏向量的距离度量只支持点积(Dot Product)
full_scan_threshold控制何时使用倒排索引 vs 暴力扫描
MultiVector 多向量
MultiVector(多向量)允许单个 Point 关联多个向量,用于 ColBERT 等后期交互模型:
{
"id": 1,
"vector": {
"colbert": [
[0.1, 0.2, 0.3, ...],
[0.4, 0.5, 0.6, ...],
[0.7, 0.8, 0.9, ...]
]
}
}
MultiVector 的配置:
PUT /collections/colbert_docs
{
"vectors": {
"colbert": {
"size": 128,
"distance": "Cosine",
"multivector_config": {
"comparator": "max_sim"
}
}
}
}
max_sim 比较器的原理:
ColBERT 的 MaxSim 操作:
查询:[q1, q2, q3](3 个 token 的向量)
文档:[d1, d2, d3, d4, d5](5 个 token 的向量)
MaxSim 计算:
对查询的每个向量 qi,找到文档中与其最相似的向量 dj
score = sum(max_j(sim(qi, dj)) for qi in query)
这模拟了词汇级别的精确匹配语义,比单向量的 CLS 表示更精确
MultiVector 的适用场景:
| 场景 | 说明 |
|---|---|
| ColBERT 文档检索 | 每个 token 一个向量,实现 token-level 交互 |
| 图像-文本多视角 | 同一图像从不同角度/描述的多个向量 |
| 文档多段落表示 | 长文档拆分为多段,每段一个向量 |
Named Vectors 命名向量
Named Vectors(命名向量)是 Qdrant 的重要设计,允许一个 Point 关联多个不同类型/维度的向量:
{
"id": 1,
"vector": {
"title_embedding": [0.1, 0.2, ...],
"body_embedding": [0.3, 0.4, ...],
"image_embedding": [0.5, 0.6, ...]
},
"payload": {
"title": "Qdrant Introduction",
"body": "Qdrant is a vector database...",
"image_url": "https://example.com/img.png"
}
}
命名向量的查询:
# 指定使用哪个向量进行搜索
client.query_points(
collection_name="articles",
query=[0.1, 0.2, ...],
using="title_embedding", # 指定向量名称
limit=10,
)
Named Vectors 的工程价值:
- 减少数据冗余:相关的不同向量表示归属同一 Point,Payload 只需存一份
- 联合查询:先用 title 向量召回候选,再用 body 向量精排
- 多模态统一:文本和图像向量挂载同一 Point,实现跨模态搜索
- 向量局部更新:可以只更新某个命名向量,不影响其他向量
距离度量类型
Qdrant 支持四种向量距离度量,需在 Collection 创建时指定:
| 距离类型 | 公式 | 值域 | 语义 | 适用场景 |
|---|---|---|---|---|
| Cosine | cos(θ)(返回相似度分数) |
[-1, 1] | 分数越大越相似,1表示同方向 | 文本语义相似度(最常用) |
| Dot | dot(a, b)(返回点积分数) |
(-∞, +∞) | 分数越大越相似 | 单位归一化向量的排序与Cosine一致;推荐系统 |
| Euclid | ` | a - b | ||
| Manhattan | ` | a - b |
Cosine vs Dot 的选择:
理论上,对于已归一化(||v|| = 1)的向量:
Cosine相似度(a, b) = dot(a, b)
两者等价,但 Dot Product 计算更快(省去归一化步骤)
实际推荐:
如果你的 embedding 模型输出已归一化向量 → 使用 Dot
如果不确定是否归一化 → 使用 Cosine(更安全)
OpenAI 的 embedding → 两者均可(输出已归一化)
Payload 元数据
Payload 的字段类型
Payload 是 Qdrant 为每个 Point 存储的 JSON 元数据。虽然是动态 Schema,但 Qdrant 识别和优化以下具体类型:
1. Keyword(关键词)
{
"status": "active",
"category": "electronics",
"tags": ["new", "sale", "featured"]
}
- 存储:原始字符串,不分词
- 用途:精确匹配、多值过滤
- 索引:HashMap(O(1) 查询)
- 适用:分类标签、状态字段、枚举值
2. Integer(整数)
{
"price_cents": 9999,
"stock_count": 156,
"year": 2024
}
- 存储:int64 整数
- 用途:范围查询、精确匹配、数值排序
- 索引:BTreeMap(O(log n) 范围查询)
3. Float(浮点数)
{
"rating": 4.5,
"price": 99.99,
"discount_rate": 0.15
}
- 存储:float64 浮点数
- 用途:范围查询、数值过滤
- 索引:BTreeMap
4. Bool(布尔值)
{
"is_available": true,
"is_featured": false,
"has_discount": true
}
- 存储:true/false
- 用途:精确匹配
- 索引:Bitset(极度高效)
5. Datetime(日期时间)
{
"created_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-12-31T23:59:59+08:00"
}
- 存储:遵循 RFC 3339 / ISO 8601 格式
- 用途:时间范围查询
- 索引:BTreeMap(时间戳)
- 支持时区:自动转换为 UTC 存储
6. Geo(地理坐标)
{
"location": {
"lon": 116.397428,
"lat": 39.90923
}
}
- 存储:经纬度对象
- 用途:地理范围查询(圆形、矩形、多边形)
- 索引:R-Tree
7. Text(全文文本)
{
"description": "This is a high-performance vector database written in Rust.",
"title": "Qdrant: Vector Search Engine"
}
- 存储:原始字符串
- 用途:全文搜索(模糊匹配、词语搜索)
- 索引:倒排索引(Inverted Index)
- 与 Keyword 区别:Text 会分词,Keyword 不分词
8. UUID(唯一标识符)
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "7f3f5d2a-1234-5678-abcd-ef0123456789"
}
- 存储:UUID 字符串
- 用途:精确匹配(外键关联)
- 索引:HashMap
Payload 的存储策略
Qdrant 提供两种 Payload 存储策略,在 Collection 级别配置:
PUT /collections/my_collection
{
"on_disk_payload": true
}
| 策略 | on_disk_payload: false(默认) |
on_disk_payload: true |
|---|---|---|
| 存储位置 | 内存(Gridstore/RocksDB) | 磁盘(memmap 文件) |
| 访问延迟 | 极低(内存访问) | 较低(可能有 IO) |
| RAM 消耗 | 高(Payload 全部在内存) | 低(按需加载) |
| 适用场景 | Payload 较小,查询频繁 | Payload 较大,内存有限 |
Payload 大小的估算:
典型 Payload 大小:
最小(几个字段):< 1 KB / 点
中等(数十个字段):1-10 KB / 点
大型(含长文本):10-100 KB / 点
100万个点,平均 Payload = 5 KB:
总 Payload 内存 = 100万 × 5 KB = 5 GB
如果 RAM 中无法容纳 5 GB 的 Payload:
→ 启用 on_disk_payload=true,按需从磁盘加载
Payload 操作 API
设置/覆盖 Payload(set_payload):
# 覆盖指定字段(其他字段不变)
client.set_payload(
collection_name="my_products",
payload={
"status": "sold_out",
"last_updated": "2024-01-15T10:30:00Z"
},
points=[1, 2, 3], # 批量操作多个点
)
覆盖全部 Payload(overwrite_payload):
# 完全替换 Payload(旧字段会被删除)
client.overwrite_payload(
collection_name="my_products",
payload={"status": "active"}, # 只保留 status 字段
points=[1],
)
删除特定 Payload 字段:
client.delete_payload(
collection_name="my_products",
keys=["temporary_flag", "debug_info"],
points=[1, 2, 3],
)
清除全部 Payload:
client.clear_payload(
collection_name="my_products",
points_selector=models.FilterSelector(
filter=models.Filter(
must=[
models.FieldCondition(
key="status",
match=models.MatchValue(value="deprecated"),
)
]
)
),
)
嵌套 Payload 访问
Qdrant 支持访问嵌套的 JSON 字段,使用点号(.)表示路径:
{
"id": 1,
"payload": {
"product": {
"details": {
"color": "red",
"size": "L"
},
"prices": [
{"currency": "USD", "amount": 99.99},
{"currency": "CNY", "amount": 699.00}
]
}
}
}
# 访问嵌套字段
models.FieldCondition(
key="product.details.color",
match=models.MatchValue(value="red"),
)
# 访问数组元素中的字段(嵌套对象数组)
models.FieldCondition(
key="product.prices[].currency",
match=models.MatchValue(value="USD"),
)
Payload 索引
没有索引的 Payload 字段仍然可以过滤,但需要全量扫描所有 Segment。创建索引后,过滤操作从 O(n) 降为 O(log n) 或 O(1)。
索引类型详解
Keyword 索引(精确匹配):
client.create_payload_index(
collection_name="my_products",
field_name="category",
field_schema=models.PayloadSchemaType.KEYWORD,
)
底层使用 HashMap,支持:
MatchValue:精确匹配单值MatchAny:匹配多值中的任一个MatchExcept:排除匹配
Integer 索引(范围查询 + 精确匹配):
from qdrant_client.models import IntegerIndexParams, IntegerIndexType
client.create_payload_index(
collection_name="my_products",
field_name="price_cents",
field_schema=models.PayloadFieldSchema(
type=models.PayloadSchemaType.INTEGER,
params=IntegerIndexParams(
type=IntegerIndexType.INTEGER,
lookup=True, # 启用精确匹配(MatchValue)
range=True, # 启用范围查询(Range)
),
),
)
Float 索引(范围查询):
client.create_payload_index(
collection_name="my_products",
field_name="rating",
field_schema=models.PayloadSchemaType.FLOAT,
)
Bool 索引(极致高效):
client.create_payload_index(
collection_name="my_products",
field_name="is_available",
field_schema=models.PayloadSchemaType.BOOL,
)
Bool 索引内部使用 Bitset,过滤操作通过位运算实现,是最快的索引类型。
Datetime 索引(时间范围):
client.create_payload_index(
collection_name="events",
field_name="created_at",
field_schema=models.PayloadSchemaType.DATETIME,
)
UUID 索引(精确 UUID 匹配):
client.create_payload_index(
collection_name="my_products",
field_name="user_id",
field_schema=models.PayloadSchemaType.UUID,
)
Full-text 全文索引
全文索引是 Text 类型字段的专用索引,支持词语级别的搜索:
from qdrant_client.models import TextIndexParams, TokenizerType
client.create_payload_index(
collection_name="documents",
field_name="content",
field_schema=models.PayloadFieldSchema(
type=models.PayloadSchemaType.TEXT,
params=TextIndexParams(
type="text",
tokenizer=TokenizerType.WORD, # 分词方式
min_token_len=2, # 最短 token 长度
max_token_len=20, # 最长 token 长度
lowercase=True, # 转小写(大小写不敏感)
),
),
)
分词器类型:
| Tokenizer | 说明 | 适用语言 |
|---|---|---|
word |
按空白字符和标点分词 | 英语、欧洲语言 |
whitespace |
仅按空白字符分词(不处理标点) | 英语(精确控制) |
prefix |
生成所有前缀 token(支持前缀搜索) | 自动补全场景 |
multilingual |
多语言 Unicode 感知分词 | 中文、日文等非空格分隔语言 |
warning 中文全文搜索的注意事项
中文文本由于词语之间没有空格,word 分词器对中文效果很差——它会把整句话当作一个 token。
对中文场景,应该使用 multilingual 分词器,或在写入前进行预处理(如使用 jieba 分词后存储分词结果)。
全文搜索的使用:
# 全文搜索过滤
models.FieldCondition(
key="content",
match=models.MatchText(text="vector database"),
)
Geo 地理索引
client.create_payload_index(
collection_name="restaurants",
field_name="location",
field_schema=models.PayloadSchemaType.GEO,
)
Geo 索引的底层是 R-Tree(矩形树),支持:
- 圆形查询:以某点为圆心,半径 r 内的所有点
- 矩形查询:经纬度矩形框内的所有点
- 多边形查询:任意多边形区域内的所有点(v1.7+)
索引管理 API
查看 Collection 的所有索引:
GET /collections/my_products
# 在返回的 payload_schema 字段中查看已创建的索引
删除索引:
client.delete_payload_index(
collection_name="my_products",
field_name="category",
)
索引创建的时机建议:
建议在数据写入之前创建索引,而非之后。
如果在数据写入后创建索引,Qdrant 会进行一次全量扫描来构建索引,
对于大规模数据集这会消耗较长时间和资源。
最佳实践:
1. 创建 Collection
2. 创建 Payload 索引
3. 批量写入数据(数据写入时索引同步维护)
Filter 过滤器
Filter 是 Qdrant 实现向量搜索与结构化查询融合的关键机制,支持灵活的逻辑组合和丰富的条件类型。
逻辑组合:must / should / must_not
Qdrant 的 Filter 采用布尔逻辑树结构:
must → AND(所有条件都必须满足)
should → OR(至少一个条件满足)
must_not → NOT(所有条件都不能满足)
完整示例:
models.Filter(
must=[
# 必须:category = "electronics"
models.FieldCondition(
key="category",
match=models.MatchValue(value="electronics"),
),
# 必须:is_available = true
models.FieldCondition(
key="is_available",
match=models.MatchValue(value=True),
),
],
should=[
# 应该满足之一:brand = "Apple" 或 brand = "Samsung"
models.FieldCondition(
key="brand",
match=models.MatchValue(value="Apple"),
),
models.FieldCondition(
key="brand",
match=models.MatchValue(value="Samsung"),
),
],
must_not=[
# 不能:status = "discontinued"
models.FieldCondition(
key="status",
match=models.MatchValue(value="discontinued"),
),
],
)
等价的 SQL 语义:
WHERE category = 'electronics'
AND is_available = true
AND (brand = 'Apple' OR brand = 'Samsung')
AND status != 'discontinued'
嵌套逻辑:
# 复杂的嵌套条件
models.Filter(
must=[
models.Filter(
should=[
models.FieldCondition(key="tier", match=models.MatchValue(value="premium")),
models.Filter(
must=[
models.FieldCondition(key="rating", range=models.Range(gte=4.5)),
models.FieldCondition(key="review_count", range=models.Range(gte=100)),
]
),
]
)
]
)
Match 精确匹配条件
MatchValue(单值精确匹配):
# 字符串匹配
models.FieldCondition(key="status", match=models.MatchValue(value="active"))
# 整数匹配
models.FieldCondition(key="year", match=models.MatchValue(value=2024))
# 布尔匹配
models.FieldCondition(key="is_featured", match=models.MatchValue(value=True))
MatchAny(多值匹配,IN 操作):
# 等价于 SQL: WHERE category IN ('phones', 'tablets', 'laptops')
models.FieldCondition(
key="category",
match=models.MatchAny(any=["phones", "tablets", "laptops"]),
)
MatchExcept(排除匹配,NOT IN 操作):
# 等价于 SQL: WHERE status NOT IN ('deleted', 'archived')
models.FieldCondition(
key="status",
match=models.MatchExcept(**{"except": ["deleted", "archived"]}),
)
MatchText(全文搜索匹配):
# 在已创建全文索引的字段上搜索
models.FieldCondition(
key="description",
match=models.MatchText(text="machine learning"),
)
Range 范围条件
Range 条件适用于数值(Integer/Float)和时间(Datetime)字段:
from qdrant_client.models import Range, DatetimeRange
# 价格范围:100 <= price <= 500
models.FieldCondition(
key="price",
range=Range(gte=100.0, lte=500.0),
)
# 半开区间:rating > 4.0
models.FieldCondition(
key="rating",
range=Range(gt=4.0),
)
# 时间范围查询(Datetime 字段专用)
models.FieldCondition(
key="created_at",
range=DatetimeRange(
gte="2024-01-01T00:00:00Z",
lt="2024-12-31T23:59:59Z",
),
)
Range 参数:
| 参数 | 含义 | 示例 |
|---|---|---|
gt |
大于(不含) | gt=100 表示 > 100 |
gte |
大于等于(含) | gte=100 表示 >= 100 |
lt |
小于(不含) | lt=500 表示 < 500 |
lte |
小于等于(含) | lte=500 表示 <= 500 |
Geo 地理条件
GeoRadius(圆形范围):
from qdrant_client.models import GeoPoint, GeoRadius
# 北京天安门 5km 范围内的餐厅
models.FieldCondition(
key="location",
geo_radius=GeoRadius(
center=GeoPoint(lon=116.397428, lat=39.90923),
radius=5000.0, # 单位:米
),
)
GeoBoundingBox(矩形范围):
from qdrant_client.models import GeoBoundingBox
# 北京市的大致经纬度矩形
models.FieldCondition(
key="location",
geo_bounding_box=GeoBoundingBox(
top_left=GeoPoint(lon=115.7, lat=41.1),
bottom_right=GeoPoint(lon=117.4, lat=39.4),
),
)
GeoPolygon(多边形范围,精确行政区划):
from qdrant_client.models import GeoPolygon, GeoLineString
# 使用多边形精确框定不规则区域
models.FieldCondition(
key="location",
geo_polygon=GeoPolygon(
exterior=GeoLineString(
points=[
GeoPoint(lon=116.0, lat=39.5),
GeoPoint(lon=117.0, lat=39.5),
GeoPoint(lon=117.0, lat=40.5),
GeoPoint(lon=116.0, lat=40.5),
GeoPoint(lon=116.0, lat=39.5), # 闭合多边形
]
),
interiors=[], # 可选:多边形中的"空洞"
),
)
IsNull / IsEmpty 空值条件
# 匹配字段值为 null 的点;缺失字段使用 IsEmpty 条件
models.IsNullCondition(
is_null=models.PayloadField(key="discount_rate")
)
# 匹配字段不存在、值为 null 或为空数组的点;空字符串不属于此处的空值条件
models.IsEmptyCondition(
is_empty=models.PayloadField(key="tags")
)
使用场景:
IsNull:
- 找出字段显式为null的点(数据补全场景)
- 过滤分类字段显式为null的内容;缺失字段使用IsEmpty条件
IsEmpty:
- 找出标签列表为空的产品
- 审计无内容的记录
Nested 嵌套对象条件
对于数组中的嵌套对象,使用 Nested 过滤器确保条件在同一对象内成立:
# 假设 Payload 结构:
# { "prices": [{"currency": "USD", "amount": 99}, {"currency": "CNY", "amount": 699}] }
# 错误方式(不用 Nested):
# 以下条件可能匹配到:currency="USD" 来自第一个元素,amount<100 来自第一个元素
# 但如果 USD 价格是 200,CNY 价格是 99,下面的条件仍然会错误匹配
models.Filter(
must=[
models.FieldCondition(key="prices[].currency", match=models.MatchValue(value="USD")),
models.FieldCondition(key="prices[].amount", range=Range(lte=100)),
]
)
# 正确方式(使用 Nested):
# 确保 currency="USD" 和 amount<100 来自同一个数组元素
models.Filter(
must=[
models.NestedCondition(
nested=models.Nested(
key="prices",
filter=models.Filter(
must=[
models.FieldCondition(key="currency", match=models.MatchValue(value="USD")),
models.FieldCondition(key="amount", range=Range(lte=100)),
]
),
)
)
]
)
HasId 点 ID 条件
有时需要将向量搜索结果限定在特定 Point ID 的集合内:
from qdrant_client.models import HasIdCondition
# 只在指定 ID 的点中搜索
models.Filter(
must=[
HasIdCondition(has_id=[1, 5, 23, 456, 789])
]
)
使用场景:
- 对已知候选集进行重排序(如从 Redis 获取候选 ID 后再向量排序)
- A/B 实验中只在测试组用户的 ID 范围内搜索
- 增量索引:只搜索新增的点
Filter 性能优化策略
策略1:只对高频过滤字段创建索引
原则:不是所有 Payload 字段都需要索引
创建索引的字段:查询时 80% 以上的请求都会用到的过滤字段
不建议索引的字段:偶尔使用的字段、基数极高的字段(如 UUID 字段)
索引的内存成本:
每个索引都需要额外的内存
Keyword 索引:约 50-200 bytes/点(取决于字段基数)
Integer/Float 索引:约 50-100 bytes/点
策略2:利用选择率优化 Filter 顺序
Qdrant 会自动估算各个条件的选择率,选择最优的执行顺序
但可以通过选择合适的 Filter 组合帮助优化器做出更好的决策:
高选择率(少量结果)的条件放在 must 中:
should 条件的选择率是各子条件选择率的并集,总体更高
must 条件的选择率是各子条件的交集,可以大幅减少候选集
策略3:避免全量 Payload 扫描
在没有 Payload 索引的字段上过滤时,Qdrant 需要:
1. 遍历所有候选点
2. 读取每个点的 Payload
3. 执行条件匹配
这是 O(n) 操作,在大集合上极慢。
解决方案:为常用过滤字段创建对应类型的 Payload 索引。
Snapshot 快照
Snapshot 是 Qdrant 提供的数据备份与迁移机制,支持 Collection 级别和整个存储的快照。
快照的适用场景
| 场景 | 说明 |
|---|---|
| 灾难恢复 | 从快照恢复误删除的 Collection 或损坏的数据 |
| 数据迁移 | 从一个 Qdrant 实例迁移到另一个实例(不同版本/不同机器) |
| 环境复制 | 将生产环境数据同步到测试环境 |
| 版本升级 | 在 Qdrant 大版本升级前备份数据 |
| 数据分析 | 对某个时间点的快照进行离线分析 |
快照操作 API
创建 Collection 快照:
# 创建快照(异步操作,返回快照信息)
snapshot_info = client.create_snapshot(
collection_name="my_products",
)
print(f"快照名称: {snapshot_info.name}")
print(f"快照大小: {snapshot_info.size} bytes")
print(f"创建时间: {snapshot_info.creation_time}")
# REST API
POST /collections/my_products/snapshots
列出所有快照:
snapshots = client.list_snapshots(collection_name="my_products")
for snap in snapshots:
print(f"{snap.name}: {snap.size} bytes, created at {snap.creation_time}")
下载快照(到本地文件):
# 直接下载到本地
GET /collections/my_products/snapshots/{snapshot_name}
# 返回 .snapshot 文件(二进制格式)
从快照恢复 Collection:
# 方式1:从Qdrant服务端可访问的文件恢复,并非直接读取客户端本地文件
client.recover_snapshot(
collection_name="my_products_restored",
location="file:///path/to/snapshot.snapshot",
)
# 方式2:从 URL 恢复(适合分布式场景)
client.recover_snapshot(
collection_name="my_products_restored",
location="http://other-qdrant-host:6333/collections/my_products/snapshots/snapshot_name",
)
删除快照:
client.delete_snapshot(
collection_name="my_products",
snapshot_name="my_products-12345678-0.snapshot",
)
整个存储的全量快照:
# 对整个 Qdrant 实例的所有数据创建快照
POST /snapshots
# 列出全量快照
GET /snapshots
# 恢复全量快照(需要 Qdrant 实例重启)
PUT /snapshots/recover
分布式快照策略
在多节点集群中,快照操作有额外的考虑:
%%{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
subgraph DIST["分布式快照策略"]
direction TB
NODE1["节点 1<br/>包含 Shard 0 副本"] -->|"创建快照"| SNAP1["Shard 0 快照"]
NODE2["节点 2<br/>包含 Shard 1 副本"] -->|"创建快照"| SNAP2["Shard 1 快照"]
NODE3["节点 3<br/>包含 Shard 0+1 副本"] -->|"可选备用"| SNAP3["备用快照"]
end
SNAP1 --> STORAGE["集中存储<br/>(对象存储 / NFS)"]
SNAP2 --> STORAGE
class DIST,NODE1,SNAP1,NODE2,SNAP2,NODE3,SNAP3,STORAGE default
分布式场景的快照注意事项:
-
覆盖实际分片与节点:集群模式下,节点的Collection快照覆盖该节点持有的数据,不能将一次请求视为所有节点的完整备份;备份任务需确保每个实际分片都有可恢复的快照,并记录分片与节点对应关系
-
一致性保证:Qdrant 不保证跨分片的精确时间点一致性(即没有全局快照事务),但在大多数应用场景中这是可接受的
-
快照不包含 Alias:Collection 的别名需要单独记录和恢复
-
推荐恢复方式:
# 集群环境优先使用 REST API 恢复(而非直接文件操作)
# 此请求不能单独保证整个集群恢复完成;仍需处理分片覆盖、恢复优先级及目标拓扑
PUT /collections/my_products/snapshots/recover
{
"location": "http://storage-server/backups/my_products.snapshot"
}
Alias 别名
别名的核心价值
Alias(别名)是指向 Collection 的逻辑名称,可以随时原子地将别名从一个 Collection 切换到另一个 Collection。
这看似简单,却解决了向量数据库生产运营中一个常见的棘手问题:如何在不停机的情况下更新向量索引?
典型场景:
问题:embedding 模型从 v1 升级到 v2,所有向量需要重新生成
挑战:重建向量索引需要数小时,期间不能中断线上服务
朴素解决方案(有停机):
1. 停止服务
2. 删除旧集合,创建新集合
3. 重新生成所有向量并写入
4. 重启服务
Alias 解决方案(在查询模型同步切换、增量数据追平且容量充足等条件满足时,可支持零停机):
1. 创建新集合 products_v2(后台写入,不影响现有服务)
2. 服务继续使用别名 products → products_v1
3. 数据写入完成后,原子切换:products → products_v2
4. 删除旧集合 products_v1(可选)
零停机版本切换
Step 1:创建并填充新 Collection(后台)
# 用新模型生成向量,写入新 Collection
client.create_collection(
collection_name="products_v2",
vectors_config=VectorParams(size=3072, distance=Distance.COSINE),
)
# 批量写入新的向量(这一步可能需要几小时)
for batch in get_all_products_batches():
new_vectors = new_embedding_model.encode(batch)
client.upsert(
collection_name="products_v2",
points=[
PointStruct(id=p["id"], vector=v, payload=p["payload"])
for p, v in zip(batch, new_vectors)
]
)
Step 2:创建初始别名(如果不存在)
# 首次部署时,创建别名指向 v1
client.update_aliases(
change_aliases_operations=[
models.CreateAliasOperation(
create_alias=models.CreateAlias(
collection_name="products_v1",
alias_name="products",
)
)
]
)
Step 3:原子切换别名(零停机)
# 在单个原子操作中:删除旧别名 + 创建新别名
# 不存在"别名不指向任何 Collection"的中间状态
client.update_aliases(
change_aliases_operations=[
models.DeleteAliasOperation(
delete_alias=models.DeleteAlias(alias_name="products")
),
models.CreateAliasOperation(
create_alias=models.CreateAlias(
collection_name="products_v2",
alias_name="products",
)
),
]
)
# 别名切换改变集合名称路由;查询模型须同步切换,且不能据此保证所有副本立即可见
Step 4:清理旧 Collection(可选)
# 确认新集合运行稳定后,删除旧集合
# 建议保留 24-48 小时作为回滚窗口
client.delete_collection("products_v1")
回滚操作(如果新版本有问题):
# 快速回滚:将别名切回旧 Collection
client.update_aliases(
change_aliases_operations=[
models.DeleteAliasOperation(
delete_alias=models.DeleteAlias(alias_name="products")
),
models.CreateAliasOperation(
create_alias=models.CreateAlias(
collection_name="products_v1", # 切回旧版本
alias_name="products",
)
),
]
)
别名操作 API
列出所有别名:
aliases = client.get_aliases()
for alias in aliases.aliases:
print(f"{alias.alias_name} → {alias.collection_name}")
查询某 Collection 的所有别名:
collection_aliases = client.get_collection_aliases(
collection_name="products_v2"
)
删除别名:
client.update_aliases(
change_aliases_operations=[
models.DeleteAliasOperation(
delete_alias=models.DeleteAlias(alias_name="products")
)
]
)
别名与 Collection 同名的情况:
别名名称不能与已有 Collection 名称冲突;应使用独立的业务逻辑名作为别名。
建议:
使用有意义的命名区分 Collection 和 Alias
Collection:products_v1, products_v2(带版本号)
Alias:products(业务逻辑名,无版本号)
数据模型设计最佳实践
向量维度与模型匹配
原则:Collection 的 vector.size 必须严格匹配 embedding 模型的输出维度
常见模型维度参考:
OpenAI text-embedding-3-small: 1536(可缩减至 512)
OpenAI text-embedding-3-large: 3072(可缩减至 256)
BAAI/bge-large-zh-v1.5: 1024
BAAI/bge-m3 (dense): 1024
Cohere embed-v3: 1024
Sentence-BERT (all-MiniLM-L6-v2): 384
错误示例:
模型输出 1536 维,但 Collection 配置了 768 维
→ upsert 时报错
Payload 设计原则
原则1:只存储需要过滤或展示的字段
向量已经编码了语义信息,不需要在 Payload 中重复存储大量文本
建议:存储 ID、关键元数据、显示用字段;避免存储大型 raw 文本
原则2:预处理高基数字段
城市名:保持字符串(基数数百,适合 Keyword 索引)
用户 ID:考虑整数化(提升索引效率)
时间:使用 ISO 8601 格式(支持 Datetime 索引的范围查询)
原则3:扁平化嵌套结构(如果嵌套层次过深)
深层嵌套的访问路径更难维护
如果嵌套对象不需要作为整体进行 Nested 过滤,考虑扁平化存储
集合规模与分片规划
单 Collection 的规模上限(经验值):
内存充足时:无硬性上限,已有亿级点的案例
实践建议:单 Segment 不超过 500万点
分片规划:
单节点场景:1-2 个分片
3 节点集群:6 个分片(每节点 2 个,冗余度 replication_factor=2)
10 节点集群:20 个分片(每节点 2 个,replication_factor=2)
关键原则:分片数应是节点数的整数倍,保证均匀分布
内存预算计算
单个 Collection 的内存需求估算:
向量内存(全内存模式):
N 个点 × D 维 × 4 bytes(float32)
例:100万点 × 1536维 × 4 = 6.1 GB
HNSW 图内存:
N 个点 × m × 2 × 8 bytes(约)
例:100万点 × 16 × 2 × 8 = 256 MB
Payload 内存(on_disk_payload=false):
N 个点 × 平均 Payload 大小
例:100万点 × 2 KB = 2 GB
Payload 索引内存:
每个索引约 N × 50-200 bytes
例:5 个索引,100万点 × 100 bytes = 500 MB
总计(粗略估算):6.1 + 0.26 + 2 + 0.5 ≈ 9 GB
建议:预留 1.5-2 倍的估算内存作为安全余量
查询模式优化
高频查询模式1:语义搜索 + 状态过滤
→ 为 status 字段创建 Keyword 索引
→ 使用 must 条件(高选择率场景用 Pre-filtering)
高频查询模式2:地理位置 + 类别过滤
→ 为 location 创建 Geo 索引
→ 为 category 创建 Keyword 索引
→ 通常地理过滤选择率更低,配合 Post-filtering
高频查询模式3:时间范围 + 语义搜索
→ 为 created_at 创建 Datetime 索引
→ 时间范围过滤通常选择率较高,用 Pre-filtering
混合搜索(Hybrid Search)模式:
→ 配置 Dense + Sparse 命名向量
→ 使用 RRF(Reciprocal Rank Fusion)合并结果
→ 适合电商搜索、文档检索等需要关键词+语义的场景
summary 核心概念速查卡
| 概念 | 一句话总结 | 关键配置/参数 |
|---|---|---|
| Collection | 向量数据的顶级容器,类似数据库的表 | size, distance, hnsw_config |
| Point | 基本数据单元 = ID + Vector(s) + Payload | id(uint64 或 UUID) |
| Dense Vector | 所有维度有值的传统 embedding | size, distance, on_disk |
| Sparse Vector | 只存非零维度,适合关键词权重 | indices + values,必须命名 |
| MultiVector | 一个 Point 多个向量,用于 ColBERT | comparator: max_sim |
| Payload | 动态 JSON 元数据,用于过滤和展示 | on_disk_payload |
| Payload 索引 | 加速结构化过滤的辅助索引 | Keyword/Integer/Float/Geo/Text |
| Filter | 向量搜索时的结构化条件 | must/should/must_not |
| Snapshot | 数据备份的二进制快照 | 定期创建,存储到外部 |
| Alias | Collection 的逻辑别名 | 原子切换名称映射;配合模型和数据同步可支持零停机升级 |
延伸阅读
- Qdrant 学习路线图 - 系统学习路径
- Qdrant 架构深度解析 - 存储引擎与分布式架构
- ANN 算法原理 - HNSW 数学基础与向量搜索原理
- Milvus 架构对比 - 两种主流向量数据库的设计哲学
#Qdrant #向量数据库 #Collection #Point #Vector #Payload #Filter #Snapshot #Alias #技术与工具