未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合具备 Python 基础的读者学习文章向量化、批量写入、语义搜索与结构化过滤,并参考多文件搜索引擎的组织方式,不应将所有片段视为独立可运行脚本或生产方案。依赖清单未限定 search、recommend 等接口的兼容版本范围,端到端章节的“可直接运行”尚缺充分运行条件;FastAPI 示例未列出 fastapi、uvicorn 依赖,也未为其独立内存客户端创建 Collection 和写入数据;写入调优的两次 update_collection 仍使用错误的 optimizer_config 参数。Payload 索引章节与文末对无索引查询是否全表扫描的解释冲突,不能据此前者制定索引策略。默认内存数据随进程结束消失,Payload 索引、HNSW、量化和快照应在相应服务端环境理解与评估。中文示例使用英文为主的模型,输出排名、分数和耗时仅为格式示意;切换模型须匹配向量维度并重新编码语料,模型精度比较也不能视为已验证结论。量化比例仅指理论单元素存储变化,样例语料不是数据库选型依据,相关文档名称只是可选延伸。本次仅作全文结构审阅,未运行代码或进行外部事实核验。 已取消无法确定目标的可点击引用「向量数据库/Qdrant/00-Qdrant学习路线图」,保留原提及;Missing link target: 向量数据库/Qdrant/00-Qdrant学习路线图 已取消无法确定目标的可点击引用「向量数据库/Qdrant/03-核心概念详解」,保留原提及;Missing link target: 向量数据库/Qdrant/03-核心概念详解 已取消无法确定目标的可点击引用「向量数据库/Qdrant/04-安装与部署」,保留原提及;Missing link target: 向量数据库/Qdrant/04-安装与部署 已取消无法确定目标的可点击引用「向量数据库/Milvus/04-安装与部署」,保留原提及;Missing link target: 向量数据库/Milvus/04-安装与部署 已取消无法确定目标的可点击引用「向量数据库/00-向量数据库选型指南」,保留原提及;Missing link target: 向量数据库/00-向量数据库选型指南
第一个 Qdrant 应用:从零构建语义搜索引擎
tip 文档定位
本文是面向初学者的动手实践教程,带你从零构建一个完整的文章语义搜索系统。通过 Python 示例覆盖连接、创建、写入、搜索、过滤的完整操作链路;部分片段依赖前文初始化,FastAPI 集成及性能调优片段还需结合维护说明补齐运行条件,不能视为全部可直接运行。
目录
- 应用概述与目标
- 环境准备
- 连接 Qdrant
- 创建 Collection
- 数据准备与向量化
- 批量插入数据
- 基础向量搜索
- 带 Payload 过滤的搜索
- 异步操作模式
- 完整端到端示例:文章语义搜索系统
- 常见操作速查
- 性能调优建议
应用概述与目标
我们要构建什么
本文将从零构建一个文章语义搜索引擎。给定一段自然语言查询(如"如何优化 Spark 性能"),系统能够从文章库中找到语义最相近的内容,即使文章中没有出现查询中的关键词。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph LR
classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
A[原始文章文本] --> B[sentence-transformers<br/>向量化模型]
B --> C[384维浮点向量]
C --> D[Qdrant Collection<br/>存储向量+Payload]
E[用户输入查询] --> F[同一向量化模型]
F --> G[查询向量]
G --> H[Qdrant 向量搜索<br/>余弦相似度]
H --> I[Top-K 相关文章]
D --> H
class A,B,C,D,E,F,G,H,I default
技术选型说明
| 组件 | 选型 | 原因 |
|---|---|---|
| 向量数据库 | Qdrant(本地内嵌模式) | 零配置,适合教学 |
| 向量化模型 | all-MiniLM-L6-v2 | 384维,体积小,精度好 |
| Python 客户端 | qdrant-client 1.x | 官方维护,API 最完整 |
| 距离度量 | Cosine(余弦相似度) | 文本语义匹配的标准选择 |
note 为什么选择语义搜索而不是关键词搜索?
关键词搜索(如 Elasticsearch 的 BM25)依赖词语的字面匹配。语义搜索通过向量空间中的距离衡量含义的接近程度。例如查询"服务器挂了",语义搜索可能匹配包含"节点宕机"、"实例不可用"的文章,效果取决于模型和语料;仅依赖字面词项的关键词搜索可能漏检,但配置同义词或查询扩展后也可能匹配这些表达。
环境准备
系统要求
Python 版本: 3.8+(推荐 3.11+)
内存: 最低 2GB(模型加载约占 100~500MB)
磁盘: 最低 500MB(模型下载)
网络: 首次运行需要下载模型(约 90MB)
创建虚拟环境
# 创建项目目录
mkdir qdrant-semantic-search
cd qdrant-semantic-search
# 创建虚拟环境(推荐,避免依赖污染)
python -m venv .venv
# 激活虚拟环境
# macOS / Linux:
source .venv/bin/activate
# Windows:
.venv\Scripts\activate
# 验证 Python 环境
python --version
which python # macOS/Linux
安装依赖
# 方式一:最小化安装(推荐入门)
pip install qdrant-client sentence-transformers
# 方式二:仅供使用 FastEmbed 的其他实现;本文后续代码使用 SentenceTransformer,仍需安装 sentence-transformers
pip install "qdrant-client[fastembed]"
# 方式三:完整安装(异步 + FastEmbed + gRPC)
pip install "qdrant-client[fastembed,async]" sentence-transformers numpy
# 验证安装
python -c "from importlib.metadata import version; print('qdrant-client:', version('qdrant-client'))"
python -c "from sentence_transformers import SentenceTransformer; print('sentence-transformers: OK')"
创建 requirements.txt
# requirements.txt
qdrant-client>=1.7.0
sentence-transformers>=2.7.0
numpy>=1.24.0
# 根据 requirements.txt 安装
pip install -r requirements.txt
warning 首次运行下载模型
第一次使用 SentenceTransformer("all-MiniLM-L6-v2") 时,会自动从 Hugging Face 下载约 90MB 的模型文件。国内环境可能需要设置镜像代理:
export HF_ENDPOINT=https://hf-mirror.com
连接 Qdrant
Qdrant 支持三种连接方式,开发和生产场景各有不同的最优选择。
方式一:内存模式(最简单)
# connect_memory.py
from qdrant_client import QdrantClient
# 内存模式:无需任何服务端,数据存在内存中
# 进程结束后数据销毁
client = QdrantClient(":memory:")
print("内存模式连接成功")
from importlib.metadata import version
print(f"客户端版本: {version('qdrant-client')}")
# 验证:列出所有 Collection
collections = client.get_collections()
print(f"当前 Collection 数量: {len(collections.collections)}")
方式二:本地 Docker 服务
# connect_local.py
from qdrant_client import QdrantClient
# 连接本地 Docker 运行的 Qdrant 实例
# 前提:docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
client = QdrantClient(
host="localhost",
port=6333,
# 若启用了 gRPC,可以使用以下参数提升性能
# prefer_grpc=True,
# grpc_port=6334,
timeout=10, # 连接超时(秒)
)
# 也可以使用 URL 方式
# client = QdrantClient(url="http://localhost:6333")
try:
# 健康检查
result = client.get_collections()
print(f"本地 Qdrant 连接成功")
print(f"当前 Collection 数: {len(result.collections)}")
except Exception as e:
print(f"连接失败,请检查 Qdrant 是否已启动: {e}")
print("启动命令: docker run -p 6333:6333 qdrant/qdrant")
方式三:Qdrant Cloud 托管服务
# connect_cloud.py
from qdrant_client import QdrantClient
import os
# 从环境变量读取凭证(安全实践,不硬编码密钥)
QDRANT_URL = os.getenv("QDRANT_URL", "https://your-cluster.cloud.qdrant.io")
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY", "your-api-key-here")
client = QdrantClient(
url=QDRANT_URL,
api_key=QDRANT_API_KEY,
timeout=30, # Cloud 网络延迟稍大
prefer_grpc=True, # gRPC 在 Cloud 场景性能更好
)
try:
info = client.get_collections()
print(f"Qdrant Cloud 连接成功: {QDRANT_URL}")
print(f"当前 Collection 数: {len(info.collections)}")
except Exception as e:
print(f"Cloud 连接失败: {e}")
连接器工厂函数(推荐封装方式)
# qdrant_helper.py
from qdrant_client import QdrantClient
from typing import Optional
import os
def get_qdrant_client(
mode: str = "memory",
host: str = "localhost",
port: int = 6333,
url: Optional[str] = None,
api_key: Optional[str] = None,
local_path: Optional[str] = None,
) -> QdrantClient:
"""
工厂函数:根据模式创建 Qdrant 客户端
Args:
mode: "memory" | "local" | "cloud" | "file"
host: 本地服务 host(mode="local")
port: 本地服务端口(mode="local")
url: 完整 URL(mode="cloud")
api_key: API Key(mode="cloud" 或本地启用认证时)
local_path: 本地文件路径(mode="file")
Returns:
已初始化的 QdrantClient 实例
"""
if mode == "memory":
client = QdrantClient(":memory:")
print("[Qdrant] 内存模式就绪")
elif mode == "file":
path = local_path or "./qdrant_data"
client = QdrantClient(path=path)
print(f"[Qdrant] 文件模式就绪: {path}")
elif mode == "local":
client = QdrantClient(host=host, port=port, api_key=api_key)
print(f"[Qdrant] 本地服务连接: http://{host}:{port}")
elif mode == "cloud":
client = QdrantClient(
url=url or os.getenv("QDRANT_URL"),
api_key=api_key or os.getenv("QDRANT_API_KEY"),
timeout=30,
prefer_grpc=True,
)
print(f"[Qdrant] Cloud 连接: {url}")
else:
raise ValueError(f"不支持的连接模式: {mode}")
return client
# 使用示例
if __name__ == "__main__":
# 开发环境使用内存模式
client = get_qdrant_client(mode="memory")
创建 Collection
Collection 是 Qdrant 中存储向量的基本单元,类似关系型数据库的"表"。创建时需要指定向量的维度和距离度量方式。
基础 Collection 创建
# create_collection.py
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams,
Distance,
)
client = QdrantClient(":memory:")
COLLECTION_NAME = "articles"
VECTOR_SIZE = 384 # all-MiniLM-L6-v2 输出维度
DISTANCE = Distance.COSINE # 文本语义搜索推荐使用余弦相似度
# 创建 Collection
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(
size=VECTOR_SIZE,
distance=DISTANCE,
),
)
# 验证创建结果
collection_info = client.get_collection(COLLECTION_NAME)
print(f"Collection '{COLLECTION_NAME}' 创建成功")
print(f" 向量维度: {collection_info.config.params.vectors.size}")
print(f" 距离度量: {collection_info.config.params.vectors.distance}")
print(f" 当前向量数: {collection_info.points_count}")
vectors_config 参数详解
# vectors_config_explained.py
from qdrant_client.models import VectorParams, Distance, HnswConfigDiff, QuantizationConfig, ScalarQuantization, ScalarType
# 完整配置示例(生产环境推荐)
client.create_collection(
collection_name="articles_advanced",
vectors_config=VectorParams(
# ====================================================
# 必填参数
# ====================================================
# 向量维度(必须与实际嵌入模型输出维度一致)
# 常见维度: 384(MiniLM)、768(BERT)、1536(OpenAI small)
size=384,
# 距离度量方式
# Distance.COSINE — 余弦相似度,文本/图像最常用
# Distance.EUCLID — 欧式距离,坐标/回归场景
# Distance.DOT — 点积,已归一化的向量(更快)
# Distance.MANHATTAN — 曼哈顿距离
distance=Distance.COSINE,
# ====================================================
# 可选参数
# ====================================================
# 是否将向量存储到磁盘(降低内存占用,增加查询延迟)
# on_disk=False, # 默认 False(内存中)
# HNSW 索引配置(覆盖全局默认值)
# hnsw_config=HnswConfigDiff(
# m=16, # 节点连接数(精度/内存权衡)
# ef_construct=100, # 构建时候选集
# full_scan_threshold=10000, # 小数据集暴力扫描阈值
# on_disk=False, # HNSW 图是否存磁盘
# ),
# 量化配置(压缩内存,可能略降低精度)
# quantization_config=ScalarQuantization(
# scalar=ScalarQuantizationConfig(
# type=ScalarType.INT8, # float32 转 int8 的理论单元素存储减少 75%,不代表总内存减少 75%
# quantile=0.99,
# always_ram=True, # 量化后的向量保持在内存中
# )
# ),
),
)
多向量 Collection(高级)
# multi_vector_collection.py
from qdrant_client.models import VectorParams, Distance
# 同一个 Collection 支持多个向量字段(如:文本向量 + 图像向量)
client.create_collection(
collection_name="multimodal_articles",
vectors_config={
# 文本语义向量
"text": VectorParams(size=384, distance=Distance.COSINE),
# 图像向量(如 CLIP 模型输出)
"image": VectorParams(size=512, distance=Distance.COSINE),
# 标题向量(单独建索引,提升标题匹配准确性)
"title": VectorParams(size=384, distance=Distance.COSINE),
},
)
print("多向量 Collection 创建成功")
删除和重建 Collection
# manage_collection.py
from qdrant_client.models import VectorParams, Distance
COLLECTION_NAME = "articles"
# 检查 Collection 是否存在
def collection_exists(client, name: str) -> bool:
collections = client.get_collections().collections
return any(c.name == name for c in collections)
# 破坏性重建(先删除再创建,会删除已有数据;仅用于可丢弃的练习数据或已备份场景)
def recreate_collection(client, name: str, vector_size: int):
if collection_exists(client, name):
print(f"删除已有 Collection: {name}")
client.delete_collection(name)
client.create_collection(
collection_name=name,
vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE),
)
print(f"Collection '{name}' 已重建")
# 使用示例
client = QdrantClient(":memory:")
recreate_collection(client, COLLECTION_NAME, vector_size=384)
tip Collection 命名建议
- 使用小写字母和下划线:
article_vectors,user_embeddings - 避免使用空格和特殊字符
- 名称应体现业务含义,便于维护
数据准备与向量化
将文本数据转换为向量是整个流程的核心步骤。这里使用 sentence-transformers 库中的 all-MiniLM-L6-v2 模型。
加载向量化模型
# embedding_model.py
from sentence_transformers import SentenceTransformer
import numpy as np
import time
# 加载模型(首次运行会自动下载,约 90MB)
print("正在加载向量化模型...")
start = time.time()
model = SentenceTransformer("all-MiniLM-L6-v2")
print(f"模型加载完成,耗时: {time.time() - start:.1f}s")
print(f"向量维度: {model.get_sentence_embedding_dimension()}") # 输出: 384
# 单条文本向量化测试
text = "Apache Spark 是一个分布式计算框架"
vector = model.encode(text)
print(f"文本: {text}")
print(f"向量类型: {type(vector)}, 形状: {vector.shape}, 数据类型: {vector.dtype}")
# 输出: 向量类型: <class 'numpy.ndarray'>, 形状: (384,), 数据类型: float32
常用向量化模型对比
| 模型名称 | 维度 | 模型大小 | 速度 | 多语言 | 推荐场景 |
|---|---|---|---|---|---|
| all-MiniLM-L6-v2 | 384 | 80MB | 极快 | 否(英文为主) | 入门、英文内容 |
| all-mpnet-base-v2 | 768 | 420MB | 中等 | 否(英文为主) | 英文高精度 |
| paraphrase-multilingual-MiniLM-L12-v2 | 384 | 470MB | 快 | 是(50+语言) | 中文/多语言 |
| text2vec-base-chinese | 768 | 400MB | 中等 | 中文 | 中文专用 |
| BAAI/bge-small-zh-v1.5 | 512 | 93MB | 快 | 中文 | 中文高性价比 |
tip 中文内容推荐
对于中文知识库,推荐使用 paraphrase-multilingual-MiniLM-L12-v2 或 BAAI/bge-small-zh-v1.5。前者多语言支持好,后者中文专项优化,精度更高。
# 中文专用模型加载
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
# 注意: BGE 模型在查询时需要加前缀 "为这个句子生成表示以用于检索相关文章:"
query = "为这个句子生成表示以用于检索相关文章:如何优化 Spark 性能"
批量向量化
# batch_encoding.py
from sentence_transformers import SentenceTransformer
import numpy as np
import time
model = SentenceTransformer("all-MiniLM-L6-v2")
# 示例文章数据
articles = [
"Apache Spark 是一个用于大规模数据处理的开源分布式计算框架",
"Kafka 是一个分布式事件流平台,广泛用于实时数据管道",
"Flink 提供有状态的流处理,支持事件时间和精确一次语义",
"Qdrant 是一个高性能向量数据库,专为 AI 应用设计",
"数据湖架构允许以原始格式存储大量结构化和非结构化数据",
"HDFS 是 Hadoop 生态系统中的分布式文件系统",
"HBase 是基于 Hadoop 的分布式 NoSQL 数据库",
"Hive 提供基于 Hadoop 的数据仓库功能,支持 HiveQL 查询",
"Presto 是 Facebook 开发的分布式 SQL 查询引擎",
"ClickHouse 是一个高性能列式数据库,适合 OLAP 场景",
]
# 批量编码(自动使用 GPU 如果可用)
print(f"开始向量化 {len(articles)} 篇文章...")
start = time.time()
vectors = model.encode(
articles,
batch_size=32, # 批处理大小,根据内存调整
show_progress_bar=True, # 显示进度条
normalize_embeddings=True, # 归一化(使用余弦相似度时推荐)
convert_to_numpy=True, # 返回 numpy 数组
)
print(f"向量化完成,耗时: {time.time() - start:.2f}s")
print(f"输出形状: {vectors.shape}") # (10, 384)
print(f"内存占用: {vectors.nbytes / 1024:.1f} KB")
print(f"向量范数(归一化后应为 1.0): {np.linalg.norm(vectors[0]):.4f}")
批量插入数据
向量化完成后,将向量和元数据(Payload)一起存入 Qdrant。
基础 upsert 操作
# upsert_basic.py
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, VectorParams, Distance
from sentence_transformers import SentenceTransformer
import uuid
client = QdrantClient(":memory:")
model = SentenceTransformer("all-MiniLM-L6-v2")
# 创建 Collection
client.create_collection(
collection_name="articles",
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)
# 准备数据
articles = [
{
"id": 1,
"title": "Spark 性能调优实战",
"content": "Apache Spark 是一个用于大规模数据处理的分布式计算框架",
"category": "大数据框架",
"author": "张工",
"views": 1500,
"tags": ["Spark", "性能优化", "大数据"],
},
{
"id": 2,
"title": "Kafka 消息队列详解",
"content": "Kafka 是一个分布式事件流平台,广泛用于实时数据管道和消息传递",
"category": "消息队列",
"author": "李工",
"views": 2300,
"tags": ["Kafka", "消息队列", "实时处理"],
},
{
"id": 3,
"title": "Flink 流处理入门",
"content": "Flink 提供有状态的流处理,支持事件时间语义和精确一次处理语义",
"category": "流处理",
"author": "王工",
"views": 1800,
"tags": ["Flink", "流处理", "实时计算"],
},
]
# 向量化文章内容
contents = [a["content"] for a in articles]
vectors = model.encode(contents, normalize_embeddings=True)
# 构建 PointStruct 列表
points = [
PointStruct(
id=article["id"], # 支持整数 ID 或 UUID
vector=vector.tolist(), # numpy array 转换为 list
payload={ # 存储任意 JSON 兼容的元数据
"title": article["title"],
"content": article["content"],
"category": article["category"],
"author": article["author"],
"views": article["views"],
"tags": article["tags"],
},
)
for article, vector in zip(articles, vectors)
]
# 执行 upsert(insert + update 的合并操作)
operation_info = client.upsert(
collection_name="articles",
points=points,
wait=True, # 等待操作完成(确保写后读一致性)
)
print(f"upsert 完成")
print(f"操作状态: {operation_info.status}")
print(f"当前 Collection 向量数: {client.get_collection('articles').points_count}")
大批量数据写入
# upsert_batch.py
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, VectorParams, Distance
from sentence_transformers import SentenceTransformer
import numpy as np
import time
from typing import List, Dict, Any
def batch_upsert(
client: QdrantClient,
collection_name: str,
data: List[Dict[str, Any]],
model: SentenceTransformer,
text_field: str = "content",
batch_size: int = 100,
id_field: str = "id",
) -> int:
"""
批量 upsert 向量数据,自动分批处理
Args:
client: Qdrant 客户端
collection_name: 目标 Collection 名称
data: 原始数据列表
model: 向量化模型
text_field: 用于向量化的文本字段名
batch_size: 每批处理的数量(推荐 64~256)
id_field: 作为 Point ID 的字段名
Returns:
成功写入的 Point 数量
"""
total = len(data)
inserted = 0
print(f"开始批量写入 {total} 条数据,批大小: {batch_size}")
start_time = time.time()
for batch_start in range(0, total, batch_size):
batch = data[batch_start: batch_start + batch_size]
# 批量向量化
texts = [item[text_field] for item in batch]
vectors = model.encode(texts, normalize_embeddings=True, show_progress_bar=False)
# 构建 Points
points = []
for item, vector in zip(batch, vectors):
# 构建 Payload(排除 ID 字段和向量字段)
payload = {k: v for k, v in item.items() if k != id_field}
points.append(PointStruct(
id=item[id_field],
vector=vector.tolist(),
payload=payload,
))
# 执行 upsert
client.upsert(
collection_name=collection_name,
points=points,
wait=True,
)
inserted += len(batch)
elapsed = time.time() - start_time
speed = inserted / elapsed
eta = (total - inserted) / speed if speed > 0 else 0
print(f" 进度: {inserted}/{total} ({100*inserted/total:.1f}%) "
f"| 速度: {speed:.0f} 条/秒 "
f"| 预计剩余: {eta:.0f}s")
total_time = time.time() - start_time
print(f"\n批量写入完成!共 {inserted} 条,总耗时: {total_time:.1f}s")
return inserted
# 生成测试数据
def generate_test_data(n: int) -> List[Dict[str, Any]]:
"""生成 n 条测试文章数据"""
categories = ["大数据框架", "消息队列", "流处理", "数据仓库", "机器学习"]
topics = [
"Spark 分布式计算原理与实践",
"Kafka 消息队列高吞吐量设计",
"Flink 实时流处理架构",
"Hive 数据仓库查询优化",
"机器学习模型部署最佳实践",
"数据湖与数据仓库架构选型",
"ClickHouse OLAP 性能调优",
"Kubernetes 大数据集群管理",
"数据质量监控与治理体系",
"向量数据库在推荐系统中的应用",
]
data = []
for i in range(n):
topic = topics[i % len(topics)]
category = categories[i % len(categories)]
data.append({
"id": i + 1,
"title": f"{topic} - 第{i+1}篇",
"content": f"本文详细介绍{topic},涵盖核心概念、架构设计和最佳实践。",
"category": category,
"author": f"作者_{i % 5 + 1}",
"views": (i * 137 + 500) % 5000,
"publish_year": 2023 + (i % 3),
"tags": [category, topic.split(" ")[0]],
})
return data
# 使用示例
if __name__ == "__main__":
client = QdrantClient(":memory:")
model = SentenceTransformer("all-MiniLM-L6-v2")
client.create_collection(
collection_name="articles",
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)
# 生成 500 条测试数据并批量写入
test_data = generate_test_data(500)
batch_upsert(
client=client,
collection_name="articles",
data=test_data,
model=model,
text_field="content",
batch_size=100,
)
# 验证写入结果
info = client.get_collection("articles")
print(f"\nCollection 状态:")
print(f" 向量数: {info.points_count}")
print(f" 索引状态: {info.status}")
Point ID 类型说明
# point_id_types.py
from qdrant_client.models import PointStruct
import uuid
# Qdrant 支持两种 ID 类型:
# 类型 1:unsigned int(无符号整数)
# 适合:数据库中有自增 ID 的场景
point_int_id = PointStruct(
id=42, # 非负整数
vector=[0.1, 0.2, 0.3],
payload={"title": "整数 ID 示例"},
)
# 类型 2:UUID 字符串
# 适合:分布式系统、没有统一 ID 序列的场景
point_uuid = PointStruct(
id=str(uuid.uuid4()), # 标准 UUID 字符串
vector=[0.1, 0.2, 0.3],
payload={"title": "UUID 示例"},
)
# 注意:同一个 Collection 内可以同时使用无符号整数 ID 和 UUID
# 应保持业务记录与 Point ID 的映射稳定,避免重复 ID 覆盖已有数据
基础向量搜索
数据写入完成后,可以执行向量相似度搜索。Qdrant 的 search 方法返回与查询向量最相似的 Top-K 个结果。
基础搜索
# basic_search.py
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, VectorParams, Distance, ScoredPoint
from sentence_transformers import SentenceTransformer
from typing import List
# (假设 client 和 model 已初始化,Collection 已写入数据)
# 参考上面的批量写入示例完成准备工作
def semantic_search(
client: QdrantClient,
model: SentenceTransformer,
query: str,
collection_name: str = "articles",
top_k: int = 5,
) -> List[ScoredPoint]:
"""
执行语义搜索
Args:
query: 自然语言查询文本
top_k: 返回最相似结果的数量
Returns:
ScoredPoint 列表,按相似度降序排列
"""
# 将查询文本转换为向量
query_vector = model.encode(query, normalize_embeddings=True).tolist()
# 执行向量搜索
results = client.search(
collection_name=collection_name,
query_vector=query_vector,
limit=top_k, # 返回 Top-K 结果
with_payload=True, # 返回 Payload 数据(search 默认 True)
with_vectors=False, # 不返回向量本身(节省带宽)
score_threshold=0.3, # 示例最低相似度阈值;余弦相似度范围为 -1~1,阈值需按语料评估
)
return results
def print_search_results(results: List[ScoredPoint], query: str):
"""格式化打印搜索结果"""
print(f"\n查询: 「{query}」")
print(f"找到 {len(results)} 条结果:")
print("-" * 60)
for rank, hit in enumerate(results, 1):
print(f"#{rank} 相似度: {hit.score:.4f}")
if hit.payload:
print(f" 标题: {hit.payload.get('title', 'N/A')}")
print(f" 分类: {hit.payload.get('category', 'N/A')}")
print(f" 内容: {hit.payload.get('content', '')[:80]}...")
print()
# 执行不同类型的查询
queries = [
"分布式计算框架性能优化",
"实时数据流处理",
"消息传递中间件",
"数据仓库查询加速",
]
for query in queries:
results = semantic_search(client, model, query, top_k=3)
print_search_results(results, query)
搜索参数详解
# search_params.py
from qdrant_client.models import SearchParams
# 完整搜索参数示例
results = client.search(
collection_name="articles",
query_vector=query_vector,
# ====================================================
# 结果控制参数
# ====================================================
limit=10, # 最多返回多少条(默认 10)
offset=0, # 分页偏移量(用于翻页)
score_threshold=0.0, # 示例最低分数阈值;余弦相似度范围为 -1~1
# ====================================================
# 数据返回参数
# ====================================================
with_payload=True, # 返回完整 Payload
# 也可以只返回特定字段:
# with_payload=["title", "category"],
# 或使用 PayloadSelectorInclude:
# with_payload={"include": ["title", "category"]},
# with_payload={"exclude": ["content"]}, # 排除大字段
with_vectors=False, # 返回向量数据(默认 False,节省带宽)
# ====================================================
# HNSW 搜索精度控制
# ====================================================
search_params=SearchParams(
# ef 参数:搜索时的候选集大小
# 越大精度越高,速度越慢
# 可从 64~512 试调;未指定时使用服务端默认搜索配置,不应将其等同于构建参数 ef_construct
hnsw_ef=128,
# 是否精确搜索(暴力扫描,精度 100% 但很慢)
exact=False,
# 量化搜索策略(若 Collection 启用了量化)
# quantization=QuantizationSearchParams(
# ignore=False,
# rescore=True,
# oversampling=2.0,
# ),
),
)
通过 ID 获取 Point
# get_by_id.py
# 获取单个 Point
point = client.retrieve(
collection_name="articles",
ids=[1], # 支持整数 ID 列表
with_payload=True,
with_vectors=False,
)
print(f"Point ID: {point[0].id}")
print(f"Payload: {point[0].payload}")
# 批量获取多个 Point
points = client.retrieve(
collection_name="articles",
ids=[1, 2, 3, 4, 5],
with_payload=True,
)
print(f"获取到 {len(points)} 个 Points")
带 Payload 过滤的搜索
Qdrant 的强大之处在于支持向量搜索与结构化过滤的组合查询。过滤在 ANN(近似最近邻)计算之前或之后应用,大幅缩小搜索空间,精度和速度兼顾。
基础过滤搜索
# filter_search.py
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range
# 过滤条件 1:精确匹配(category == "大数据框架")
category_filter = Filter(
must=[
FieldCondition(
key="category",
match=MatchValue(value="大数据框架"),
)
]
)
results = client.search(
collection_name="articles",
query_vector=query_vector,
query_filter=category_filter, # 应用过滤条件
limit=5,
with_payload=True,
)
print("过滤条件:category == '大数据框架'")
for hit in results:
print(f" [{hit.score:.3f}] {hit.payload['title']} | {hit.payload['category']}")
过滤条件全览
# filter_types.py
from qdrant_client.models import (
Filter,
FieldCondition,
MatchValue, # 精确匹配
MatchAny, # 多值匹配(IN)
MatchText, # 全文包含匹配
Range, # 数值范围
DatetimeRange, # 时间范围
GeoBoundingBox, # 地理围栏
GeoRadius, # 地理半径
IsNullCondition, # 字段为 null
IsEmptyCondition, # 字段为空
HasIdCondition, # 按 ID 过滤
NestedCondition, # 嵌套条件
FilterSelector, # 过滤选择器
)
# ============================================================
# 单条件过滤
# ============================================================
# 精确匹配字符串
exact_filter = Filter(must=[
FieldCondition(key="category", match=MatchValue(value="大数据框架"))
])
# 多值匹配(相当于 SQL IN)
any_filter = Filter(must=[
FieldCondition(key="category", match=MatchAny(any=["大数据框架", "流处理"]))
])
# 数值范围过滤(views >= 1000 AND views <= 3000)
range_filter = Filter(must=[
FieldCondition(
key="views",
range=Range(gte=1000, lte=3000)
# gte: >= , gt: > , lte: <= , lt: <
)
])
# 文本包含匹配(content 包含 "Spark")
text_filter = Filter(must=[
FieldCondition(key="content", match=MatchText(text="Spark"))
])
# 年份过滤(publish_year >= 2024)
year_filter = Filter(must=[
FieldCondition(key="publish_year", range=Range(gte=2024))
])
# ============================================================
# 复合条件过滤
# ============================================================
# must:所有条件必须满足(AND)
and_filter = Filter(
must=[
FieldCondition(key="category", match=MatchValue(value="大数据框架")),
FieldCondition(key="views", range=Range(gte=1000)),
]
)
# should:至少满足一个条件(OR)
or_filter = Filter(
should=[
FieldCondition(key="category", match=MatchValue(value="大数据框架")),
FieldCondition(key="category", match=MatchValue(value="流处理")),
]
)
# must_not:不满足任何条件(NOT)
not_filter = Filter(
must_not=[
FieldCondition(key="category", match=MatchValue(value="机器学习")),
]
)
# 复杂组合:(category IN ['大数据框架','流处理']) AND (views >= 500) AND NOT (author == '作者_1')
complex_filter = Filter(
must=[
FieldCondition(
key="category",
match=MatchAny(any=["大数据框架", "流处理"])
),
FieldCondition(key="views", range=Range(gte=500)),
],
must_not=[
FieldCondition(key="author", match=MatchValue(value="作者_1")),
]
)
# ============================================================
# 数组字段过滤(tags 包含 "Spark")
# ============================================================
tag_filter = Filter(must=[
FieldCondition(
key="tags", # 数组类型字段
match=MatchValue(value="Spark") # 数组中包含该值
)
])
为 Payload 字段建立索引
# payload_index.py
from qdrant_client.models import PayloadSchemaType
# 为经常用于过滤的字段建立索引,大幅提升过滤查询性能
# 建议:在大量数据写入前建立索引
client.create_payload_index(
collection_name="articles",
field_name="category",
field_schema=PayloadSchemaType.KEYWORD, # 字符串精确匹配
)
client.create_payload_index(
collection_name="articles",
field_name="views",
field_schema=PayloadSchemaType.INTEGER, # 整数范围查询
)
client.create_payload_index(
collection_name="articles",
field_name="publish_year",
field_schema=PayloadSchemaType.INTEGER,
)
client.create_payload_index(
collection_name="articles",
field_name="tags",
field_schema=PayloadSchemaType.KEYWORD, # 数组关键词索引
)
# 全文索引(用于 MatchText 过滤)
client.create_payload_index(
collection_name="articles",
field_name="content",
field_schema=PayloadSchemaType.TEXT, # 全文索引
)
print("Payload 索引创建完成")
warning 什么时候需要 Payload 索引?
- 数据量 > 10,000 条时,过滤字段建议加索引
- 没有索引时,Qdrant 会暴力扫描所有 Points 的 Payload,性能下降明显
- 建索引会略微增加内存消耗,是空间换时间的经典权衡
完整过滤搜索示例
# filter_search_complete.py
def filtered_search(
client,
model,
query: str,
category: str = None,
min_views: int = None,
max_views: int = None,
author: str = None,
tags: list = None,
top_k: int = 5,
) -> list:
"""
带多维度过滤条件的语义搜索
Args:
query: 搜索关键词
category: 限定分类(精确匹配)
min_views: 最低阅读量
max_views: 最高阅读量
author: 限定作者
tags: 限定标签列表(OR 关系)
top_k: 返回结果数量
"""
from qdrant_client.models import Filter, FieldCondition, MatchValue, MatchAny, Range
# 动态构建过滤条件
must_conditions = []
should_conditions = []
if category:
must_conditions.append(
FieldCondition(key="category", match=MatchValue(value=category))
)
if min_views is not None or max_views is not None:
range_params = {}
if min_views is not None:
range_params["gte"] = min_views
if max_views is not None:
range_params["lte"] = max_views
must_conditions.append(
FieldCondition(key="views", range=Range(**range_params))
)
if author:
must_conditions.append(
FieldCondition(key="author", match=MatchValue(value=author))
)
if tags:
# 任一标签匹配即可(OR)
for tag in tags:
should_conditions.append(
FieldCondition(key="tags", match=MatchValue(value=tag))
)
# 构建最终过滤器(无条件时为 None)
query_filter = None
if must_conditions or should_conditions:
query_filter = Filter(
must=must_conditions if must_conditions else None,
should=should_conditions if should_conditions else None,
)
# 执行搜索
query_vector = model.encode(query, normalize_embeddings=True).tolist()
results = client.search(
collection_name="articles",
query_vector=query_vector,
query_filter=query_filter,
limit=top_k,
with_payload=True,
score_threshold=0.2,
)
return results
# 使用示例
results = filtered_search(
client, model,
query="分布式计算性能优化",
category="大数据框架",
min_views=500,
)
print(f"\n过滤搜索结果(分类=大数据框架,阅读量>=500):")
for hit in results:
print(f" [{hit.score:.3f}] {hit.payload['title']} | 阅读:{hit.payload['views']}")
滚动翻页(Scroll)
# scroll_example.py
from qdrant_client.models import Filter, FieldCondition, MatchValue
# Scroll 用于遍历满足条件的所有 Points,不基于向量相似度
# 常用场景:数据导出、批量处理、数据统计
# 翻页遍历所有"大数据框架"分类的文章
offset = None # None 表示从头开始
page = 0
all_points = []
while True:
results, next_offset = client.scroll(
collection_name="articles",
scroll_filter=Filter(must=[
FieldCondition(key="category", match=MatchValue(value="大数据框架"))
]),
limit=10, # 每页 10 条
offset=offset, # 当前页起始位置
with_payload=True,
with_vectors=False,
)
all_points.extend(results)
page += 1
print(f"第 {page} 页:获取 {len(results)} 条,next_offset: {next_offset}")
if next_offset is None: # None 表示已到最后一页
break
offset = next_offset # 更新偏移量
print(f"\n共遍历 {len(all_points)} 条「大数据框架」分类文章")
异步操作模式
Qdrant Python 客户端从 v1.6.1 开始提供完整的异步 API,适合高并发 Web 服务场景(如 FastAPI)。
异步客户端基础
# async_client.py
import asyncio
from qdrant_client import AsyncQdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
async def main():
# 创建异步客户端
client = AsyncQdrantClient(":memory:")
# 异步创建 Collection
await client.create_collection(
collection_name="async_articles",
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)
# 异步 upsert
await client.upsert(
collection_name="async_articles",
points=[
PointStruct(id=1, vector=[0.1] * 384, payload={"title": "异步示例"}),
PointStruct(id=2, vector=[0.2] * 384, payload={"title": "并发搜索"}),
],
wait=True,
)
# 异步搜索
results = await client.search(
collection_name="async_articles",
query_vector=[0.15] * 384,
limit=2,
with_payload=True,
)
print(f"异步搜索结果: {len(results)} 条")
for hit in results:
print(f" [{hit.score:.4f}] {hit.payload['title']}")
# 关闭连接
await client.close()
# 在 Jupyter Notebook 中使用 await 直接调用
# await main()
# 普通 Python 脚本中使用 asyncio.run
if __name__ == "__main__":
asyncio.run(main())
与 FastAPI 集成
# fastapi_example.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from qdrant_client import AsyncQdrantClient
from qdrant_client.models import VectorParams, Distance, Filter, FieldCondition, MatchValue
from sentence_transformers import SentenceTransformer
from typing import List, Optional
import asyncio
app = FastAPI(title="语义搜索 API", version="1.0.0")
# 全局初始化(实际项目中使用 lifespan 事件管理)
qdrant_client = AsyncQdrantClient(":memory:")
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
COLLECTION = "articles"
class SearchRequest(BaseModel):
query: str
top_k: int = 5
category: Optional[str] = None
min_score: float = 0.0
class SearchResult(BaseModel):
id: int
score: float
title: str
category: str
content: str
@app.get("/health")
async def health():
return {"status": "ok", "collection": COLLECTION}
@app.post("/search", response_model=List[SearchResult])
async def search(request: SearchRequest):
"""执行语义搜索"""
# 向量化(在线程池中运行,避免阻塞事件循环)
loop = asyncio.get_event_loop()
query_vector = await loop.run_in_executor(
None,
lambda: embedding_model.encode(request.query, normalize_embeddings=True).tolist()
)
# 构建过滤条件
query_filter = None
if request.category:
query_filter = Filter(must=[
FieldCondition(key="category", match=MatchValue(value=request.category))
])
# 执行搜索
results = await qdrant_client.search(
collection_name=COLLECTION,
query_vector=query_vector,
query_filter=query_filter,
limit=request.top_k,
with_payload=True,
score_threshold=request.min_score,
)
if not results:
return []
return [
SearchResult(
id=hit.id,
score=round(hit.score, 4),
title=hit.payload.get("title", ""),
category=hit.payload.get("category", ""),
content=hit.payload.get("content", "")[:200],
)
for hit in results
]
# 启动: uvicorn fastapi_example:app --reload
完整端到端示例:文章语义搜索系统
将前面所有知识整合,构建一个完整的、可直接运行的文章语义搜索系统。
项目结构
| 文件名 | 说明 |
|---|---|
requirements.txt |
依赖清单 |
config.py |
配置管理 |
data_loader.py |
数据加载与处理 |
search_engine.py |
搜索引擎核心类 |
main.py |
主程序入口 |
demo.ipynb |
Jupyter 演示 |
config.py
# config.py
from dataclasses import dataclass
from typing import Optional
import os
@dataclass
class QdrantConfig:
"""Qdrant 连接配置"""
# 本地模式
mode: str = "memory" # "memory" | "local" | "cloud" | "file"
host: str = "localhost"
port: int = 6333
# Cloud 模式
url: Optional[str] = None
api_key: Optional[str] = None
# 文件模式
local_path: str = "./qdrant_data"
@classmethod
def from_env(cls) -> "QdrantConfig":
"""从环境变量读取配置"""
url = os.getenv("QDRANT_URL")
api_key = os.getenv("QDRANT_API_KEY")
if url and api_key:
return cls(mode="cloud", url=url, api_key=api_key)
else:
return cls(mode="memory")
@dataclass
class EmbeddingConfig:
"""向量化模型配置"""
model_name: str = "all-MiniLM-L6-v2"
vector_size: int = 384
batch_size: int = 64
normalize: bool = True
@dataclass
class SearchConfig:
"""搜索配置"""
collection_name: str = "knowledge_articles"
default_top_k: int = 10
score_threshold: float = 0.3
hnsw_ef: int = 128
data_loader.py
# data_loader.py
from typing import List, Dict, Any
import json
import os
# 样例知识库数据(大数据技术主题)
KNOWLEDGE_BASE = [
{
"id": 1,
"title": "Apache Spark 架构原理深度解析",
"content": "Spark 是一个统一的分析引擎,用于大规模数据处理。它基于 RDD(弹性分布式数据集)抽象,通过 DAG 调度器实现任务的高效执行。核心组件包括 Spark Core、Spark SQL、Spark Streaming、MLlib 和 GraphX。",
"category": "大数据框架",
"subcategory": "计算引擎",
"tags": ["Spark", "RDD", "DAG", "分布式计算"],
"difficulty": "中级",
"views": 3500,
"likes": 280,
"author": "数据架构师_A",
"publish_year": 2024,
},
{
"id": 2,
"title": "Spark 性能调优实战指南",
"content": "Spark 性能优化涵盖多个层面:数据倾斜处理(Salting、广播变量)、内存管理(executor.memory、memoryFraction)、序列化优化(Kryo)、并行度调整(spark.default.parallelism)以及 AQE(自适应查询执行)。",
"category": "大数据框架",
"subcategory": "性能优化",
"tags": ["Spark", "性能优化", "数据倾斜", "AQE"],
"difficulty": "高级",
"views": 4200,
"likes": 350,
"author": "数据架构师_A",
"publish_year": 2024,
},
{
"id": 3,
"title": "Apache Kafka 消息队列原理与实践",
"content": "Kafka 是一个分布式事件流平台,基于发布-订阅模型。核心概念包括 Topic(主题)、Partition(分区)、Offset(偏移量)、Consumer Group(消费者组)。Kafka 通过顺序 I/O 和零拷贝技术实现高吞吐量。",
"category": "消息队列",
"subcategory": "流式平台",
"tags": ["Kafka", "消息队列", "发布订阅", "流处理"],
"difficulty": "中级",
"views": 5100,
"likes": 420,
"author": "数据工程师_B",
"publish_year": 2025,
},
{
"id": 4,
"title": "Kafka 高可用集群部署与运维",
"content": "Kafka 高可用部署需要配置多 Broker 集群、ZooKeeper/KRaft 元数据管理、副本因子(Replication Factor >= 3)、ISR(同步副本集合)、ACK 确认机制。监控指标包括 Consumer Lag、Leader Election Rate。",
"category": "消息队列",
"subcategory": "运维管理",
"tags": ["Kafka", "高可用", "集群部署", "运维"],
"difficulty": "高级",
"views": 2800,
"likes": 190,
"author": "数据工程师_B",
"publish_year": 2025,
},
{
"id": 5,
"title": "Flink 流处理核心概念详解",
"content": "Flink 提供有状态的流处理能力,核心特性:事件时间(Event Time)vs 处理时间(Processing Time)、Watermark 机制、State Backend(RocksDB/Heap)、Checkpoint 与 Savepoint、窗口操作(滚动、滑动、会话窗口)。",
"category": "流处理",
"subcategory": "流计算引擎",
"tags": ["Flink", "流处理", "Watermark", "状态管理"],
"difficulty": "中级",
"views": 3900,
"likes": 310,
"author": "实时计算专家_C",
"publish_year": 2024,
},
{
"id": 6,
"title": "Flink CDC 实时数据同步实战",
"content": "Flink CDC(变更数据捕获)基于 Debezium 实现数据库到数据湖的实时同步。支持 MySQL、PostgreSQL、MongoDB 等数据源。通过 Changelog 流实现精确一次语义,适用于实时数仓、数据同步场景。",
"category": "流处理",
"subcategory": "数据同步",
"tags": ["Flink", "CDC", "实时同步", "数据湖"],
"difficulty": "高级",
"views": 4600,
"likes": 380,
"author": "实时计算专家_C",
"publish_year": 2025,
},
{
"id": 7,
"title": "数据湖架构选型:Delta Lake vs Apache Iceberg vs Apache Hudi",
"content": "开放表格式比较示例:可比较 Delta Lake 与 Databricks 生态的集成、Iceberg 与 Snowflake/Hive/Flink 等系统的兼容情况,以及 Hudi 的 Upsert 和增量处理能力;具体支持范围取决于组件版本与集成方式,不据此给出标准化程度排名。选型需考虑现有技术栈、ACID 事务需求、时间旅行深度。",
"category": "数据架构",
"subcategory": "数据湖",
"tags": ["数据湖", "Delta Lake", "Iceberg", "Hudi", "选型"],
"difficulty": "高级",
"views": 6200,
"likes": 510,
"author": "数据架构师_D",
"publish_year": 2025,
},
{
"id": 8,
"title": "ClickHouse OLAP 查询性能优化",
"content": "ClickHouse 性能优化策略:合理设计分区键和排序键(ORDER BY)、使用物化视图预计算聚合、列存储充分利用压缩(LZ4/ZSTD)、避免大表 JOIN(使用字典或 JOIN 的局部性)、合理设置 max_threads 并行度。",
"category": "数据仓库",
"subcategory": "列式数据库",
"tags": ["ClickHouse", "OLAP", "性能优化", "列存储"],
"difficulty": "高级",
"views": 3200,
"likes": 260,
"author": "数据工程师_E",
"publish_year": 2024,
},
{
"id": 9,
"title": "向量数据库选型指南:Qdrant vs Milvus vs Weaviate",
"content": "向量数据库比较示例:Qdrant 以 Rust 实现,可评估其内存使用与过滤能力;Milvus 支持多种索引类型,可评估其生态适配性;Weaviate 内置模块化 ML 能力,可评估其 RAG 集成方式。上述比较是检索样例语料,不代表性能或生态排名。选型关键:向量数量、QPS 要求、过滤复杂度。",
"category": "向量数据库",
"subcategory": "选型指南",
"tags": ["Qdrant", "Milvus", "向量数据库", "选型"],
"difficulty": "中级",
"views": 7800,
"likes": 620,
"author": "AI工程师_F",
"publish_year": 2025,
},
{
"id": 10,
"title": "RAG(检索增强生成)系统构建实战",
"content": "RAG 系统核心组件:文档解析(PDF/Word/HTML)、文本切片(Chunk Strategy)、向量化(Embedding Model)、向量检索(Vector Store)、提示词工程(Prompt Template)、LLM 生成。关键指标:检索召回率、答案准确性。",
"category": "AI应用",
"subcategory": "RAG",
"tags": ["RAG", "LLM", "向量检索", "AI应用"],
"difficulty": "高级",
"views": 9500,
"likes": 780,
"author": "AI工程师_F",
"publish_year": 2025,
},
{
"id": 11,
"title": "Kubernetes 大数据集群管理最佳实践",
"content": "K8s 管理大数据集群要点:使用 StatefulSet 部署有状态服务(Kafka/Zookeeper)、配置 PodAntiAffinity 分散副本、合理设置 Resource Requests/Limits、使用 StorageClass 动态供应 PV、配置 HPA 自动扩缩容。",
"category": "云原生",
"subcategory": "容器编排",
"tags": ["Kubernetes", "大数据", "容器化", "运维"],
"difficulty": "高级",
"views": 2900,
"likes": 230,
"author": "云架构师_G",
"publish_year": 2024,
},
{
"id": 12,
"title": "数据质量体系建设与数据治理实践",
"content": "数据质量体系涵盖六个维度:完整性、准确性、一致性、时效性、唯一性、有效性。治理工具:Great Expectations(规则定义)、Apache Atlas(元数据管理)、DataHub(数据目录)。PDCA 循环持续改进数据质量。",
"category": "数据治理",
"subcategory": "数据质量",
"tags": ["数据治理", "数据质量", "元数据管理"],
"difficulty": "中级",
"views": 4100,
"likes": 330,
"author": "数据治理专家_H",
"publish_year": 2025,
},
]
def load_knowledge_base() -> List[Dict[str, Any]]:
"""加载知识库数据"""
return KNOWLEDGE_BASE
def get_texts_for_embedding(articles: List[Dict[str, Any]]) -> List[str]:
"""
提取用于向量化的文本
策略:标题 + 内容组合,提升语义完整性
"""
return [
f"{article['title']}。{article['content']}"
for article in articles
]
search_engine.py
# search_engine.py
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, Distance, PointStruct,
Filter, FieldCondition, MatchValue, MatchAny, Range,
SearchParams, PayloadSchemaType,
)
from sentence_transformers import SentenceTransformer
from typing import List, Dict, Any, Optional
import logging
import time
logger = logging.getLogger(__name__)
class ArticleSearchEngine:
"""
文章语义搜索引擎
功能:
- 文章向量化存储
- 语义相似度搜索
- 多维度 Payload 过滤
- 搜索结果格式化
"""
def __init__(
self,
qdrant_client: QdrantClient,
embedding_model: SentenceTransformer,
collection_name: str = "knowledge_articles",
vector_size: int = 384,
):
self.client = qdrant_client
self.model = embedding_model
self.collection_name = collection_name
self.vector_size = vector_size
self._initialized = False
def initialize(self, force_recreate: bool = False) -> None:
"""
初始化搜索引擎:创建 Collection 并建立 Payload 索引
Args:
force_recreate: 是否强制重建(删除已有数据)
"""
# 检查是否需要重建
collections = {c.name for c in self.client.get_collections().collections}
if self.collection_name in collections:
if force_recreate:
logger.info(f"删除已有 Collection: {self.collection_name}")
self.client.delete_collection(self.collection_name)
else:
logger.info(f"Collection 已存在: {self.collection_name},跳过创建")
self._initialized = True
return
# 创建 Collection
logger.info(f"创建 Collection: {self.collection_name} (dim={self.vector_size})")
self.client.create_collection(
collection_name=self.collection_name,
vectors_config=VectorParams(
size=self.vector_size,
distance=Distance.COSINE,
),
)
# 建立 Payload 索引(提升过滤查询性能)
indexes = [
("category", PayloadSchemaType.KEYWORD),
("subcategory", PayloadSchemaType.KEYWORD),
("difficulty", PayloadSchemaType.KEYWORD),
("author", PayloadSchemaType.KEYWORD),
("tags", PayloadSchemaType.KEYWORD),
("views", PayloadSchemaType.INTEGER),
("likes", PayloadSchemaType.INTEGER),
("publish_year", PayloadSchemaType.INTEGER),
("title", PayloadSchemaType.TEXT),
("content", PayloadSchemaType.TEXT),
]
for field_name, field_schema in indexes:
self.client.create_payload_index(
collection_name=self.collection_name,
field_name=field_name,
field_schema=field_schema,
)
logger.debug(f"已建索引: {field_name} ({field_schema})")
logger.info(f"Collection 初始化完成,已建立 {len(indexes)} 个 Payload 索引")
self._initialized = True
def index_articles(
self,
articles: List[Dict[str, Any]],
text_fields: List[str] = None,
batch_size: int = 64,
) -> int:
"""
批量索引文章数据
Args:
articles: 文章列表
text_fields: 用于生成向量的文本字段(默认组合 title + content)
batch_size: 批处理大小
Returns:
成功索引的文章数量
"""
if not self._initialized:
raise RuntimeError("请先调用 initialize() 方法")
if text_fields is None:
# 默认使用标题 + 内容的组合
texts = [f"{a['title']}。{a['content']}" for a in articles]
else:
texts = [" ".join(str(a.get(f, "")) for f in text_fields) for a in articles]
total = len(articles)
indexed = 0
logger.info(f"开始索引 {total} 篇文章...")
start_time = time.time()
for batch_start in range(0, total, batch_size):
batch_articles = articles[batch_start: batch_start + batch_size]
batch_texts = texts[batch_start: batch_start + batch_size]
# 批量向量化
vectors = self.model.encode(
batch_texts,
normalize_embeddings=True,
show_progress_bar=False,
)
# 构建 PointStruct 列表
points = [
PointStruct(
id=article["id"],
vector=vector.tolist(),
payload={k: v for k, v in article.items() if k != "id"},
)
for article, vector in zip(batch_articles, vectors)
]
# 写入 Qdrant
self.client.upsert(
collection_name=self.collection_name,
points=points,
wait=True,
)
indexed += len(batch_articles)
logger.info(f" 已索引: {indexed}/{total}")
elapsed = time.time() - start_time
logger.info(f"索引完成!{indexed} 篇文章,耗时 {elapsed:.1f}s")
return indexed
def search(
self,
query: str,
top_k: int = 5,
category: Optional[str] = None,
difficulty: Optional[str] = None,
tags: Optional[List[str]] = None,
min_views: Optional[int] = None,
min_year: Optional[int] = None,
score_threshold: float = 0.2,
) -> List[Dict[str, Any]]:
"""
语义搜索(支持多维度过滤)
Args:
query: 搜索查询文本
top_k: 返回结果数量
category: 限定分类
difficulty: 限定难度("初级"/"中级"/"高级")
tags: 标签过滤(任一匹配)
min_views: 最低阅读量
min_year: 最低发布年份
score_threshold: 最低相似度阈值
Returns:
格式化的搜索结果列表
"""
# 构建过滤条件
must_conditions = []
should_conditions = []
if category:
must_conditions.append(
FieldCondition(key="category", match=MatchValue(value=category))
)
if difficulty:
must_conditions.append(
FieldCondition(key="difficulty", match=MatchValue(value=difficulty))
)
if min_views:
must_conditions.append(
FieldCondition(key="views", range=Range(gte=min_views))
)
if min_year:
must_conditions.append(
FieldCondition(key="publish_year", range=Range(gte=min_year))
)
if tags:
for tag in tags:
should_conditions.append(
FieldCondition(key="tags", match=MatchValue(value=tag))
)
query_filter = None
if must_conditions or should_conditions:
query_filter = Filter(
must=must_conditions or None,
should=should_conditions or None,
)
# 向量化查询
query_vector = self.model.encode(
query, normalize_embeddings=True
).tolist()
# 执行搜索
hits = self.client.search(
collection_name=self.collection_name,
query_vector=query_vector,
query_filter=query_filter,
limit=top_k,
with_payload=True,
with_vectors=False,
score_threshold=score_threshold,
search_params=SearchParams(hnsw_ef=128),
)
# 格式化结果
return [
{
"rank": rank,
"id": hit.id,
"score": round(hit.score, 4),
"title": hit.payload.get("title", ""),
"content": hit.payload.get("content", "")[:150] + "...",
"category": hit.payload.get("category", ""),
"subcategory": hit.payload.get("subcategory", ""),
"difficulty": hit.payload.get("difficulty", ""),
"tags": hit.payload.get("tags", []),
"views": hit.payload.get("views", 0),
"author": hit.payload.get("author", ""),
"publish_year": hit.payload.get("publish_year", ""),
}
for rank, hit in enumerate(hits, 1)
]
def get_stats(self) -> Dict[str, Any]:
"""获取 Collection 统计信息"""
info = self.client.get_collection(self.collection_name)
return {
"collection_name": self.collection_name,
"total_points": info.points_count,
"indexed_vectors": info.indexed_vectors_count,
"status": str(info.status),
"vector_size": self.vector_size,
}
main.py
# main.py
"""
文章语义搜索系统 - 完整演示
运行方式:
python main.py
前置条件:
pip install qdrant-client sentence-transformers
"""
import logging
from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer
from data_loader import load_knowledge_base, get_texts_for_embedding
from search_engine import ArticleSearchEngine
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)
def print_results(results, query: str, filters: dict = None):
"""美观打印搜索结果"""
filter_str = ""
if filters:
filter_str = " | 过滤: " + ", ".join(f"{k}={v}" for k, v in filters.items() if v)
print(f"\n{'='*65}")
print(f"查询: 「{query}」{filter_str}")
print(f"找到 {len(results)} 条结果")
print("-" * 65)
if not results:
print(" (无匹配结果)")
return
for result in results:
print(f"#{result['rank']} 相似度: {result['score']:.4f} [{result['difficulty']}]")
print(f" 标题: {result['title']}")
print(f" 分类: {result['category']} > {result['subcategory']}")
print(f" 标签: {', '.join(result['tags'])}")
print(f" 摘要: {result['content'][:80]}...")
print(f" 作者: {result['author']} | 阅读: {result['views']:,} | {result['publish_year']}")
print()
def run_demo(engine: ArticleSearchEngine):
"""运行演示搜索"""
demo_cases = [
# (查询文本, 过滤条件字典)
("分布式计算框架性能优化", {}),
("实时数据流处理与状态管理", {"category": "流处理"}),
("数据湖和 Lakehouse 架构比较", {}),
("向量数据库在 AI 场景的应用", {"min_views": 5000}),
("消息队列高可用部署方案", {"difficulty": "高级"}),
("大数据平台运维监控", {"tags": ["Kafka", "Kubernetes"]}),
("如何提升查询速度", {"category": "数据仓库"}),
]
for query, filters in demo_cases:
results = engine.search(
query=query,
top_k=3,
**filters,
)
print_results(results, query, filters)
def main():
logger.info("=== 文章语义搜索系统启动 ===")
# ====================================================
# 1. 初始化 Qdrant 客户端
# ====================================================
logger.info("初始化 Qdrant 客户端(内存模式)...")
qdrant = QdrantClient(":memory:")
# 若要连接本地 Docker:
# qdrant = QdrantClient(host="localhost", port=6333)
# 若要连接 Qdrant Cloud:
# import os
# qdrant = QdrantClient(url=os.getenv("QDRANT_URL"), api_key=os.getenv("QDRANT_API_KEY"))
# ====================================================
# 2. 加载向量化模型
# ====================================================
logger.info("加载向量化模型(all-MiniLM-L6-v2)...")
model = SentenceTransformer("all-MiniLM-L6-v2")
logger.info(f"模型就绪,向量维度: {model.get_sentence_embedding_dimension()}")
# ====================================================
# 3. 创建搜索引擎并初始化
# ====================================================
engine = ArticleSearchEngine(
qdrant_client=qdrant,
embedding_model=model,
collection_name="knowledge_articles",
vector_size=384,
)
engine.initialize(force_recreate=True)
# ====================================================
# 4. 加载并索引知识库数据
# ====================================================
logger.info("加载知识库数据...")
articles = load_knowledge_base()
logger.info(f"共加载 {len(articles)} 篇文章")
engine.index_articles(articles, batch_size=32)
# 输出统计信息
stats = engine.get_stats()
logger.info(f"索引状态: {stats}")
# ====================================================
# 5. 执行演示搜索
# ====================================================
logger.info("开始执行演示搜索...")
run_demo(engine)
# ====================================================
# 6. 交互式搜索(可选)
# ====================================================
print("\n" + "="*65)
print("进入交互式搜索模式(输入 'q' 退出)")
print("="*65)
while True:
query = input("\n请输入搜索关键词: ").strip()
if query.lower() in ("q", "quit", "exit", ""):
print("已退出搜索。")
break
results = engine.search(query=query, top_k=5)
print_results(results, query)
if __name__ == "__main__":
main()
运行输出示意(以下排名、分数和耗时仅作为格式示例,不代表已提供可复现的实测结果)
=== 文章语义搜索系统启动 ===
初始化 Qdrant 客户端(内存模式)...
加载向量化模型(all-MiniLM-L6-v2)...
模型就绪,向量维度: 384
创建 Collection: knowledge_articles (dim=384)
Collection 初始化完成,已建立 10 个 Payload 索引
加载知识库数据...
共加载 12 篇文章
开始索引 12 篇文章...
已索引: 12/12
索引完成!12 篇文章,耗时 0.8s
=================================================================
查询: 「分布式计算框架性能优化」
找到 3 条结果
-----------------------------------------------------------------
#1 相似度: 0.8912 [高级]
标题: Spark 性能调优实战指南
分类: 大数据框架 > 性能优化
标签: Spark, 性能优化, 数据倾斜, AQE
摘要: Spark 性能优化涵盖多个层面:数据倾斜处理(Salting、广播变量)、...
作者: 数据架构师_A | 阅读: 4,200 | 2024
#2 相似度: 0.7453 [中级]
标题: Apache Spark 架构原理深度解析
分类: 大数据框架 > 计算引擎
标签: Spark, RDD, DAG, 分布式计算
摘要: Spark 是一个统一的分析引擎,用于大规模数据处理。...
作者: 数据架构师_A | 阅读: 3,500 | 2024
#3 相似度: 0.5831 [高级]
标题: ClickHouse OLAP 查询性能优化
分类: 数据仓库 > 列式数据库
标签: ClickHouse, OLAP, 性能优化, 列存储
摘要: ClickHouse 性能优化策略:合理设计分区键和排序键...
作者: 数据工程师_E | 阅读: 3,200 | 2024
常见操作速查
数据管理操作
# quick_reference.py
from qdrant_client.models import PointIdsList, FilterSelector, Filter, FieldCondition, MatchValue
# ====================================================
# 更新 Payload(不重新计算向量)
# ====================================================
# 设置(覆盖)Payload 字段
client.set_payload(
collection_name="articles",
payload={"views": 9999, "featured": True},
points=[1, 2, 3], # 指定 Point ID 列表
)
# 追加 Payload 字段(不影响其他字段)
client.set_payload(
collection_name="articles",
payload={"last_updated": "2026-03-11"},
points=[1],
)
# 删除 Payload 中的特定字段
client.delete_payload(
collection_name="articles",
keys=["temp_field", "draft"],
points=[1, 2],
)
# 按过滤条件更新 Payload(批量操作)
client.set_payload(
collection_name="articles",
payload={"status": "archived"},
points=FilterSelector(
filter=Filter(must=[
FieldCondition(key="publish_year", match=MatchValue(value=2023))
])
),
)
# ====================================================
# 删除 Points
# ====================================================
# 按 ID 删除
client.delete(
collection_name="articles",
points_selector=PointIdsList(points=[10, 11, 12]),
)
# 按过滤条件删除(批量)
client.delete(
collection_name="articles",
points_selector=FilterSelector(
filter=Filter(must=[
FieldCondition(key="status", match=MatchValue(value="archived"))
])
),
)
# ====================================================
# 统计和管理
# ====================================================
# 统计满足条件的 Point 数量
count = client.count(
collection_name="articles",
count_filter=Filter(must=[
FieldCondition(key="category", match=MatchValue(value="大数据框架"))
]),
exact=True, # 精确计数(慢但准确)
)
print(f"大数据框架类文章数: {count.count}")
# 创建快照(备份)
snapshot = client.create_snapshot(collection_name="articles")
print(f"快照创建成功: {snapshot.name}")
# 列出所有快照
snapshots = client.list_snapshots(collection_name="articles")
for s in snapshots:
print(f" {s.name}: {s.size} bytes")
推荐搜索(基于已有 Point)
# recommend_search.py
# 基于某个已有 Point 的向量进行推荐(不需要重新计算向量)
recommendations = client.recommend(
collection_name="articles",
positive=[1, 2], # 正样本 Point ID("我喜欢这些")
negative=[5], # 负样本 Point ID("我不喜欢这个")
limit=5,
with_payload=True,
score_threshold=0.3,
)
print("基于第 1、2 篇文章的推荐(排除第 5 篇的风格):")
for hit in recommendations:
print(f" [{hit.score:.4f}] {hit.payload['title']}")
性能调优建议
写入性能优化
# performance_write.py
from qdrant_client.models import OptimizersConfigDiff
# 1. 批量写入而非逐条写入
# 错误做法(慢):
for article in articles:
client.upsert(collection_name="articles", points=[...])
# 正确做法(快,减少网络往返):
client.upsert(collection_name="articles", points=[...batch_of_100...])
# 2. 大量数据写入时临时关闭优化器
collection_info = client.get_collection("articles")
client.update_collection(
collection_name="articles",
optimizer_config=OptimizersConfigDiff(
indexing_threshold=0, # 暂停索引构建(写入更快)
),
)
# ... 批量写入 ...
# 写入完成后恢复
client.update_collection(
collection_name="articles",
optimizer_config=OptimizersConfigDiff(
indexing_threshold=20000, # 恢复默认值
),
)
搜索性能优化
# performance_search.py
# 1. 按需返回 Payload 字段(减少数据传输量)
results = client.search(
collection_name="articles",
query_vector=query_vector,
limit=10,
with_payload=["title", "category", "score"], # 只返回需要的字段
with_vectors=False, # 不返回向量
)
# 2. 合理设置 hnsw_ef(精度/速度权衡)
# hnsw_ef 控制查询候选集,ef_construct 控制索引构建候选集;未指定 hnsw_ef 时使用服务端默认搜索配置
# 降低 hnsw_ef 可以提升搜索速度,但会降低精度
from qdrant_client.models import SearchParams
results = client.search(
collection_name="articles",
query_vector=query_vector,
limit=10,
search_params=SearchParams(hnsw_ef=64), # 速度优先
# search_params=SearchParams(hnsw_ef=256), # 精度优先
)
# 3. 为频繁过滤的字段建立 Payload 索引
# (已在 search_engine.py 的 initialize() 方法中实现)
# 4. 评估量化压缩(float32 转 int8 的理论单元素存储减少 75%;总内存及检索质量变化需实测)
from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig, ScalarType
client.update_collection(
collection_name="articles",
quantization_config=ScalarQuantization(
scalar=ScalarQuantizationConfig(
type=ScalarType.INT8,
quantile=0.99,
always_ram=True, # 量化向量保留在内存
)
),
)
tip 生产环境核心调优原则
- 批量操作:单次 upsert 100~500 条,避免逐条写入
- Payload 索引:在服务端环境为常用过滤字段评估建立索引;收益取决于数据量、过滤选择性和执行计划,不应断言所有无索引查询都会全表扫描
- 字段选择:只返回展示需要的 Payload 字段
- 向量归一化:使用余弦相似度时,编码时加
normalize_embeddings=True - hnsw_ef 调优:精度要求高时调大(128~256),速度优先时调小(32~64)
- 量化:内存有限时可评估 Scalar 量化;float32 转 int8 的理论单元素存储减少 75%,不等于总内存减少 75%;精度损失 < 5% 仅可作为待验证假设,需结合语料、重评分策略及评估指标测量
相关文档
- Qdrant 学习路线图 — 全局学习路径规划
- 核心概念详解 — Collection、Point、Payload、Filter 深度解析
- 安装与部署 — 本文使用的 Qdrant 部署方式详解
- Milvus 安装对比 — 另一主流向量数据库的部署方案
- 向量数据库选型指南 — 在 Qdrant/Milvus/Weaviate 之间选型
#Qdrant #向量数据库 #Python #语义搜索 #sentence-transformers #实战教程 #RAG