Qdrant 向量数据库工程实战:从部署到封装工具调用,打通 RAG 检索链路
一、为什么选 Qdrant
做 RAG 检索增强生成,向量库是绕不开的一环。市面上选择不少------Chroma 轻量但扛不住并发,Milvus 功能全但部署重,pgvector 复用 PostgreSQL 但检索性能有天花板。
Qdrant 用 Rust 写的,这是它最核心的竞争力。高并发查询和大规模写入时,CPU 利用率和内存控制都很稳。单机跑 Docker 也能扛千万级向量,这对大多数团队已经够用了。
几个让我选它的理由:
- HNSW + Payload 过滤:搜索向量索引的同时,利用元数据索引做预过滤,速度几乎不损失。Faiss 做同样的事会很慢
- 混合检索:Dense + Sparse 向量融合,数据库内核层面支持,不依赖外部拼接
- 量化压缩:Scalar 量化压 4 倍内存,Binary 量化更狠,处理 OpenAI 3072 维向量时省一大笔内存
- 部署极简:一个 Docker 镜像就能跑,运维成本低
| 向量库 | 语言 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| Qdrant | Rust | 高性能、混合检索、过滤强 | 需独立部署 | 生产级 RAG/推荐 |
| Chroma | Python | 轻量、上手快 | 并发弱 | 快速原型 |
| Milvus | Go/C++ | 分布式、大规模 | 部署复杂 | 企业级海量数据 |
| pgvector | C | 复用 PG 生态 | 性能有上限 | 已有 PG 环境 |
二、部署:Docker 一键启动
二进制部署、源码编译都可以,但日常开发测试,Docker 是最省心的方式。
创建 docker-compose.yml:
yaml
version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
restart: always
ports:
- "6333:6333" # REST API + Web Dashboard
- "6334:6334" # gRPC API,批量操作性能更高
volumes:
- ./qdrant_storage:/qdrant/storage # 持久化,容器删了数据不丢
启动:
bash
docker-compose up -d
验证:
bash
curl http://localhost:6333/healthz
# 返回 {"status":"ok"} 就成了
浏览器打开 http://localhost:6333/dashboard,能看到 Qdrant 自带的 Web 管理界面,查看 Collection、点数、索引状态都行。
两个端口的分工:6333 走 REST,适合调试和小批量操作;6334 走 gRPC,吞吐量比 HTTP 高约 30%,生产环境批量写入和查询优先用这个。
三、核心模型:Collection → Point → Payload
理解 Qdrant 不用记一堆概念,抓三个就够了。
Collection(集合 = 表)
└── Point(点 = 行)
├── id # 唯一标识,int 或 UUID
├── vector # 向量数组,维度必须和 Collection 定义一致
└── payload # 元数据,JSON 格式,可建索引做过滤
和关系型数据库类比一下就清楚了:
| 概念 | MySQL | Qdrant |
|---|---|---|
| 库 | Database | Cluster / Client |
| 表 | Table | Collection |
| 行 | Row | Point |
| 列 | Column | Payload |
| 主键 | Primary Key | ID (int/uuid) |
有一个容易踩的坑:Collection 创建时必须指定向量维度和距离度量,且后续不可改。 维度取决于你用的 Embedding 模型------OpenAI text-embedding-3-small 输出 1536 维,BGE 系列有 768 的也有 1024 的,搞清楚再建集合。
常用距离度量:
| 类型 | 适用场景 | 计算方式 |
|---|---|---|
| COSINE | 文本相似度(最常用) | 1 - cosθ |
| EUCLID | 图像/空间距离 | √Σ(x-y)² |
| DOT | 线性相关性 | x·y |
文本 RAG 场景,闭眼选 COSINE 就对。
四、Python SDK 核心操作
4.1 安装
bash
pip install qdrant-client
4.2 连接
python
from qdrant_client import QdrantClient
# 连接 Docker 启动的服务
client = QdrantClient(host="localhost", port=6333)
# 或者优先 gRPC,批量操作更快
client = QdrantClient(host="localhost", port=6333, grpc_port=6334, prefer_grpc=True)
# 开发测试可以用内存模式,不用跑服务端
client = QdrantClient(":memory:")
4.3 创建集合
python
from qdrant_client.models import Distance, VectorParams
client.create_collection(
collection_name="knowledge_base",
vectors_config=VectorParams(
size=1536, # OpenAI text-embedding-3-small 维度
distance=Distance.COSINE
),
)
4.4 写入向量
python
from qdrant_client.models import PointStruct
client.upsert(
collection_name="knowledge_base",
points=[
PointStruct(
id=1,
vector=[0.1, 0.2, 0.3, ...], # 1536 维
payload={"source": "web", "title": "LangGraph 架构解析", "category": "AI"}
),
PointStruct(
id=2,
vector=[0.4, 0.5, 0.6, ...],
payload={"source": "book", "title": "RAG 实战", "category": "AI"}
),
],
)
用 upsert 不用 insert------upsert 是幂等的,id 相同就覆盖,不用先查再插。
4.5 向量检索(带过滤)
这是 Qdrant 真正好用的地方------搜向量的同时按元数据过滤。
python
from qdrant_client.models import Filter, FieldCondition, MatchValue
results = client.search(
collection_name="knowledge_base",
query_vector=[0.15, 0.25, 0.35, ...], # 查询向量,1536 维
query_filter=Filter(
must=[
FieldCondition(
key="category",
match=MatchValue(value="AI") # 只搜 category 为 AI 的
)
]
),
limit=3,
with_payload=True,
)
for hit in results:
print(f"Score: {hit.score:.4f} | Title: {hit.payload['title']}")
过滤发生在搜索过程中(Pre-filtering),不是搜完再筛。 这是 Qdrant 的工程优势------在 HNSW 图遍历的同时利用 payload 索引做过滤,延迟几乎没有增长。Faiss 做同样的事是先搜回 top-k 再用 Python 过滤,结果不准还慢。
五、封装成工具:QdrantManager
裸用 SDK 没问题,但项目里每次都写一堆连接、建集合、构造 Point 的代码,重复且容易出错。封装一层管理类,对外暴露简洁接口,让 LLM Agent 也能像调函数一样调向量库。
设计思路:
QdrantManager
├── __init__ # 连接 + 自动建集合
├── add_documents # 批量写入(文本 → embedding → upsert)
├── search # 语义检索(文本 → embedding → search)
├── delete # 按 id 删除
└── info # 集合状态
完整实现:
python
import uuid
from typing import Optional
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, PointStruct,
Filter, FieldCondition, MatchValue
)
class QdrantManager:
"""Qdrant 向量数据库管理器,封装常用操作。"""
def __init__(
self,
host: str = "localhost",
port: int = 6333,
collection_name: str = "knowledge_base",
vector_size: int = 1536,
grpc_port: int = 6334,
prefer_grpc: bool = True,
):
self.client = QdrantClient(
host=host,
port=port,
grpc_port=grpc_port,
prefer_grpc=prefer_grpc,
)
self.collection_name = collection_name
self.vector_size = vector_size
# 自动建集合(不存在时才建)
if not self.client.collection_exists(collection_name):
self.client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=vector_size,
distance=Distance.COSINE,
),
)
def add_documents(
self,
texts: list[str],
embeddings: list[list[float]],
metadatas: Optional[list[dict]] = None,
) -> list[str]:
"""批量写入文档。texts 和 embeddings 等长,metadatas 可选。"""
if len(texts) != len(embeddings):
raise ValueError("texts 和 embeddings 长度不一致")
ids = [str(uuid.uuid4()) for _ in range(len(texts))]
points = [
PointStruct(
id=ids[i],
vector=embeddings[i],
payload={
"text": texts[i],
**(metadatas[i] if metadatas else {}),
},
)
for i in range(len(texts))
]
self.client.upsert(
collection_name=self.collection_name,
points=points,
)
return ids
def search(
self,
query_embedding: list[float],
limit: int = 5,
filter_conditions: Optional[list[dict]] = None,
) -> list[dict]:
"""语义检索。filter_conditions 格式: [{"key": "source", "value": "web"}]"""
query_filter = None
if filter_conditions:
query_filter = Filter(
must=[
FieldCondition(
key=cond["key"],
match=MatchValue(value=cond["value"]),
)
for cond in filter_conditions
]
)
results = self.client.search(
collection_name=self.collection_name,
query_vector=query_embedding,
query_filter=query_filter,
limit=limit,
with_payload=True,
)
return [
{
"id": hit.id,
"score": hit.score,
"text": hit.payload.get("text", ""),
"metadata": {k: v for k, v in hit.payload.items() if k != "text"},
}
for hit in results
]
def delete(self, point_ids: list[str]):
"""按 id 删除向量。"""
self.client.delete(
collection_name=self.collection_name,
points_selector=point_ids,
)
def info(self) -> dict:
"""获取集合状态。"""
info = self.client.get_collection(self.collection_name)
return {
"collection": self.collection_name,
"vectors_count": info.vectors_count,
"status": info.status,
}
六、调用:在业务中直接用
封装好了,调用就几行的事。
6.1 写入知识库
python
from qdrant_manager import QdrantManager
# 初始化(自动建集合)
qdrant = QdrantManager(
host="localhost",
port=6333,
collection_name="my_kb",
vector_size=1536,
)
# 假设 embedding 模型已经把文本转向量
texts = [
"LangGraph 用有状态图编排 Agent 工作流",
"RAG 通过检索外部知识减少大模型幻觉",
"Qdrant 的 HNSW 索引支持带过滤的近似搜索",
]
embeddings = [
[0.1, 0.2, ...], # 1536 维
[0.3, 0.4, ...],
[0.5, 0.6, ...],
]
metadatas = [
{"source": "blog", "category": "Agent"},
{"source": "doc", "category": "RAG"},
{"source": "blog", "category": "VectorDB"},
]
ids = qdrant.add_documents(texts, embeddings, metadatas)
print(f"写入 {len(ids)} 条,IDs: {ids}")
6.2 语义检索 + 过滤
python
# 查询向量(同样由 embedding 模型生成)
query_vector = [0.15, 0.25, ...]
# 不带过滤
results = qdrant.search(query_embedding=query_vector, limit=3)
for r in results:
print(f"Score: {r['score']:.4f} | {r['text'][:50]}")
# 带 filter:只搜 source=blog 的
results = qdrant.search(
query_embedding=query_vector,
limit=3,
filter_conditions=[{"key": "source", "value": "blog"}],
)
for r in results:
print(f"[filtered] Score: {r['score']:.4f} | {r['text'][:50]}")
6.3 接入 RAG 链路
python
def retrieve_context(query: str, top_k: int = 3) -> str:
"""RAG 检索:query → embedding → 向量搜索 → 拼接上下文。"""
query_embedding = embed(query) # 你的 embedding 函数
results = qdrant.search(
query_embedding=query_embedding,
limit=top_k,
)
context = "\n---\n".join([r["text"] for r in results])
return context
# 完整 RAG 调用
context = retrieve_context("LangGraph 的核心架构是什么?")
prompt = f"基于以下知识回答问题:\n{context}\n\n问题:LangGraph 的核心架构是什么?"
answer = llm.invoke(prompt)
print(answer)
整个过程的数据流:
用户提问
→ Embedding 模型转向量
→ QdrantManager.search() 向量检索
→ 召回 top-k 相关文档
→ 拼接上下文 → LLM 生成回答
七、几个工程实践经验
1. 用 UUID 不要用自增 int。 多个写入源并发操作时,自增 id 会撞车。UUID 保证全局唯一,upsert 天然幂等。
2. 批量写入用 gRPC。 数据量大时,prefer_grpc=True 吞吐量高约 30%。REST 适合调试和小批量。
3. payload 字段建索引。 如果某个字段频繁用于过滤(比如 source、category),在创建集合时给它建 payload 索引,过滤速度会有数量级提升。
python
from qdrant_client.models import PayloadSchemaType
client.create_payload_index(
collection_name="knowledge_base",
field_name="source",
field_schema=PayloadSchemaType.KEYWORD,
)
4. 别忘了退出条件。 如果用循环检索(搜一次不够再搜一次),必须设尝试次数上限,不然死循环。
5. 公网部署必须开认证。 Qdrant 默认无身份认证,裸跑在公网上就是裸奔。配置 API Key 再暴露。
小结
Qdrant 的工程定位很清晰:Rust 性能底座 + HNSW 索引 + Payload 过滤 + 量化压缩,四件套组合下来,在向量检索这条赛道上,单机性能和功能完整度都够打。
把核心操作封装成 QdrantManager 这种工具类之后,上层业务代码只管调 add_documents 和 search,不用关心连接管理、Point 构造、Filter 拼接这些细节。RAG 链路里检索这一环,算是跑通了。