Qdrant向量数据库工程实战

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 字段建索引。 如果某个字段频繁用于过滤(比如 sourcecategory),在创建集合时给它建 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_documentssearch,不用关心连接管理、Point 构造、Filter 拼接这些细节。RAG 链路里检索这一环,算是跑通了。

相关推荐
DM今天肝到几点?1 小时前
GPT-6 Astra 发布:ARC-AGI-3 从 7.8% 跳到 99.9%,OpenAI 宣告「欢迎进入 AGI 时代」
人工智能·gpt·深度学习·agi
智途 Tech1 小时前
2026年分析多个Excel、CSV和网页数据的AI工具清单:Tabbit 浏览器多源引用
大数据·人工智能·excel
李妍.1 小时前
PyTorch GPU 版安装记录(RTX 5060 + Python 3.14)
人工智能·pytorch·python
海天一色y1 小时前
注意力机制的进化:MHA → GQA → MLA → CSA → HCA
人工智能·注意力机制
广慈新医知1 小时前
当健康问题越来越复杂,AI更适合先帮我们“把问题问清楚”
人工智能·python
驱动小百科1 小时前
GPT-6 Astra正式亮相 OpenAI为何称其开启AGI时代?
人工智能·gpt·agi·gpt-6·gpt-6 astra
法正智能1 小时前
AI编标赛道的终局:法正智能用“编+检+数据资产”构建企业级护城河
人工智能
ZGG0031 小时前
线程池详解:从 7 大参数到生产避坑
java·数据库·后端
龙亘川1 小时前
数智赋能退役军人服务:V1.0 系统搭建全生命周期闭环服务体系
大数据·数据库·人工智能