作为一名在 AI 领域摸爬滚打多年的开发者,我深知数据检索对于 AI 应用的重要性。去年我帮一家电商公司优化搜索系统时,他们每个月在 OpenAI API 上的开销高达 $2000,其中超过 60% 都浪费在了无关的检索结果重新排序上。直到我引入了 Qdrant 向量数据库的过滤机制,他们的 API 调用量直接下降了 45%,搜索准确率反而提升了 30%。今天我就把这套实战经验分享给大家,手把手教你用 Qdrant 实现工业级的 RAG 过滤与分面搜索。

一、什么是 RAG?为什么你的 AI 应用需要向量过滤

先科普一下 RAG(Retrieval-Augmented Generation)。简单说就是:当用户提问时,先从知识库中找到最相关的文档片段,再把这些片段和问题一起交给大模型生成答案。没有 RAG,大模型只能靠"记忆"回答,很容易"一本正经地胡说八道"。

但这里有个痛点——普通向量搜索只返回"最相似"的结果,不考虑时间、类别、权限等业务维度。比如:

这就是 Qdrant 的强项——它支持在向量相似度搜索的基础上,叠加多维度条件过滤,完美解决"相关性"与"业务规则"的矛盾。

二、前置准备:注册 HolySheep AI 获取 API Key

在开始之前,你需要一个大模型 API Key。我推荐使用 HolySheep AI,原因有三:

点击 立即注册 HolySheep AI,完成手机号验证后,在控制台左侧菜单点击"API Keys",创建一个新的 Key,复制备用。Key 格式类似 HS-xxxxxxxxxxxxxxxx

【截图提示:HolySheep AI 控制台 → API Keys 页面 → 显示新建的 API Key】

三、搭建 Qdrant 向量数据库环境

Qdrant 是用 Rust 编写的高性能向量数据库,支持本地部署和云端服务。对于初学者,我建议先用 Docker 本地部署,零成本上手。

3.1 安装 Docker Desktop

如果你还没装 Docker,官网下载 Docker Desktop 安装包(Windows/Mac 均支持),双击安装即可。安装完成后打开终端,输入以下命令验证:

docker --version

正常情况输出:Docker version 24.x.x

3.2 一键启动 Qdrant

docker run -d --name qdrant \
  -p 6333:6333 \
  -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant

参数说明:

启动成功后,浏览器访问 http://localhost:6333/dashboard,你能看到 Qdrant 的管理界面。

【截图提示:Qdrant Dashboard 界面,显示 "Qdrant is ready" 状态】

四、Python 环境配置与依赖安装

我假设你已经装了 Python 3.8+。如果没有,去 Python 官网下载安装包。打开终端,创建项目目录:

mkdir rag-qdrant-tutorial && cd rag-qdrant-tutorial
python -m venv venv
source venv/bin/activate  # Windows 用户运行 venv\Scripts\activate

安装所需依赖包:

pip install qdrant-client openai python-dotenv sentence-transformers

创建 .env 文件存储敏感信息:

HOLYSHEEP_API_KEY=你的HS-API-KEY
HOLYSHEEP_BASE_URL=https://api.holysheep.ai/v1
QDRANT_HOST=localhost
QDRANT_PORT=6333

五、实战一:构建带过滤条件的文档知识库

5.1 定义文档数据结构

假设我们要构建一个产品知识库,支持按"产品类别"和"在售状态"过滤。我创建 documents.py

import json
from datetime import datetime

定义产品文档数据结构

products = [ { "id": "prod_001", "content": "iPhone 15 Pro 采用钛金属设计,配备 A17 Pro 芯片,支持灵动岛功能。", "category": "手机", "status": "在售", "price": 7999, "update_time": "2024-03-15" }, { "id": "prod_002", "content": "MacBook Pro 14 英寸搭载 M3 Pro 芯片,续航长达 17 小时。", "category": "电脑", "status": "在售", "price": 15999, "update_time": "2024-02-20" }, { "id": "prod_003", "content": "iPhone 13 已从官网下架,仅在授权经销商处有库存。", "category": "手机", "status": "已下架", "price": 4999, "update_time": "2023-12-01" }, { "id": "prod_004", "content": "AirPods Pro 2 支持主动降噪和空间音频,续航 6 小时。", "category": "配件", "status": "在售", "price": 1899, "update_time": "2024-01-10" } ] print(f"加载了 {len(products)} 条产品文档")

5.2 初始化 Qdrant 客户端并创建 Collection

创建 qdrant_setup.py,这是核心的数据库初始化逻辑:

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, Filter, FieldCondition, MatchValue, Range
import os
from dotenv import load_dotenv

