第2章 向量检索数据库¶
学习目标¶
- 了解 Qdrant、Milvus、PGVector 三类向量数据库的架构特点与适用场景
- 掌握向量数据库的核心操作:集合管理、向量插入、相似度查询
- 学会将向量库与 OpenAI SDK 集成,构建端到端的语义检索
- 理解距离度量(余弦/欧氏/点积)的取舍
- 掌握向量库性能调优的关键参数与实践
问题导入¶
某初创公司用 Python 列表存了 50 万条文档向量,每次检索都遍历全部计算余弦相似度,响应时间从最初的 100ms 涨到 8 秒,且内存占用飙升到 12GB。团队尝试用 NumPy 矩阵运算优化,速度有所提升但仍无法支撑百万级数据,且每次重启都要重新加载全部向量到内存。更麻烦的是,他们无法对"只检索某部门文档"这类过滤需求做高效处理。
问题的本质是:向量检索需要专门的存储与检索引擎。向量数据库专为高维向量的近似最近邻搜索(ANN)设计,提供持久化、过滤、分布式扩展与高可用能力。本章对比三种主流向量库,并以 Qdrant 为例讲解完整操作。
核心概念¶
1. 三类向量数据库概览¶
| 维度 | Qdrant | Milvus | PGVector |
|---|---|---|---|
| 实现语言 | Rust | Go/C++ | C(PG 扩展) |
| 部署形态 | 单机/Docker/K8s | 分布式集群 | 复用 PostgreSQL |
| 索引类型 | HNSW(默认) | IVF/HNSW/DiskANN | HNSW/IVFFlat |
| 过滤能力 | 强(payload 过滤) | 强 | SQL WHERE |
| 规模上限 | 千万级 | 十亿级 | 百万级 |
| 事务一致性 | 否 | 否 | 是(PG 事务) |
| 学习成本 | 低 | 中高 | 低(会 SQL 即可) |
| 适用场景 | 中小团队、性能敏感 | 超大规模、生产集群 | 已有 PG、需事务 |
2. 选型决策树¶
- 数据量 < 100 万,且已有 PostgreSQL → PGVector,复用现有运维体系
- 数据量 100 万-1 亿,追求查询延迟与开发体验 → Qdrant
- 数据量 > 1 亿,需要分布式与多副本 → Milvus
- 需要强事务(向量与业务数据强一致) → PGVector
- 需要复杂 payload 过滤与多租户 → Qdrant 或 Milvus
3. 距离度量¶
向量库通常支持三种距离:
- 余弦相似度(Cosine):衡量方向相似性,对向量长度不敏感,是文本 Embedding 的默认选择。Qdrant 中
Distance.COSINE。 - 欧氏距离(Euclid):衡量空间绝对距离,对向量长度敏感,适合图像 Embedding。
Distance.EUCLID。 - 点积(Dot):兼顾方向与模长,当向量已归一化时等价于余弦。
Distance.DOT。
文本 RAG 场景默认选 Cosine。
4. 集合(Collection)管理¶
向量库以"集合"组织数据,类似关系库的表。创建集合时需指定:
- 向量维度(必须与 Embedding 模型输出一致,如 1536)
- 距离度量
- 索引类型与参数(HNSW 的 M、ef_construction)
- 是否启用量化(Quantization)以压缩存储
5. 与 OpenAI SDK 集成¶
标准集成模式:用 OpenAI 生成 Embedding → 用向量库客户端写入/查询 → 把检索结果拼回 Prompt。注意 Embedding 模型与入库时保持一致,否则检索质量崩塌。
原理图¶
flowchart LR
subgraph 写入路径
Doc[文档分块] --> Embed1[OpenAI Embedding API]
Embed1 --> Upsert[qdrant.upsert]
Upsert --> Coll[(Qdrant Collection<br/>vectors + payload)]
end
subgraph 查询路径
Query[用户问题] --> Embed2[OpenAI Embedding API]
Embed2 --> Search[qdrant.search]
Search --> Filter{payload 过滤}
Coll --> Filter
Filter --> TopK[Top-K 候选]
TopK --> Prompt[拼装 Prompt]
Prompt --> LLM[LLM 生成]
end
subgraph 存储层
Coll --> HNSW[HNSW 索引]
Coll --> Payload[Payload 存储<br/>doc_id/text/metadata]
Coll --> Quant[标量量化<br/>压缩内存]
end
图:向量数据库在 RAG 中的写入与查询路径
示例代码¶
以 Qdrant 为例的完整集合管理、插入与查询:
import os
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance,
VectorParams,
PointStruct,
Filter,
FieldCondition,
MatchValue,
)
from openai import OpenAI
os.environ.setdefault("OPENAI_API_KEY", "sk-xxx")
oai = OpenAI()
# 生产环境改为 QdrantClient(host="localhost", port=6333)
qdrant = QdrantClient(":memory:")
COLLECTION = "articles"
DIM = 1536
def create_collection() -> None:
"""创建集合,1536 维 + 余弦距离 + HNSW 索引。"""
if qdrant.collection_exists(COLLECTION):
qdrant.delete_collection(COLLECTION)
qdrant.create_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=DIM, distance=Distance.COSINE),
# HNSW 参数(Qdrant 默认 M=16, ef_construct=100,可显式调整)
hnsw_config={"m": 16, "ef_construct": 100},
)
def embed(text: str) -> list[float]:
return oai.embeddings.create(
model="text-embedding-3-small", input=text
).data[0].embedding
def insert_points(points: list[dict]) -> None:
"""批量插入向量与 payload。"""
structs = [
PointStruct(
id=p["id"],
vector=embed(p["text"]),
payload={
"text": p["text"],
"source": p["source"],
"category": p["category"],
},
)
for p in points
]
qdrant.upsert(collection_name=COLLECTION, points=structs)
def search(query: str, top_k: int = 3, category: str | None = None) -> list[dict]:
"""语义检索,可选按 category 过滤。"""
query_filter = None
if category:
query_filter = Filter(
must=[FieldCondition(key="category", match=MatchValue(value=category))]
)
results = qdrant.search(
collection_name=COLLECTION,
query_vector=embed(query),
limit=top_k,
query_filter=query_filter,
with_payload=True,
)
return [
{"score": r.score, "text": r.payload["text"], "source": r.payload["source"]}
for r in results
]
if __name__ == "__main__":
create_collection()
insert_points(
[
{"id": 1, "text": "Qwen3 是阿里开源大模型。", "source": "wiki", "category": "model"},
{"id": 2, "text": "Llama 3.3 由 Meta 发布。", "source": "wiki", "category": "model"},
{"id": 3, "text": "Python 是动态类型语言。", "source": "blog", "category": "lang"},
]
)
print("全库检索:", search("阿里开源模型"))
print("按分类过滤:", search("阿里开源模型", category="lang"))
运行结果说明:全库检索返回 Qwen3 片段得分最高;按 category=lang 过滤后只返回 Python 片段,体现了 payload 过滤的价值。
性能调优要点¶
# 1. 批量插入(减少网络往返)
qdrant.upsert(collection_name=COLLECTION, points=batch, wait=False)
# 2. 启用标量量化压缩内存(牺牲极小精度换 4x 内存节省)
from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig
qdrant.update_collection(
collection_name=COLLECTION,
quantization_config=ScalarQuantization(
scalar=ScalarQuantizationConfig(type="int8", quantile=0.99, always_ram=True)
),
)
# 3. 查询时调 ef_search(Qdrant 中用 params 控制)
results = qdrant.search(
collection_name=COLLECTION,
query_vector=qv,
limit=10,
search_params=SearchParams(hnsw_ef=128, exact=False),
)
企业案例¶
案例 1:选型对比实践¶
某电商团队在选型时对三款库做了基准测试(100 万 1536 维向量): - PGVector:建索引慢,查询 50-200ms,但能直接 JOIN 商品表做复杂过滤 - Qdrant:查询 5-15ms,payload 过滤灵活,Docker 部署 10 分钟搞定 - Milvus:查询 8-20ms,分布式扩展能力强,但运维复杂度高
最终选 Qdrant,因为 100 万规模下单机够用且开发体验最佳。
案例 2:多租户知识库¶
某 SaaS 厂商用 Qdrant 服务 200 个企业客户,每个客户数据隔离。方案是为每个 tenant_id 创建独立集合,配合 payload 过滤实现二级隔离。单实例支撑 200 集合、总计 3000 万向量,查询 P99 < 30ms。
案例 3:从 PGVector 迁移到 Milvus¶
某视频平台随着向量规模从 50 万涨到 2 亿,PGVector 查询从 30ms 涨到 800ms 且索引构建耗时数天。迁移到 Milvus 集群(3 节点 + DiskANN 索引)后,查询稳定在 20ms,索引构建 4 小时完成。
常见问题¶
Q1:向量维度选多少?
A:由 Embedding 模型决定。OpenAI text-embedding-3-small 是 1536 维,3-large 是 3072 维。可用 dimensions 参数降维(如 1536→256)以省存储,但召回会略降。
Q2:Qdrant 单机能存多少向量? A:1536 维 + 余弦,1 亿向量约需 60GB 内存(开启 int8 量化后约 15GB)。超出后用分片集群。
Q3:向量库和关系库怎么协同?
A:向量库存检索向量和 payload 摘要;关系库存完整业务数据。检索拿到 doc_id 后去关系库 JOIN 出完整字段,避免向量库承载过多业务数据。
Q4:HNSW 查询为什么有时比 IVF 快很多?
A:HNSW 在图上贪心游走,复杂度约 O(log N);IVF 需扫描 nprobe 个簇的所有向量。但 HNSW 内存占用更高,是典型的空间换时间。
面试题¶
1. Qdrant、Milvus、PGVector 如何选型? 参考答案:百万级以下且已有 PG 选 PGVector;百万到亿级选 Qdrant,开发体验与性能平衡;十亿级以上或需分布式选 Milvus。需强事务选 PGVector,需复杂过滤选 Qdrant/Milvus。
2. 为什么文本检索默认用余弦相似度? 答题要点:文本 Embedding 关注语义方向而非向量模长,余弦只比较方向,对归一化后的向量等价于点积。欧氏距离会受模长影响,可能让长文本与短文本相似度偏低。
3. 解释 HNSW 的 ef_search 与 ef_construction 区别。
参考答案:ef_construction 是构建索引时候选邻居池大小,影响索引质量与构建耗时,建好后固定;ef_search 是查询时候选池大小,在线可调,越大召回越高、查询越慢。
4. 向量库如何实现增量更新而不停服? 答题要点:HNSW 支持在线 upsert/insert;更新用"先删后插";删除按 payload 过滤。Qdrant/Milvus 都支持在线写入与查询并发,无需停服。
练习题¶
- 三库对比实验:用相同的 10 万条数据分别在 Qdrant、PGVector、Milvus 上建索引,对比建索引耗时、查询 P99、内存占用。
- 过滤查询实现:在 Qdrant 中实现"只检索 source=wiki 且 category in [model, lang] 的块"。
- 量化对比:对同一集合分别用未量化、int8 量化、binary 量化,对比内存占用与 Recall@10 差异。
小结¶
向量数据库是 RAG 的存储与检索引擎。本章对比了 Qdrant(中小团队首选)、Milvus(超大规模首选)、PGVector(已有 PG 场景首选)三类方案,并以 Qdrant 为例演示了集合管理、向量插入、过滤查询与性能调优。生产环境推荐 Qdrant 作为起步方案,配合 HNSW 索引与 int8 量化平衡速度与成本。下一章将进入搜索策略,探讨稠密、稀疏与混合检索。