深入理解 Qdrant 向量数据库:从 Filterable HNSW 到混合搜索,一篇讲透
读完这篇,你将彻底搞懂:Qdrant 凭什么用 Rust 单枪匹马扛住亿级向量搜索?"Filterable HNSW" 到底和普通 HNSW 差在哪?混合搜索里的 RRF 融合又是怎么一回事?
一、开场:先讲一个"电梯演讲"
想象一下这样的场景:
你的 RAG 应用上线了,老板很满意。直到某天,数据量从 10 万涨到 1000 万,噩梦开始了------
- 用 FAISS:只有索引没有存储,没有元数据过滤,写"带条件的搜索"得自己造轮子,轮子还漏油;
- 用 Elasticsearch:向量搜索比传统数据库慢 10-50 倍,QPS 上不去,老板看着监控曲线直摇头;
- 用 Pinecone:性能确实好,但月费蹭蹭涨,而且数据进了"黑盒",想迁移都搬不走(供应商锁定)。
传统的向量数据库,似乎总要在速度、功能、可靠性里三选二------要么快但不持久,要么功能全但慢,要么啥都好但贵且锁。
直到你遇到了 Qdrant:
python
from qdrant_client import QdrantClient, models
client = QdrantClient("localhost", port=6333) # 装个 Docker 就能跑,Rust 单二进制
client.create_collection(
collection_name="demo",
vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE),
)
# 向量 + 元数据(Payload)一起存,一个数据库搞定
client.upsert(collection_name="demo",
points=[models.PointStruct(id=1, vector=[0.1, -0.3, 0.5, ...],
payload={"category": "编程", "level": "入门"})])
# 带过滤条件的向量搜索:一次图遍历,边走边筛(这就是 Filterable HNSW)
results = client.search(
collection_name="demo",
query_vector=[0.1, -0.2, 0.4, ...],
query_filter=models.Filter(must=[models.FieldCondition(
key="category", match=models.MatchValue(value="编程"))]),
limit=10,
)
几行代码,语义搜索 + 元数据过滤 + 持久化,全齐了。
一句话定位:Qdrant(发音 "quadrant",意为"象限")是一个纯 Rust 编写的高性能开源向量搜索引擎。 它不是"给现有数据库加个向量功能",而是 2021 年从零开始为向量搜索量身定制的数据库------把过滤、持久化、分布式、量化全做进了引擎里(Apache 2.0 协议,免费商用)。
二、先看懂大框架:一座四层大楼
庖丁解牛的故事大家都听过------庖丁为什么游刃有余?因为他看透了牛的结构。理解 Qdrant 也一样,先看它的"骨架":
┌──────────────────────────────────────────────────────────┐
│ 客户端层 (Client Layer) │
│ Python · JS/TS · Rust · Go · .NET · Java · HTTP/gRPC │
├──────────────────────────────────────────────────────────┤
│ API 层 (API Layer) │
│ REST API (HTTP) + gRPC API │
├──────────────────────────────────────────────────────────┤
│ 集合管理层 (Collection Manager) │
│ 数据路由 · 分片管理 · 复制控制 · 查询分发 │
├───────────────┬──────────────────────────────────────────┤
│ ┌────────────┴─────────────┐ │
│ │ Segment 引擎 │ │
│ │ · Segment 管理器 │ │
│ │ · WAL(写前日志) │ │
│ │ · 存储管理 │ │
│ ├──────────────────────────┤ │
│ │ 索引层 (Index Layer) │ │
│ │ · Filterable HNSW(稠密) │ │
│ │ · 倒排索引(稀疏向量) │ │
│ │ · Payload Index(元数据) │ │
│ │ · 量化器 (SQ/PQ/Binary) │ │
│ └──────────────────────────┘ │
├──────────────────────────────────────────────────────────┤
│ 存储层 (Storage Layer) │
│ 内存(向量热存)· 磁盘(MMAP)· RocksDB(元数据) │
└──────────────────────────────────────────────────────────┘
记住这张图,整篇文章就是给这四层"讲故事":
- 客户端层:你用哪种语言、哪个协议访问它;
- API 层:REST + gRPC 双协议,把请求接进来;
- 集合管理层:你的请求该路由到哪个 Shard、哪个副本;
- Segment 引擎 + 索引层 + 存储层:数据到底怎么存、怎么索引、怎么搜------这是本文的主角。
2.1 数据组织层级:Collection → Shard → Segment
Qdrant 的数据组织,很像"国家 → 省 → 市 → 人"的行政区划:
Collection (集合) ← 逻辑上的"一张表",配置维度、距离度量、索引参数
│
├── Shard 1 ──── Replica 1 (Leader) ← 水平分片,可分布在不同节点
│ ├── Segment 1 [Points 1..100K] ← 物理数据片段,独立索引
│ ├── Segment 2 [Points 100K..200K]
│ └── WAL [未归档操作]
│
├── Shard 1 ──── Replica 2 (Follower) ← 副本,用 Raft 保持一致性
│ └── Segment ...(同一数据副本)
│
└── Shard 2 ──── Replica 1
└── ...
| 层级 | 是什么 | 类比 |
|---|---|---|
| Collection | 向量和 Payload 的逻辑存储单元,配置维度、距离、索引参数 | 传统数据库的"表" |
| Shard | 集合的水平分区,分布在不同节点上(可配 shard_number) |
分库分表 |
| Replica | 每个 Shard 的副本,Raft 保证一致性(可配 replication_factor) |
数据备份 |
| Segment | Shard 内的物理数据片段,独立索引和 WAL | 一张表底下的数据文件 |
| Point | 单个向量 + 其 Payload | 表中的一行 |
一句话人话:Collection 是逻辑上的"一张表",Shard 是"分库",Segment 是"分库里的数据文件",Point 是"一行"。
2.2 分布式一致性:Raft 共识
多副本数据不一致怎么办?Qdrant 用的是分布式系统界的"老熟人"------Raft 共识协议:
Shard 0:
┌──────────────────────┐
│ Replica 0 (Leader) │ ← 所有写入都经过它,它说了算
├──────────────────────┤
│ Replica 1 (Follower) │ ← 从 Leader 同步日志
├──────────────────────┤
│ Replica 2 (Follower) │ ← 从 Leader 同步日志
└──────────────────────┘
Raft Log → 保证所有副本数据一致
Raft 干三件事:Leader 选举 (Leader 挂了自动换人)、日志复制 (写操作同步到所有副本)、安全保证 (已确认的写入不会丢)。翻译成人话:"写数据先开会表决,多数派点头才算数。"
三、核心原理一:存储设计------向量和元数据怎么"同住一屋"
很多向量数据库是"向量归向量存,元数据归另一个库管"。Qdrant 的做法是:向量和 Payload(JSON 元数据)原生共存,一个数据库全搞定,不用额外接 MySQL/PostgreSQL。
3.1 Segment:物理存储的最小单元
每个 Segment 是一个独立的小世界:
Segment = 向量数据 + HNSW 索引 + Payload 存储 + WAL
这带来一个经典的权衡:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 少量大 Segment | 查询吞吐高(更大的 HNSW 图 → 更少跨段遍历) | 构建慢、写入慢 |
| 多量小 Segment | 索引快、写入快 | 查询要扫描更多 Segment |
类比:大 Segment 像"大卖场"------东西全,逛一次就买齐,但进货慢;小 Segment 像"便利店"------随时上新货,但买齐东西得跑好几家店。
3.2 WAL:先记账,再干活
写入流程中最关键的一步:
Client → upsert(points)
│
▼
API Layer(验证请求)
│
▼
Collection Manager(路由到目标 Shard)
│
▼
Segment Engine
│ 1. WAL(写前日志): 先把操作写进日志 → 持久化保证
│ 2. 内存中更新 Segment
│ 3. HNSW 图更新(异步增量)
│ 4. Payload 索引更新
│
▼
写入确认 → Client
这和 Redis 的 AOF 一个套路:先记账(写 WAL),再干活(改索引),确认持久化后才告诉客户端"成功" 。如果进程崩溃,重启时从 WAL 重放没干完的活。写入是内存操作(快),WAL 是磁盘操作(持久)------快和稳,两头都占。
3.3 存储模式:内存 vs 磁盘的灵活切换
Qdrant 用 MMAP(内存映射)+ 缓存 实现灵活的内存/磁盘平衡,元数据放 RocksDB:
| 配置 | 说明 | 适用 |
|---|---|---|
| 全内存 | 向量和 HNSW 图都在 RAM | 最佳性能、低延迟场景 |
| 向量磁盘 + 图内存 | 向量存磁盘,HNSW 图在内存 | 内存受限 |
| 全磁盘 | 向量和 HNSW 图都在 SSD(需 NVMe) | 极致内存节省 |
磁盘模式下,频繁访问的向量自动缓存在内存,冷数据按需从磁盘读取------就像你家冰箱:常喝的牛奶放冷藏室(内存缓存),囤的年货放地下室(磁盘),要用再去拿。
金句:"内存不够,磁盘来凑;热数据留内存,冷数据住磁盘。" 这也是 Qdrant 敢在单机上扛 1 亿+ 向量的底气之一。
另外,官方还提供快照(Snapshot)备份能力用于容灾与迁移,配合多副本策略,生产环境的可靠性就稳了。
四、核心原理二:Filterable HNSW------全场 MVP
这是整篇文章的重头戏,也是 Qdrant 最具标志性的创新,面试官最爱问。
4.1 先复习 HNSW:来自"跳表"的灵感
100 万条 768 维向量暴力搜索,一次要算 7.68 亿次浮点运算,延迟线性飙升。更致命的是维度灾难:高维空间里,B-Tree、KD-Tree 这些传统索引全部失效。
所以业界转向 ANN(近似最近邻) :牺牲一点精度,换 10 倍甚至 100 倍的快。Qdrant 用的是 HNSW(Hierarchical Navigable Small World,分层可导航小世界),复杂度 O(log N)。
HNSW 的思路和跳表(Skip List)如出一辙------把一维的跳表推广到高维空间,变成"多层图":
Layer 2 (最稀疏): ● ─────────────────────── ● ← "高速公路",大跨步导航
↓
Layer 1: ● ───── ● ───── ● ───── ● ← 中等跨度
↓
Layer 0 (最密集): ●──●──●──●──●──●──●──●──●──● ← 精确搜索层,包含所有节点
关键规则:
- 第 0 层包含所有节点,连接局部最近的邻居;
- 上层是下层的随机子集,节点数指数递减;
- 上层节点连接更远的邻居,形成"高速公路";
- 同一节点在不同层之间垂直连接,方便"坐电梯下楼"。
搜索就是贪心 + 逐层下钻:从顶层随机入口开始 → 不断移动到离 query 最近的邻居 → 本地找不到更近的就降层 → 直到第 0 层 → 返回距离最小的 K 个邻居。
类比:像你在上海找一家藏在弄堂里的小吃店------先坐地铁大站快车到市中心(顶层,一步千里),再换乘公交到街区(中层),最后步行挨家挨户看门牌(第 0 层,精确到户)。"地铁---公交---步行",就是 HNSW 的三层导航。
4.2 标准 HNSW 的致命伤:后置过滤会"漏人"
传统 HNSW 处理带条件的搜索,一般是先搜索、后过滤:
标准 HNSW 查询流程:
1. HNSW 图搜索 → 获得 topK 候选
2. 后置过滤 → 只保留符合条件的
问题: 符合条件的候选可能不足 K 个!
举个例子:你想搜"1 万篇文档里,最近邻的 10 篇 Python 教程"。HNSW 先给你 topK=10 个最近邻,结果这 10 篇全是 Java------过滤后 0 篇符合条件。要凑够 10 篇?那你得先搜 topK=1000,再做后置过滤,白白浪费 100 倍算力。
这就是"过滤漏斗效应":条件越苛刻,后置过滤越容易漏,召回率崩得越厉害。
4.3 Filterable HNSW:把过滤做进图遍历里
Qdrant 的解法很聪明------把过滤条件从"搜完之后"挪到"搜索过程中":
Filterable HNSW 查询流程:
1. 在 HNSW 图遍历中实时检查过滤条件
2. 跳过不符合条件的节点
3. 继续搜索,直到找到 K 个符合条件的最近邻居
优势: 保证返回 K 个结果,同时保持 HNSW 的效率
图遍历细节:
当前节点 → 检查 Payload 过滤条件
├─ 通过 → 加入候选列表
└─ 不通过 → 跳过,但继续沿图遍历
不会因为一个节点不满足条件而终止搜索!
这就是 Qdrant 官网说的 "同程过滤"(Filtering at the same time):
Payload Index = 扩展了 HNSW 图结构的辅助索引
工作原理:
· 在 HNSW 图构建时,将 Payload 过滤条件编码到图结构中
· 搜索时:一次图遍历即可同时完成向量搜索和过滤
· 不是前置过滤,也不是后置过滤 → 是"同程过滤"
类比:传统做法像"先面完所有候选人,再筛 985 学历"------费劲还可能一个都不符合;Filterable HNSW 像面试官一边看简历一边筛,不合条件的简历直接跳过,面试流程永不中断,直到招满为止。
一句话人话:别人家是"先搜再筛",筛完可能凑不齐人;Qdrant 是"边走边筛",保证一次遍历就把 K 个符合条件的邻居全找齐。
与普通 HNSW 实现的区别(面试金句):
| 维度 | 普通 HNSW(如 FAISS/部分实现) | Filterable HNSW(Qdrant) |
|---|---|---|
| 过滤时机 | 搜索后置过滤,可能不足 K 个 | 图遍历中同程过滤,保证 K 个 |
| 过滤方式 | 依赖外部工具(如 SQL 子查询) | Payload Index 编码进图结构 |
| 复杂过滤 | 多条件需多次 IO / 精度下降 | 一次遍历内完成,可启用 ACORN |
4.4 三大必懂参数(调优的灵魂)
| 参数 | 默认值 | 管什么 | 调大 | 调小 |
|---|---|---|---|---|
m |
16 | 图连通性(每节点最大邻居数) | 图更密、精度高、内存大涨 | 省内存、精度降 |
ef_construct |
--- | 索引构建时的探索范围 | 建索引慢、图质量高 | 建得快、精度降 |
ef |
自动 | 查询时的探索范围 | 搜索慢、召回率高 | 搜索快、可能漏真邻居 |
三个参数的生命周期完全不同:
m和ef_construct:创建 Collection 时设置,之后不可更改------它们是"盖楼图纸";ef:查询时动态设置,可随请求调整------它是"找房耐心"。
python
# 查询时动态指定 ef(高精度场景调大)
results = client.search(
collection_name="docs",
query_vector=vector,
search_params=models.SearchParams(ef=200),
limit=10,
)
一句话记忆:
ef_construct管"盖楼质量",ef管"找房耐心",m管"楼里邻居密度"。 精度和速度,永远是鱼和熊掌。
4.5 Payload Index 与 ACORN
Payload Index 是把过滤做进 HNSW 的关键,也是踩坑高发区:
- 最佳实践:在建索引之前创建 Payload Index(效率最高);
- 如果已有大量数据后 再创建:Payload Index 不会立即影响 HNSW 图,需要触发 Segment 重建(比如临时设置 m=0 再恢复)才会生效------就像装修完才想起加电梯,得先把楼拆了重盖。
python
# 创建 Payload Index(在建索引前!)
client.create_payload_index(
collection_name="docs",
field_name="category",
field_schema="keyword", # 或 "integer" / "float" / "text" 等
)
另外,对于多个高基数字段过滤 的场景,Qdrant 还实现了 ACORN 算法来进一步改善搜索精度------多个复杂过滤条件叠加时,同程过滤的图结构可能会失真,ACORN 就是专门来"纠偏"的。
金句:"Filterable HNSW 是 Qdrant 的灵魂:别人把过滤当附加功能,它把过滤做进了索引的 DNA。"
五、核心原理三:向量量化------内存不够,精度来凑
向量数据库最大的成本往往是内存 。10M 条 768 维的 float32 向量,裸存要 ~30 GB。Qdrant 提供三种量化策略,在内存和精度之间做交易:
| 量化类型 | 压缩比 | 精度损失 | 内存占用(10M × 768 维) |
|---|---|---|---|
| 无量化 | 1x | 0% | ~30 GB |
| Scalar Quantization (SQ) | 4x | < 1% | ~8 GB |
| Product Quantization (PQ) | 8-32x | 3-5% | ~2-4 GB |
| Binary Quantization (BQ) | 32x | 5-10% | ~1 GB |
5.1 Scalar Quantization(标量量化):把浮点"四舍五入"成整数
SQ 的思路极其朴素:每个 float32 占 4 字节,干脆缩成 1 字节的 int8:
Float32: [0.023456, -0.145678, ...] 每个值 4 bytes
↓ 标量量化
Int8: [23, -146, ...] 每个值 1 byte
4 倍压缩,精度损失却 < 1%------性价比之王,官方建议"内存不足时优先启用 SQ"。
5.2 Product Quantization(乘积量化):向量切成"盲盒段",每段查码本
PQ 更激进:把高维向量切成 N 段,每段用"码本"编码成一个小数字:
向量 [384维]
↓ 切成 N 段
[48维] [48维] ... [48维]
↓ ↓ ↓
Code 3 Code 7 ... Code 12 ← 每个子段从码本里挑最接近的码字
就像把一道菜切成几份分别"快递",每份只留一个标签,收货时按标签组合。8-32 倍压缩,代价是 3-5% 精度损失,适合内存极其紧张的场景。
一句话人话:SQ 是把每个数"四舍五入"(4 倍省内存),PQ 是把向量"切片打码"(8-32 倍省内存),精度换内存,各取所需。
5.3 量化与存储组合拳
量化(压缩单条向量)+ 磁盘模式(卸载冷数据)可以叠加使用------内存不够时先上 SQ(4x 压缩、<1% 损失),还不够再让向量住磁盘,层层递进,像"先节流再开源"。
六、核心原理四:混合搜索------稠密与稀疏的"左右互搏"
6.1 为什么需要混合搜索
稠密向量(语义)和稀疏向量(关键词)各有所长,也各有盲区:
| 场景 | 稠密向量 | 稀疏向量 |
|---|---|---|
| "climate change" 找 "global warming" | ✓ 语义匹配 | ✗ 词不匹配 |
| 找包含 "API_KEY_XYZ123" 的文档 | ✗ 可能遗漏 | ✓ 精确匹配 |
| 找技术术语 "Rust cargo build" | ✓ 理解编程话题 | ✓ 精确术语匹配 |
类比:稠密向量像"阅读理解高手",懂引申义但记不住生僻词;稀疏向量像"字典扫描机",字字精确但对"气候变化"和"全球变暖"这种同义词一概不知。合体才能打。
6.2 稀疏向量:倒排索引
Qdrant 用倒排索引存储稀疏向量------只存非零维度,一个字一个词条:
稀疏向量: {word_id_123: 0.8, word_id_456: 0.5, word_id_789: 1.2, ...}
倒排索引:
word_id_123 → [Point(id=1, weight=0.8), Point(id=3, weight=0.6), ...]
word_id_456 → [Point(id=1, weight=0.5), ...]
word_id_789 → [Point(id=1, weight=1.2), Point(id=5, weight=0.9), ...]
查"内存泄漏",直接查词条 内存泄漏 → 拿到所有包含它的 Point,精确匹配,一个不漏。
6.3 Universal Query API:一套 API 三种玩法
Qdrant 的 Query API 统一了稠密、稀疏、混合三种搜索:
python
from qdrant_client import QdrantClient, models
client = QdrantClient("localhost", port=6333)
# 玩法一:纯稠密向量搜索(语义)
results = client.query(
collection_name="docs",
query=models.NearestQuery(nearest=query_vector),
limit=10,
)
# 玩法二:纯稀疏向量搜索(关键词)
results = client.query(
collection_name="docs",
query=models.NearestQuery(nearest=sparse_vector),
limit=10,
)
# 玩法三:混合搜索(RRF 融合)
results = client.query(
collection_name="docs",
prefetch=[
models.Prefetch(query=models.NearestQuery(nearest=query_vector), limit=20),
models.Prefetch(query=models.NearestQuery(nearest=sparse_vector), limit=20),
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=10,
)
注意写法:先 prefetch 两个"预检索",再 query 一个"融合器"------每个检索通道各取前 20,最后融合取 top 10。这就是"各显神通,再论功行赏"。
6.4 RRF(Reciprocal Rank Fusion)融合算法
融合的关键问题:两个通道的分数量纲完全不同 (余弦距离 vs 关键词权重),没法直接相加。RRF 的聪明之处在于------不比分数,比排名:
RRF Score = Σ ( 1 / (k + rank_i) )
其中:
rank_i = 结果在第 i 个搜索结果列表中的排名
k = 平滑常数(默认 60)
具体演算:
结果 A: 稠密搜索排名 #1, 稀疏搜索排名 #3
RRF = 1/(60+1) + 1/(60+3) = 0.0164 + 0.0159 = 0.0323
结果 B: 稠密搜索排名 #2, 稀疏搜索排名 #1
RRF = 1/(60+2) + 1/(60+1) = 0.0161 + 0.0164 = 0.0325 → 排名更高
结果 C: 稠密搜索排名 #5, 稀疏搜索排名 #5
RRF = 1/(60+5) + 1/(60+5) = 0.0308 → 排名最低
为什么用排名不用分数? 因为两个通道的分数量纲完全不同(余弦距离 vs 关键词权重),没法直接相加;而排名是"序数",天然可比。就像乒乓球比赛:A 队冠军 + 季军,和 B 队亚军 + 亚军,比的是"名次之和"而不是"得分之和"------因为两个赛场的裁判打分标准不一样。
一句话人话:RRF 就是"多个评委排座次,名次越靠前权重越高,求和定总榜"------绕开了不同打分体系的量纲问题。
七、一次查询的五段旅程(查询执行流程)
调 client.query() 时,Qdrant 内部走一条清晰的流水线:
Client → search / query
│
▼
API Layer(验证请求、鉴权)
│
▼
Collection Manager(确定目标 Shard(s),查询分发)
│
▼
Segment Engine(每个 Segment 并行执行)
│ 1. 如果是过滤查询:
│ → Filterable HNSW: 遍历图时实时检查过滤条件(同程过滤)
│ 2. 如果是混合查询:
│ → 并行执行稠密 + 稀疏搜索
│ → RRF 融合结果
│ 3. 如果是纯向量搜索:
│ → HNSW 图贪心搜索,逐层下钻
│
▼
结果合并(每个 Shard → 全局)
│ 去重、排序、取 topK
▼
最终 topK → Client
三个关键设计:
- 过滤查询走同程过滤:Filterable HNSW 在遍历时实时检查条件,跳过不满足的节点,保证凑够 K 个;
- 混合查询并行 + 融合:稠密、稀疏两条通道独立并行搜索,再用 RRF 融合------互不干扰,最后"论功行赏";
- 分段合并:每个 Segment 先算局部 topK,再到 Shard/全局合并去重------典型的"分而治之"。
优化建议速查表(生产必看)
| 方面 | 建议 | 理由 |
|---|---|---|
| Payload Index | 在大量写入之前创建 | 事后创建要触发 Segment 重建 |
| ef 参数 | 按请求动态调整 | 高精度场景调大,低延迟场景调小 |
| 量化 | 内存不足时启用 SQ | 4x 压缩,<1% 精度损失 |
| Segment | 查询密集型用大 Segment,写入密集型用小 Segment | 大段查询快、小段写入快 |
| Shard 数量 | 初始 = 节点数 | 便于后续水平扩展 |
| Replication | 生产环境 ≥ 2 | 保证高可用 |
| 磁盘 | 仅用 NVMe SSD | 全磁盘模式需要快速存储 |
八、技术栈与选型建议
8.1 完整技术栈一览
| 层面 | 技术 | 作用 |
|---|---|---|
| 核心语言 | Rust | 零成本抽象、无 GC、内存安全 |
| API 层 | REST API (HTTP) + gRPC | 双协议支持 |
| SDK | Python, JS/TS, Rust, Go, .NET, Java | 多语言客户端 |
| 稠密向量索引 | Filterable HNSW | O(log N) 搜索 + 同程过滤 |
| 稀疏向量索引 | 倒排索引 | 精确关键词匹配 |
| 混合搜索 | RRF (Reciprocal Rank Fusion) | 稠密 + 稀疏融合 |
| 量化 | SQ / PQ / Binary Quantization | 4-32x 内存压缩 |
| Payload 存储 | 原生 JSON 存储 | 向量 + 属性共存 |
| 持久化 | WAL + RocksDB | 写入持久化 + 元数据存储 |
| 内存管理 | MMAP + 缓存 | 灵活的内存/磁盘平衡 |
| 分布式 | Raft 共识 + 分片 + 复制 | 集群一致性 |
| SIMD | AVX2 / AVX-512 | 距离计算 4-8x 加速 |
为什么选 Rust? 四个字:可预测。没有 GC,就没有"垃圾回收引发的延迟尖峰";零成本抽象 + SIMD(AVX2/AVX-512),让距离计算快 4-8 倍;没有 JIT 抖动,p99 延迟稳如老狗。对实时检索来说,"偶尔慢一次"比"每次都慢一点"更致命。
8.2 性能基准参考(官方单节点,8 CPU / 64GB RAM)
| 数据集 | 维度 | QPS | p50 延迟 | p99 延迟 |
|---|---|---|---|---|
| 10M | 768 | 10,000-15,000 | 2-5ms | 5-10ms |
| 10M | 1536 | 5,000-8,000 | 5-10ms | 10-20ms |
| 100M+ | 768 | --- | --- | < 10ms |
8.3 和其他向量数据库怎么选
| 特性 | Qdrant | Chroma | Milvus | Pinecone | Weaviate |
|---|---|---|---|---|---|
| 语言 | Rust | Python + Rust (v1.0+) | Go 等 | 专有(闭源) | Go |
| 开源 | ✓ Apache 2.0 | ✓ Apache 2.0 | ✓ | ✗ SaaS | ✓ |
| 定位 | 高性能生产级 | 嵌入式开发级 | 十亿级大规模 | 免运维托管 | 模块化/图式 |
| 稠密索引 | Filterable HNSW | HNSW / SPANN | 多种(HNSW/IVF 等) | 专有 | HNSW |
| 稀疏向量 | ✓ 原生 + 倒排索引 | 有限支持 | 有限/逐步支持 | ✓ | 有限 |
| 混合搜索 | ✓ RRF 融合 | 有限 (where_document) | ✓ | ✓ | ✓ |
| Payload | ✓ 原生 JSON | metadata(16KB 限制) | ✓ | ✓ | ✓ |
| 量化 | ✓ SQ/PQ/BQ | ✗ | ✓ | ✓ | 有限 |
| 分布式 | ✓ Raft + 分片 | 有限 (Client-Server) | ✓ 原生分布式 | ✓ 完全托管 | 企业版 |
| 运维复杂度 | 中 | 极低 | 高 | 极低(贵) | 中 |
一句话选型:
- 个人项目 / RAG 原型 → Chroma(pip install 即用,零门槛)
- 百万~亿级、生产级搜索/推荐系统 → Qdrant(Rust 性能 + 过滤 + 混合搜索全都要)
- 十亿级、超大集群 → Milvus(原生分布式,规模天花板最高)
- 不想运维、预算充足、怕麻烦 → Pinecone(全托管,贵且锁定)
- 想要图数据库能力 / 模块化插件生态 → Weaviate
Qdrant 的最优场景:生产级搜索/推荐系统------既要过滤又要语义、既要混合检索又要量化省内存,还要分布式高可用,Qdrant 是"六边形战士"。
九、金句总结(看完只需要记住这几句)
- Qdrant = Rust 的性能 + Filterable HNSW 的同程过滤 + 原生 JSON Payload + 混合搜索,从零开始为向量搜索而生。
- "先搜再筛"会漏人,"边走边筛"才稳:Filterable HNSW 把过滤做进图遍历,一次遍历凑够 K 个,这是它区别于普通 HNSW 的灵魂。
- HNSW = 跳表的高维版:上层"高速公路"大跨步,第 0 层精确找,复杂度 O(log N)。
ef_construct管盖楼质量,ef管找房耐心,m管邻居密度------精度和速度是鱼和熊掌。- 量化是内存和精度的交易:SQ 4 倍压缩 <1% 损失,PQ 8-32 倍压缩 3-5% 损失,BQ 32 倍压缩 5-10% 损失。
- 混合搜索不是加法,是"论功行赏":RRF 不比分数比排名,绕开不同通道的量纲问题。
- 写入先记账(WAL)再干活:确认持久化才返回成功,崩溃也不丢数据。
课后思考题(检验你是否真懂了):
- 为什么 Filterable HNSW 是"同程过滤"而不是"前置过滤"?前置过滤先把候选集筛出来再搜索,和同程过滤比,各自有什么代价?
- 如果过滤条件极苛刻(比如 1 万篇里只有 3 篇符合),Filterable HNSW 还能保证返回 K=10 个结果吗?它的图遍历会怎么表现?
- RRF 里 k=60 这个常数的作用是什么?如果 k 调到 0,排名第 1 的结果会怎样?k 调得非常大呢?
- 为什么
ef_construct创建后不可改,而ef可以随请求动态调整?这背后的设计考量是什么? - 量化压缩的是"向量本身",那 HNSW 图的结构会被压缩吗?压缩之后搜索路径还是原来的吗?
参考资料
- Qdrant 官方文档
- Qdrant Overview(架构总览)
- Qdrant Essentials Course(免费课程)
- HNSW Indexing Fundamentals
- Filterable HNSW 官方博客
- Hybrid Queries(混合查询)
- Qdrant Sparse Vectors / 文本搜索
- Qdrant 资源优化指南
- Qdrant Distributed Deployment(分布式部署)
- Qdrant GitHub
- HNSW 原始论文 (Malkov & Yashunin)
- ACORN 论文 (Patel et al., 2024)
如果这篇文章对你有帮助,欢迎点赞、收藏、关注。下一篇可以聊聊:Milvus / Weaviate / Pinecone 的索引原理与 Qdrant 的深入对比,咱们下期见。