load_dotenv()

连接 Qdrant

client = QdrantClient( host=os.getenv("QDRANT_HOST", "localhost"), port=int(os.getenv("QDRANT_PORT", 6333)) ) COLLECTION_NAME = "product_knowledge_base" VECTOR_SIZE = 384 # sentence-transformers/all-MiniLM-L6-v2 模型的维度 def create_collection(): """创建 Collection(类似 MySQL 的表)""" # 先检查是否已存在,存在则删除重建 collections = client.get_collections().collections if any(c.name == COLLECTION_NAME for c in collections): client.delete_collection(collection_name=COLLECTION_NAME) print(f"已删除旧 Collection: {COLLECTION_NAME}") # 创建新 Collection,指定向量维度和距离计算方式 client.create_collection( collection_name=COLLECTION_NAME, vectors_config=VectorParams( size=VECTOR_SIZE, distance=Distance.COSINE # 余弦相似度,值越接近 1 越相似 ) ) print(f"成功创建 Collection: {COLLECTION_NAME}") # 创建索引以加速过滤查询(可选但强烈推荐) client.create_index( collection_name=COLLECTION_NAME, field_name="category", field_schema="keyword" # 精确匹配索引 ) client.create_index( collection_name=COLLECTION_NAME, field_name="price", field_schema="float" # 数值范围索引 ) print("已创建索引:category (keyword), price (float)") if __name__ == "__main__": create_collection()

运行 python qdrant_setup.py,你应该看到:

已删除旧 Collection: product_knowledge_base
成功创建 Collection: product_knowledge_base
已创建索引:category (keyword), price (float)

【截图提示:Qdrant Dashboard 中显示新创建的 product_knowledge_base Collection】

5.3 嵌入文档并上传到 Qdrant

现在需要把文档内容转成向量存入 Qdrant。创建 ingest.py

from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer
from dotenv import load_dotenv
import os
import numpy as np

load_dotenv()

初始化模型(首次运行会下载模型,需要几分钟)

print("正在加载文本嵌入模型...") model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') print("模型加载完成!")

连接 Qdrant

client = QdrantClient( host=os.getenv("QDRANT_HOST", "localhost"), port=int(os.getenv("QDRANT_PORT", 6333)) ) COLLECTION_NAME = "product_knowledge_base" def ingest_documents(products): """将文档嵌入并上传到 Qdrant""" points = [] for i, doc in enumerate(products): # 生成向量嵌入 vector = model.encode(doc["content"]).tolist() # 构造 Point 结构(Qdrant 的基本存储单元) point = { "id": i + 1, # Qdrant 要求整数 ID "vector": vector, "payload": { "id": doc["id"], "content": doc["content"], "category": doc["category"], "status": doc["status"], "price": doc["price"], "update_time": doc["update_time"] } } points.append(point) # 批量上传(每 100 条提交一次) if len(points) >= 100 or i == len(products) - 1: client.upsert( collection_name=COLLECTION_NAME, points=points ) print(f"已上传 {len(points)} 条文档") points = [] print(f"全部 {len(products)} 条文档上传完成!") if __name__ == "__main__": from documents import products ingest_documents(products)

运行 python ingest.py,输出类似:

正在加载文本嵌入模型...
模型加载完成!
已上传 1 条文档
已上传 1 条文档
已上传 1 条文档
已上传 1 条文档
全部 4 条文档上传完成!

【截图提示:Qdrant Dashboard 中 product_knowledge_base Collection 包含 4 条 points】

六、实战二:实现带过滤的向量搜索

6.1 基础相似度搜索(无过滤)

先看看不加过滤时的搜索效果,创建 search_basic.py

from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer
from dotenv import load_dotenv
import os

load_dotenv()

model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
client = QdrantClient(
    host=os.getenv("QDRANT_HOST", "localhost"),
    port=int(os.getenv("QDRANT_PORT", 6333))
)

COLLECTION_NAME = "product_knowledge_base"

def basic_search(query_text, top_k=3):
    """基础相似度搜索,返回最相关的 top_k 结果"""
    
    query_vector = model.encode(query_text).tolist()
    
    results = client.search(
        collection_name=COLLECTION_NAME,
        query_vector=query_vector,
        limit=top_k
    )
    
    print(f"\n🔍 搜索词:{query_text}")
    print(f"找到 {len(results)} 条结果:\n")
    
    for i, result in enumerate(results, 1):
        print(f"【结果 {i}】相似度:{result.score:.4f}")
        print(f"  内容:{result.payload['content']}")
        print(f"  分类:{result.payload['category']} | 状态:{result.payload['status']}")
        print(f"  价格:¥{result.payload['price']}\n")

