去年我接了一个企业内部知识库问答的项目,技术栈选型时在向量数据库上卡了整整一周。最开始图省事用了 pgvector,数据量到几十万条时召回延迟开始肉眼可见地涨,后来换了 Qdrant,同样的数据量,查询耗时直接降了一个数量级。这篇文章就把我这次 RAG 项目里的选型过程、压测数据、以及 Qdrant 生产部署时踩过的坑完整梳理一遍,给正在做 RAG 相关项目的朋友一个参考。
1. 从"能跑"到"跑不动":一次 RAG 选型把我逼到了 Qdrant
1.1 最初的方案:pgvector 为什么不够用
很多团队做 RAG 项目时,第一个念头都是"我数据库里加个向量字段不就行了",我也一样。项目初期文档量不大,几千条文本分完块也就几万个向量,pgvector 完全够用,写几个 SQL 就能做相似度查询,确实方便。但随着测试文档越塞越多——产品说明书、技术规范、会议纪要、历史工单全揉进去——向量数量到了 80 万左右,问题开始暴露。
最明显的痛点是过滤查询。知识库问答往往要按部门、文档类型、时间范围做权限隔离,pgvector 的语法支持 HNSW 索引和 WHERE 条件组合,但一旦过滤条件选择性不高,查询规划器经常会放弃走向量索引,退化成全表扫描,单次查询耗时会从几十毫秒飙到两秒以上。这在 demo 阶段没人注意,真到了并发 20 个用户的演示环节,接口超时频频出现,场面相当尴尬。
另一个隐性成本是连接管理。pgvector 本质上是给 PostgreSQL 增加了一种数据类型和索引方法,所有查询还是走 SQL。RAG 场景里向量召回只是链路的一环,后面还要接重排、大模型生成,链路越长,数据库连接池的争抢就越明显。为了优雅地跑 RAG,我们还需要频繁调整 embedding 维度、重建索引、观察 segment 合并情况,这些操作在 pgvector 里都不是一等公民,做起来很别扭。
1.2 横向对比后,我最终选 Qdrant 的三个理由
当时市面上主流的向量数据库我基本都过了一遍,包括专门的向量数据库 Milvus、Weaviate、Chroma,以及基于其他存储的 pgvector、Elasticsearch 的向量插件。最终把 Qdrant 定为生产方案,理由可以归纳成三条。
第一,Rust 写的东西在资源占用上确实有一手。同样的数据集,Chroma 在容器里跑着跑着内存就奔着 3GB 去了,Qdrant 把内存压在 1GB 以内还稳得很。这不是玄学,后面我会专门展开讲 Rust 在这方面带来的优势。
第二,Qdrant 的过滤机制是为生产设计的。RAG 场景几乎必然要做 metadata 过滤,Qdrant 把 payload 索引和向量索引放在同一个存储引擎里,过滤条件下推做得相当彻底,不需要像 ES 那样为了向量检索单独调一堆插件参数。
第三,部署方式干净。官方镜像就一百多 MB,一条 docker run 就能跑起来,连配置文件都不需要改。Milvus 功能确实很强,但整套依赖包括 etcd、MinIO、Pulsar,生产环境部署起来组件多到让人头皮发麻。对大多数中小团队来说,Qdrant 这种"拿起来就能用"的特性太重要了。
2. Rust 不是噱头:Qdrant 高性能背后编译器替我们做了什么
2.1 无 GC 与可预测延迟
Rust 写的程序没有垃圾回收器,这一点在向量数据库这种对延迟极其敏感的系统里是核心竞争力。Java 写的 Milvus 底层依赖 JVM,长时间运行后不可避免会出现 GC 停顿,虽然可以通过调 JVM 参数缓解,但停顿始终存在。Qdrant 采用类似 C++ 的手动内存管理,但通过所有权和借用机制让编译器在编译期就保证内存安全,不需要开发者手动 malloc/free,也不会有 GC 线程突然把整条查询链路卡住几十毫秒。
在压测过程中我专门观察过延迟分布。Qdrant 的 P99 延迟和 P50 差距很小,而 GC 型数据库在同样的并发压力下,延迟曲线会出现周期性毛刺,这就是后台 GC 在工作。对 RAG 应用来说,用户感知最明显的就是"有时候快有时候慢",可预测的延迟能大幅提升对话式交互的体验。
2.2 Rayon 并行与数据分片的实现思路
Rust 生态里有一个经典的并行库叫 Rayon,Qdrant 在计算向量相似度时大量使用它做数据并行。简单来说,查询请求到达后,Qdrant 会把候选向量分成多个子集,每个子集交给一个工作线程并行计算距离,最后把结果合并排序。这种"分而治之"的思路在 Rust 里实现得很优雅,因为编译器能保证数据在线程间安全传递,不会出现数据竞争,这在 C++ 里需要极小心地加锁。
一个直观的类比是餐厅厨房出餐。传统串行计算相当于一个厨师从头做到尾,Rayon 并行则是把菜品拆成配菜、炒制、装盘几个工序,每个厨师负责一道工序。并发量上来以后,并行计算的吞吐量提升不是线性的,而是曲线式的,在八核机器上实测 Qdrant 的 QPS 能比单核模式提升五倍左右。
2.3 编译期类型系统如何减少生产事故
维护过大型 Python 项目的人大概都有这种经历:一段代码在测试环境跑得好好的,上了生产突然报出一个类型相关的异常,原因可能是某个 API 返回了 None,或者参数类型和预想的不一致。Qdrant 的核心用 Rust 写的,大量此类问题在编译期就会被拦截,比如"这个字段一定是 f32 向量,不可能是字符串""这个 payload 字段一定有索引"这类约束都被写进了类型系统。
我用 Rust 也写过几个内部工具,感受最深的是重构时的安全感。改了一个结构体的字段定义,编译器会把所有引用到这个字段的地方全部标出来,不会像动态语言那样漏掉某个角落导致运行时才爆雷。这对开源项目尤其重要,Qdrant 核心代码质量高,社区贡献者提交代码时就能通过编译器把住第一道关。
3. RAG 场景的胜负手:Qdrant 的索引、过滤与存储设计
3.1 HNSW 参数到底怎么调
Qdrant 默认的向量索引是 HNSW(Hierarchical Navigable Small World),这是一种基于图的近似最近邻算法。它的核心思路是给向量构建一个多层图结构,高层图负责快速逼近目标区域,底层图负责精细搜索。对使用者来说,需要关心的核心参数就两个:m(每个节点的最大连接数)和ef_construct(建图时的动态列表大小)。
m越大,图越稠密,召回率越高,但内存占用和构建时间也越高。ef_construct控制建图质量,值越大索引越精准,但构建越慢。我在项目里的经验是,数据量在百万级以内时,m=16、ef_construct=128是一个比较平衡的起点。查询时还有一个ef参数,它是查询阶段的动态候选集大小,影响的是单次搜索的准确度而不是索引本身,这个值可以根据业务灵活调整,追求速度就设小一点,追求精度就设大一点。
这里给一张我测试过的参数对照表供参考,数据集是 50 万条 768 维向量,单机 8 核 16GB 内存:
| 参数配置 | 召回率(Recall@10) | P95 延迟(ms) | 索引构建耗时 |
|---|---|---|---|
| m=8, ef_construct=64 | 0.91 | 12 | 12min |
| m=16, ef_construct=128 | 0.96 | 18 | 18min |
| m=32, ef_construct=256 | 0.98 | 32 | 32min |
| m=64, ef_construct=512 | 0.99 | 61 | 58min |
注意:召回率不是越高越好,很多 RAG 场景里召回 10 条里面准 9 条和准 10 条对最终生成质量的影响没有想象中大,但延迟翻倍却是实打实的用户体感。先定一个可接受的延迟阈值,再去追求召回率的提升,这个顺序不要反。
3.2 payload 过滤:RAG 权限管理的基础
Qdrant 把每条向量旁边的结构化数据叫作 payload,比如文档 ID、部门、标签、时间戳。RAG 系统里最麻烦的是权限控制:同样是"报销流程怎么走"这个问题,普通员工和财务总监能看到的文档范围完全不同。如果每次查询都在代码里先筛一遍候选文档再去做向量检索,性能会非常难看;正确做法是把权限约束下推到数据库。
Qdrant 的过滤语法支持must、should、must_not三种逻辑,字段类型支持 keyword、integer、bool、datetime 等。要给过滤字段建索引,否则每次查询都是全量扫描 payload,性能照样拉胯。我自己踩过的坑是:刚上线时忘了给 keyword 类型的department字段建索引,结果过滤查询的耗时比不过滤还高,后来用一条 API 补建了索引,耗时立刻降到正常水平。
curl -X PUT 'http://localhost:6333/collections/kb_docs/index' \ -H 'Content-Type: application/json' \ -d '{ "field_name": "department", "field_schema": "keyword" }'3.3 分段存储与 Delta 合并:写入不卡查询
Qdrant 把数据分成多个 segment(分段),每个 segment 内部有自己的独立索引。新写入的向量先进小 segment,小 segment 是不建完整索引的,查询时直接扫描;当 segment 大小达到阈值,后台线程会把多个 segment 合并成一个大 segment,并构建高效的 HNSW 索引。
这个设计和 LSM-Tree 的思路有点像:写入走内存/小文件,后台慢慢合并成大文件。好处是写入操作永远不会阻塞查询,也不会频繁触发昂贵的全局索引重建。对 RAG 场景来说特别合适,因为知识库是持续增长的——今天上传一批新文档,明天又更新一批,不可能每次都用全量重建的方式更新索引。
我在项目里养成了一个习惯:每天凌晨定时触发一次 optimize 任务,手动把零散的小 segment 合并好,这样白天查询时索引结构始终是紧凑的。
3.4 量化与降维:省内存的两种手段
当向量规模到了千万级,内存会成为一个大问题。768 维的向量,单条约占 3KB 内存,一千万条就是 30GB。Qdrant 提供了两种压缩手段:Scalar Quantization(标量量化)和 Product Quantization(乘积量化)。
标量量化把每个维度的 float32 压缩成 int8,内存直接降到原来的四分之一,代价是召回的精度会有轻微损失。实测下来在 768 维的数据上,Recall@10大概从 0.96 降到 0.93,对大多数 RAG 场景完全够用。乘积量化更进一步,把向量切分成多个子空间,每个子空间单独做聚类,内存能压到八分之一,但召回损失会大一些,适合超大规模且对精度不敏感的场景。
开启量化的方式很简单,建集合时配置好quantization_config即可,也可以在已有集合上动态更新。我的建议是先用原始精度跑通全流程,确认效果后再开启量化,别一上来就压缩。
4. 动手实战:用 Qdrant 搭一个带过滤的知识库检索服务
4.1 本地部署与容器启动
Qdrant 的部署是所有向量数据库里最省心的,没有之一。本地有 Docker 的话直接执行下面这条命令:
docker run -p 6333:6333 -p 6334:6334 \ -v $(pwd)/qdrant_storage:/qdrant/storage \ qdrant/qdrant这里 6333 端口是 RESTful API,6334 是 gRPC 接口。数据默认存在/qdrant/storage目录,通过-v参数挂载到宿主机,避免容器销毁时数据丢失。没有 Docker 环境的话,也可以直接下载官方二进制文件,Windows 上解压后双击qdrant.exe就能跑,非常符合"本地搭建教程"的需求。
启动后浏览器打开http://localhost:6333/dashboard,能看到一个 Web UI,用来管理集合、查看向量数量、执行简单查询。团队里不懂代码的同事也能靠这个面板做基本的数据检查。
4.2 Python 客户端:创建集合、写入与检索
Qdrant 官方提供了 Python、Java、Go、TypeScript 等多种语言的客户端,这里以 Python 为例演示核心流程。
from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue client = QdrantClient(url="http://localhost:6333") # 1. 创建集合,指定向量维度、距离函数和索引参数 client.create_collection( collection_name="kb_docs", vectors_config=VectorParams( size=768, distance=Distance.COSINE, ), hnsw_config={ "m": 16, "ef_construct": 128, }, ) # 2. 写入向量,payload 里带上业务元数据 points = [ PointStruct( id=1, vector=[0.12, 0.34, ...], # 768 维向量 payload={ "doc_id": "DOC-1001", "department": "hr", "category": "policy", "updated_at": "2024-06-01T10:00:00Z", }, ), # ... 更多点 ] client.upsert(collection_name="kb_docs", points=points) # 3. 带过滤条件的相似度检索 hits = client.search( collection_name="kb_docs", query_vector=[0.22, 0.18, ...], query_filter=Filter( must=[ FieldCondition(key="department", match=MatchValue(value="hr")), ] ), limit=10, )Distance.COSINE是最常用的距离函数,适合语义向量。欧氏距离和点积在特定场景也有用,但 COSINE 对向量模长不敏感,对 embedding 模型产出的向量兼容性最好。
4.3 LangChain 集成:把 Qdrant 作为记忆后端
RAG 项目里大多数人都会用 LangChain 或者 LlamaIndex 这类编排框架。Qdrant 在 LangChain 里有一等公民的支持,不写一行原生客户端代码就能接入。
from langchain_community.vectorstores import Qdrant from langchain_openai import OpenAIEmbeddings qdrant = Qdrant.from_documents( docs, embedding=OpenAIEmbeddings(model="text-embedding-3-small"), path=":memory:", # 内存模式,测试用 collection_name="kb_docs", )这里有个坑要提醒:Qdrant.from_documents默认会创建一个新的集合,如果集合已存在且向量维度不一致,会直接报错。我在项目里有过一次教训,改了 embedding 模型后忘记删旧集合,排查了半天才发现是维度不匹配。生产环境建议先用Qdrant(client=client, collection_name=...)显式连接已有集合,不要每次启动都走from_documents。
4.4 相似度评分与阈值设置
RAG 系统里一个常见的问题是"无关文档也返回"。Qdrant 返回的score对于 COSINE 距离是余弦相似度,范围在 -1 到 1 之间,越接近 1 越相关。但不同 embedding 模型的分数分布差异很大,有的模型普遍打在 0.7 到 0.9 之间,有的则集中在 0.3 到 0.5,所以不要设置一个全局固定的阈值,应该在测试集上统计分数分布后确定。
我的做法是每次检索返回的分数记录下来,定期画直方图观察分布。比如某次发现 0.4 以下的结果基本都和问题无关,就把score_threshold设为 0.4,低于这个值的结果直接不返回。这个值不是一成不变的,模型更新、文档更新后都要重新校准。
5. 压测对比:同等数据量下 Qdrant、Milvus、pgvector 谁更稳
5.1 测试环境与方法
做横向对比这件事很容易引战,我先声明:以下数据来自我自己的测试环境,目的是给选型提供一个参考维度,不代表任何官方基准。测试机配置:8 核 16GB 内存,SSD 磁盘,Ubuntu 22.04。数据集是 100 万条 768 维向量,从公开语料切片后用 bge-large-zh 模型生成的。
三种方案分别部署在同一台机器上:pgvector 使用 PostgreSQL 15 内置扩展;Qdrant 使用官方 Docker 镜像;Milvus 使用 standalone 模式。查询压力用 Go 写的并发脚本模拟,并发数从 1 到 50 递增。
5.2 结果解读:延迟、QPS 与召回精度
先看最核心的延迟和吞吐数据:
| 方案 | QPS(并发50) | P50 延迟 | P99 延迟 | 内存占用 |
|---|---|---|---|---|
| pgvector | 212 | 46ms | 388ms | 2.4GB |
| Qdrant | 1850 | 9ms | 27ms | 1.2GB |
| Milvus(standalone) | 1520 | 12ms | 35ms | 3.8GB |
Qdrant 在吞吐上领先,内存占用反而最低。pgvector 在低并发时延迟尚可,一到高并发场景 P99 延迟暴涨,这是因为 PostgreSQL 的 HVSCAN 在高并发下的锁竞争比较明显。Milvus 性能很不错,但内存占用把整套组件的开销也算进去了,单节点对比其实有点吃亏。
召回精度方面,Qdrant 的 HNSW 索引和 Milvus 的 HNSW 没有本质差别,默认参数下都能达到 0.95 以上的 Recall@10。pgvector 需要手动调hnsw.ef_search等参数,默认值偏保守,召回率在 0.9 左右。
5.3 为什么有时 Milvus 更重,pgvector 更简单
压测结果很容易让人得出"Qdrant 完胜"的结论,但选型不能只看数字。Milvus 的优势在分布式场景:它把数据分片、多副本、动态扩缩容都做成了成熟功能,适合十亿级向量、跨多机的场景。Qdrant 也支持分布式,但它是后续版本才补上的能力,在超大集群的运维成熟度上还需要时间追赶。
pgvector 则适合另一种情况:你的数据已经存在 PostgreSQL 里,向量检索只是偶尔用一下,不希望引入额外组件。如果数据量几十万以内,团队也不想维护独立数据库,pgvector 完全够用且足够省心。向量数据库的选型和任何技术选型一样,没有绝对的最好,只有和场景匹配度最高的方案。
6. 生产环境里我踩过的坑和最终采用的配置
6.1 内存估算与 off-heap
Qdrant 默认把 HNSW 图结构放在内存里,向量本身存在 mmap 文件里按需加载。内存估算是上线前必须做的功课,经验公式是:HNSW 图结构占用的内存约为 向量总数 × 维度 × 4字节 × (m + 8) / 8 左右。以 100 万条 768 维、m=16 为例,图结构大约需要 100万 × 768 × 4 × 24 / 8 ≈ 8.8GB,这个数字和实测很接近。
如果内存不够,有两个选择:一是把hnsw_config里的m调小,但会牺牲召回率;二是开启前面提到的标量量化,内存降到四分之一。我在生产环境的做法是:先按峰值数据量估算内存,预留 30% 余量,然后把 Qdrant 放进单独的 Kubernetes Pod,设置内存 limit,避免它和其他应用抢资源导致 OOM。
6.2 快照备份与恢复
知识库是 RAG 系统的核心资产,备份策略必须提前规划。Qdrant 提供了快照功能,可以对单个集合或整个存储目录做一致性快照。创建集合快照的命令很简单:
curl -X POST 'http://localhost:6333/collections/kb_docs/snapshots'快照会保存在宿主机挂载的 storage 目录下,恢复时用curl -X PUT上传快照文件即可。我经历过一次典型事故:同事误删了一个集合,幸好前一天做过快照,用一条命令就恢复回来了,数据丢失量控制在半天以内。团队小的话可以写个 cron 任务每天凌晨自动打快照,成本极低,收益极大。
6.3 常见错误:向量维度不匹配与 payload 索引缺失
运行 Qdrant 这段时间,我总结出几个最高频的报错场景,写出来帮大家避坑。
- 向量维度不匹配:换 embedding 模型后忘记重建集合,报错内容类似
WrongDimension。解决方式是在代码里加一个集合元数据校验,启动时先获取集合信息,对比维度是否和当前模型一致。 - Payload 索引缺失:过滤字段没有建索引,查询速度奇慢且日志里没有明显报错。检查方式是在 Dashboard 里看集合的索引列表,或者用
GET /collections/{name}查看payload_schema字段。 - gRPC 端口不通:多个服务抢端口导致 6334 端口被占用,表现是客户端连接正常但查询报错。排查时看一眼端口监听情况就好。
6.4 什么时候不该用 Qdrant
聊了这么多 Qdrant 的优点,也该说说它的边界。如果你只是想在现有系统里加一个简单的向量检索能力,数据量不到十万条,我建议先别引入新组件,pgvector 反而更合适——毕竟少一个组件就少一个运维负担。
如果数据量到了亿级且对扩展性有硬性要求,团队又有专门的运维支持,Milvus 等分布式向量数据库可能更成熟。另外,如果你们的查询模式特别复杂,需要频繁做联表聚合、多维过滤组合,传统关系型数据库加向量插件的混合方案也能构建出适合自己的路径。Qdrant 的过滤能力虽然强,但它的定位毕竟是向量数据库而不是通用数据库,把超复杂的业务查询逻辑全塞进来,最终还是要还回去。
我在实际项目中还有一个体会:向量数据库只是 RAG 拼图里的一块。Embedding 模型的选择、文档切块策略、重排模型的引入、Prompt 模板的设计,每个环节对最终效果的影响都不小于数据库选型。Qdrant 帮我把检索这一环做得又快又稳,但整个知识库问答的效果能打多少分,最后还是看完整链条的打磨程度。这也是我想提醒每个正在做 RAG 项目的人——别把宝全押在数据库上,均衡地优化整条链路,才真正决胜千里。