
去年我接了一个企业内部知识库问答的项目技术栈选型时在向量数据库上卡了整整一周。最开始图省事用了 pgvector数据量到几十万条时召回延迟开始肉眼可见地涨后来换了 Qdrant同样的数据量查询耗时直接降了一个数量级。这篇文章就把我这次 RAG 项目里的选型过程、压测数据、以及 Qdrant 生产部署时踩过的坑完整梳理一遍给正在做 RAG 相关项目的朋友一个参考。1. 从能跑到跑不动一次 RAG 选型把我逼到了 Qdrant1.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 生态里有一个经典的并行库叫 RayonQdrant 在计算向量相似度时大量使用它做数据并行。简单来说查询请求到达后Qdrant 会把候选向量分成多个子集每个子集交给一个工作线程并行计算距离最后把结果合并排序。这种分而治之的思路在 Rust 里实现得很优雅因为编译器能保证数据在线程间安全传递不会出现数据竞争这在 C 里需要极小心地加锁。一个直观的类比是餐厅厨房出餐。传统串行计算相当于一个厨师从头做到尾Rayon 并行则是把菜品拆成配菜、炒制、装盘几个工序每个厨师负责一道工序。并发量上来以后并行计算的吞吐量提升不是线性的而是曲线式的在八核机器上实测 Qdrant 的 QPS 能比单核模式提升五倍左右。2.3 编译期类型系统如何减少生产事故维护过大型 Python 项目的人大概都有这种经历一段代码在测试环境跑得好好的上了生产突然报出一个类型相关的异常原因可能是某个 API 返回了 None或者参数类型和预想的不一致。Qdrant 的核心用 Rust 写的大量此类问题在编译期就会被拦截比如这个字段一定是 f32 向量不可能是字符串这个 payload 字段一定有索引这类约束都被写进了类型系统。我用 Rust 也写过几个内部工具感受最深的是重构时的安全感。改了一个结构体的字段定义编译器会把所有引用到这个字段的地方全部标出来不会像动态语言那样漏掉某个角落导致运行时才爆雷。这对开源项目尤其重要Qdrant 核心代码质量高社区贡献者提交代码时就能通过编译器把住第一道关。3. RAG 场景的胜负手Qdrant 的索引、过滤与存储设计3.1 HNSW 参数到底怎么调Qdrant 默认的向量索引是 HNSWHierarchical Navigable Small World这是一种基于图的近似最近邻算法。它的核心思路是给向量构建一个多层图结构高层图负责快速逼近目标区域底层图负责精细搜索。对使用者来说需要关心的核心参数就两个m每个节点的最大连接数和ef_construct建图时的动态列表大小。m越大图越稠密召回率越高但内存占用和构建时间也越高。ef_construct控制建图质量值越大索引越精准但构建越慢。我在项目里的经验是数据量在百万级以内时m16、ef_construct128是一个比较平衡的起点。查询时还有一个ef参数它是查询阶段的动态候选集大小影响的是单次搜索的准确度而不是索引本身这个值可以根据业务灵活调整追求速度就设小一点追求精度就设大一点。这里给一张我测试过的参数对照表供参考数据集是 50 万条 768 维向量单机 8 核 16GB 内存参数配置召回率(Recall10)P95 延迟(ms)索引构建耗时m8, ef_construct640.911212minm16, ef_construct1280.961818minm32, ef_construct2560.983232minm64, ef_construct5120.996158min注意召回率不是越高越好很多 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 维的数据上Recall10大概从 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 API6334 是 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(urlhttp://localhost:6333) # 1. 创建集合指定向量维度、距离函数和索引参数 client.create_collection( collection_namekb_docs, vectors_configVectorParams( size768, distanceDistance.COSINE, ), hnsw_config{ m: 16, ef_construct: 128, }, ) # 2. 写入向量payload 里带上业务元数据 points [ PointStruct( id1, 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_namekb_docs, pointspoints) # 3. 带过滤条件的相似度检索 hits client.search( collection_namekb_docs, query_vector[0.22, 0.18, ...], query_filterFilter( must[ FieldCondition(keydepartment, matchMatchValue(valuehr)), ] ), limit10, )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, embeddingOpenAIEmbeddings(modeltext-embedding-3-small), path:memory:, # 内存模式测试用 collection_namekb_docs, )这里有个坑要提醒Qdrant.from_documents默认会创建一个新的集合如果集合已存在且向量维度不一致会直接报错。我在项目里有过一次教训改了 embedding 模型后忘记删旧集合排查了半天才发现是维度不匹配。生产环境建议先用Qdrant(clientclient, 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 延迟内存占用pgvector21246ms388ms2.4GBQdrant18509ms27ms1.2GBMilvus(standalone)152012ms35ms3.8GBQdrant 在吞吐上领先内存占用反而最低。pgvector 在低并发时延迟尚可一到高并发场景 P99 延迟暴涨这是因为 PostgreSQL 的 HVSCAN 在高并发下的锁竞争比较明显。Milvus 性能很不错但内存占用把整套组件的开销也算进去了单节点对比其实有点吃亏。召回精度方面Qdrant 的 HNSW 索引和 Milvus 的 HNSW 没有本质差别默认参数下都能达到 0.95 以上的 Recall10。pgvector 需要手动调hnsw.ef_search等参数默认值偏保守召回率在 0.9 左右。5.3 为什么有时 Milvus 更重pgvector 更简单压测结果很容易让人得出Qdrant 完胜的结论但选型不能只看数字。Milvus 的优势在分布式场景它把数据分片、多副本、动态扩缩容都做成了成熟功能适合十亿级向量、跨多机的场景。Qdrant 也支持分布式但它是后续版本才补上的能力在超大集群的运维成熟度上还需要时间追赶。pgvector 则适合另一种情况你的数据已经存在 PostgreSQL 里向量检索只是偶尔用一下不希望引入额外组件。如果数据量几十万以内团队也不想维护独立数据库pgvector 完全够用且足够省心。向量数据库的选型和任何技术选型一样没有绝对的最好只有和场景匹配度最高的方案。6. 生产环境里我踩过的坑和最终采用的配置6.1 内存估算与 off-heapQdrant 默认把 HNSW 图结构放在内存里向量本身存在 mmap 文件里按需加载。内存估算是上线前必须做的功课经验公式是HNSW 图结构占用的内存约为 向量总数 × 维度 × 4字节 × (m 8) / 8 左右。以 100 万条 768 维、m16 为例图结构大约需要 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 项目的人——别把宝全押在数据库上均衡地优化整条链路才真正决胜千里。