if __name__ == "__main__":
    basic_search("续航时间长的笔记本电脑")

运行后你会发现,搜索"续航时间长的笔记本电脑",iPhone 15 Pro 也在结果里(因为手机也提到了一些技术参数,语义上有点相关)。这就是无过滤搜索的问题。

6.2 带过滤条件的精确搜索

现在加上过滤条件,只返回"电脑"类别的在售产品。创建 search_filtered.py

from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue, Must, MustNot
from sentence_transformers import SentenceTransformer
from dotenv import load_dotenv
import os

load_dotenv()

model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
client = QdrantClient(
    host=os.getenv("QDRANT_HOST", "localhost"),
    port=int(os.getenv("QDRANT_PORT", 6333))
)

COLLECTION_NAME = "product_knowledge_base"

def filtered_search(query_text, category=None, status=None, price_min=None, price_max=None, top_k=3):
    """
    带多维度过滤的向量搜索
    
    参数说明:
    - category: 产品类别(如 "电脑", "手机")
    - status: 在售状态(如 "在售", "已下架")
    - price_min/price_max: 价格区间
    """
    
    query_vector = model.encode(query_text).tolist()
    
    # 构建过滤条件
    must_conditions = []
    
    if category:
        must_conditions.append(
            FieldCondition(
                key="category",
                match=MatchValue(value=category)
            )
        )
    
    if status:
        must_conditions.append(
            FieldCondition(
                key="status",
                match=MatchValue(value=status)
            )
        )
    
    if price_min is not None or price_max is not None:
        price_range = {}
        if price_min is not None:
            price_range["gte"] = price_min
        if price_max is not None:
            price_range["lte"] = price_max
        must_conditions.append(
            FieldCondition(
                key="price",
                range=price_range
            )
        )
    
    # 只有设置了条件才构造 Filter
    search_filter = None
    if must_conditions:
        search_filter = Filter(must=must_conditions)
    
    # 执行搜索
    results = client.search(
        collection_name=COLLECTION_NAME,
        query_vector=query_vector,
        query_filter=search_filter,
        limit=top_k
    )
    
    print(f"\n🔍 搜索词:{query_text}")
    print(f"📋 过滤条件:category={category}, status={status}, price={price_min}-{price_max}")
    print(f"找到 {len(results)} 条结果:\n")
    
    for i, result in enumerate(results, 1):
        print(f"【结果 {i}】相似度:{result.score:.4f}")
        print(f"  内容:{result.payload['content']}")
        print(f"  分类:{result.payload['category']} | 状态:{result.payload['status']}")
        print(f"  价格:¥{result.payload['price']}\n")

if __name__ == "__main__":
    # 场景1:只搜索电脑类
    filtered_search("续航时间长的设备", category="电脑")
    
    # 场景2:搜索在售的低价产品
    filtered_search("便携设备", status="在售", price_max=2000)
    
    # 场景3:搜索特定类别的在售产品
    filtered_search("手机推荐", category="手机", status="在售")

运行后你会发现:

七、实战三:分面搜索(Faceted Search)

分面搜索是电商、文档管理系统的标配功能——用户能看到各类别的可用筛选器,并实时看到每个分类下有多少结果。Qdrant 的 scroll API 配合聚合可以轻松实现。

7.1 实现分类统计(Facet Count)

from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range, ScrollRequest
from dotenv import load_dotenv
import os
from collections import defaultdict

load_dotenv()

client = QdrantClient(
    host=os.getenv("QDRANT_HOST", "localhost"),
    port=int(os.getenv("QDRANT_PORT", 6333))
)

COLLECTION_NAME = "product_knowledge_base"

def get_facet_counts(status_filter=None, price_range=None):
    """
    获取各分类的文档数量(类似电商的筛选面板)
    返回:{category: count}
    """
    
    must_conditions = []
    
    if status_filter:
        must_conditions.append(
            FieldCondition(
                key="status",
                match=MatchValue(value=status_filter)
            )
        )
    
    if price_range:
        range_dict = {}
        if price_range.get("min") is not None:
            range_dict["gte"] = price_range["min"]
        if price_range.get("max") is not None:
            range_dict["lte"] = price_range["max"]
        must_conditions.append(FieldCondition(key="price", range=range_dict))
    
    search_filter = Filter(must=must_conditions) if must_conditions else None
    
    # 滚动获取所有符合条件的结果
    facet_results = defaultdict(int)
    offset = None
    
    while True:
        response = client.scroll(
            collection_name=COLLECTION_NAME,
            scroll_filter=search_filter,
            offset=offset,
            limit=100
        )
        
        for point in response.points:
            category = point.payload.get("category", "未知")
            facet_results[category] += 1
        
        if not response.next_page_offset:
            break
        offset = response.next_page_offset
    
    return dict(facet_results)

def display_facet_ui():
    """展示分面搜索界面"""
    
    print("=" * 50)
    print("📦 产品分类统计")
    print("=" * 50)
    
    # 所有在售产品
    print("\n【全部在售产品】")
    facets = get_facet_counts(status_filter="在售")
    for cat, count in sorted(facets.items(), key=lambda x: -x[1]):
        print(f"  {cat}: {count} 件")
    
    # 价格区间筛选
    print("\n【¥1500 以下产品】")
    facets = get_facet_counts(price_range={"max": 1500})
    for cat, count in sorted(facets.items(), key=lambda x: -x[1]):
        print(f"  {cat}: {count} 件")
    
    print("\n【¥5000-10000 产品】")
    facets = get_facet_counts(price_range={"min": 5000, "max": 10000})
    for cat, count in sorted(facets.items(), key=lambda x: -x[1]):
        print(f"  {cat}: {count} 件")

if __name__ == "__main__":
    display_facet_ui()

输出效果类似:

==================================================
📦 产品分类统计
==================================================

【全部在售产品】
  电脑: 1 件
  手机: 1 件
  配件: 1 件

【¥1500 以下产品】
  配件: 1 件

【¥5000-10000 产品】
  手机: 1 件

八、实战四:RAG 问答系统集成

现在把过滤搜索集成到完整的 RAG 流程中。创建 rag_qa.py

from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue
from sentence_transformers import SentenceTransformer
from openai import OpenAI
from dotenv import load_dotenv
import os

load_dotenv()

初始化组件

model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

接入 HolySheep API(兼容 OpenAI SDK)

client = OpenAI( api_key=os.getenv("HOLYSHEEP_API_KEY"), base_url=os.getenv("HOLYSHEEP_BASE_URL", "https://api.holysheep.ai/v1") ) qdrant_client = QdrantClient( host=os.getenv("QDRANT_HOST", "localhost"), port=int(os.getenv("QDRANT_PORT", 6333)) ) COLLECTION_NAME = "product_knowledge_base" def rag_ask(question, user_tier="普通用户"): """ 完整的 RAG 问答流程 参数: - question: 用户问题 - user_tier: 用户等级(决定能看到哪些产品) """ # Step 1:根据用户等级决定可见范围 # VIP 用户可以看所有产品,普通用户只能看在售产品 status_filter = None if user_tier == "VIP" else "在售" # Step 2:生成问题向量 query_vector = model.encode(question).tolist() # Step 3:构建过滤条件 must_conditions = [] if status_filter: must_conditions.append( FieldCondition( key="status", match=MatchValue(value=status_filter) ) ) search_filter = Filter(must=must_conditions) if must_conditions else None # Step 4:向量检索 search_results = qdrant_client.search( collection_name=COLLECTION_NAME, query_vector=query_vector, query_filter=search_filter, limit=3 ) # Step 5:构造上下文 context_parts = [] for i, result in enumerate(search_results, 1): context_parts.append( f"[文档{i}] {result.payload['content']} " f"(类别:{result.payload['category']},价格:¥{result.payload['price']})" ) context = "\n\n".join(context_parts) # Step 6:构造 Prompt system_prompt = f"""你是一个专业的产品顾问。请根据提供的参考文档回答用户问题。 参考文档: {context} 要求: 1. 只基于参考文档中的信息回答,不要编造 2. 如果参考文档中没有相关信息,明确告知用户 3. 回答要专业、简洁、有条理""" # Step 7:调用大模型(使用 HolySheep API,性价比极高) response = client.chat.completions.create( model="gpt-4.1", # HolySheep 支持的主流模型 messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": question} ], temperature=0.3, # 降低随机性,保持准确性 max_tokens=500 ) answer = response.choices[0].message.content # 输出结果 print(f"\n{'='*50}") print(f"👤 用户等级:{user_tier}") print(f"❓ 问题:{question}") print(f"{'='*50}") print(f"\n💡 参考文档:") for part in context_parts: print(f" {part}") print(f"\n🤖 AI 回答:\n{answer}") # 显示 Token 消耗(仅作示例,实际应用中应记录日志) print(f"\n📊 Token 消耗:input={response.usage.prompt_tokens}, output={response.usage.completion_tokens}") # HolySheep 价格参考:gpt-4.1 output $8/MTok = $0.008/KTok if __name__ == "__main__": print("\n🎯 场景1:VIP 用户查询(含已下架产品)") rag_ask("推荐一款苹果的便携设备", user_tier="VIP") print("\n" + "="*60) print("\n🎯 场景2:普通用户查询(只看得到在售产品)") rag_ask("推荐一款苹果的便携设备", user_tier="普通用户")

运行 python rag_qa.py,你会看到:

【截图提示:终端输出 RAG 问答结果,显示不同用户等级的差异化回答】

九、常见报错排查

报错1:Qdrant 连接超时 ConnectionTimeout

错误信息qdrant_client.http.exceptions.UnexpectedResponse: Response [ConnectionTimeout]

原因分析:Docker 容器未启动,或端口未正确映射

解决方案

# 1. 检查 Docker 容器状态
docker ps -a | grep qdrant

2. 如果容器未运行,启动它

docker start qdrant

3. 如果容器存在但状态异常,重启

docker restart qdrant

4. 验证服务是否正常

curl http://localhost:6333/health

报错2:向量维度不匹配 WrongVectorSize

错误信息ValueError: vector dimension 384 does not match collection vector size 768

原因分析:创建 Collection 时指定的 VECTOR_SIZE 与实际使用的 embedding 模型输出维度不一致

解决方案

# 检查当前 Collection 的配置
from qdrant_client import QdrantClient
client = QdrantClient(host="localhost", port=6333)
info = client.get_collection("product_knowledge_base")
print(f"Collection 向量维度: {info.vectors_config.distance['cosine'].params.size}")

重新创建 Collection,指定正确的维度

sentence-transformers/all-MiniLM-L6-v2 输出 384 维

如果用的是其他模型,请查文档确认维度

报错3:HolySheep API Key 无效 AuthenticationError

错误信息openai.AuthenticationError: Incorrect API key provided

原因分析:.env 文件中的 API Key 填写错误或已过期

解决方案

# 1. 检查 .env 文件内容
cat .env

确认格式为:HOLYSHEEP_API_KEY=HS-xxxxxxxxxxxxxxxx

2. 在 HolySheep 控制台重新生成 Key(如果怀疑泄露)

控制台地址:https://www.holysheep.ai/register

3. 验证 Key 是否有效(测试调用)

python -c " from openai import OpenAI import os from dotenv import load_dotenv load_dotenv() client = OpenAI(api_key=os.getenv('HOLYSHEEP_API_KEY'), base_url='https://api.holysheep.ai/v1') print(client.models.list().data[0].id) "

报错4:过滤条件不生效 Filter Must Not

错误信息:搜索结果包含了不应该出现的数据(如已下架产品)

原因分析:过滤条件的 key 名称与 payload 中的字段名不一致

解决方案

# 1. 打印一条 point 的 payload,确认字段名
from qdrant_client import QdrantClient
client = QdrantClient(host="localhost", port=6333)
results = client.retrieve(collection_name="product_knowledge_base", ids=[1])
print(results[0].payload)

2. 确认过滤条件的 key 与 payload 字段完全一致(区分大小写!)

错误写法:

FieldCondition(key="Status", match=MatchValue(value="在售")) # ❌ 大小写不匹配

正确写法:

FieldCondition(key="status", match=MatchValue(value="在售")) # ✅ 与 payload 一致

十、实战经验总结与优化建议

回顾整个教程,我再补充几点我在生产环境中的实战经验:

关于成本,我算了一笔账:用 HolySheep API 接入 GPT-4.1,每 1000 次 RAG 问答(约 100K input tokens + 50K output tokens),成本仅 $1.1 左右。相比直接用 OpenAI 官方 API(同场景约 $7.6),节省超过 85%。而且 HolySheep 支持微信/支付宝充值,对于国内开发者来说体验非常友好。

👉 免费注册 HolySheep AI,获取首月赠额度

完整源码下载

本教程所有代码已整理成完整项目,结构如下:

rag-qdrant-tutorial/
├── .env                    # 环境变量配置
├── documents.py            # 文档数据结构
├── qdrant_setup.py         # 数据库初始化
├── ingest.py               # 数据导入脚本
├── search_basic.py         # 基础搜索
├── search_filtered.py      # 过滤搜索
├── search_faceted.py       # 分面搜索
├── rag_qa.py               # 完整 RAG 问答
└── requirements.txt        # 依赖列表

运行前先安装依赖:pip install -r requirements.txt,然后按教程顺序执行各个脚本即可。

如果有任何问题,欢迎在评论区留言,我会尽力解答。觉得有帮助的话,转发给你身边做 AI 应用开发的朋友吧!