向量数据库选型与工程实践
原创
灵阙教研团队
S 精选 进阶 |
约 8 分钟阅读
更新于 2026-02-28 AI 导读
向量数据库选型与工程实践 Qdrant vs Milvus vs Weaviate vs pgvector 全维度对比、索引算法原理、混合搜索与生产部署指南 引言 向量数据库是 RAG(检索增强生成)系统的核心基础设施。它解决的根本问题是:给定一个查询向量,如何从数十亿向量中快速找到最相似的 K 个结果。暴力搜索的时间复杂度是...
向量数据库选型与工程实践
Qdrant vs Milvus vs Weaviate vs pgvector 全维度对比、索引算法原理、混合搜索与生产部署指南
引言
向量数据库是 RAG(检索增强生成)系统的核心基础设施。它解决的根本问题是:给定一个查询向量,如何从数十亿向量中快速找到最相似的 K 个结果。暴力搜索的时间复杂度是 O(N),在百万级数据集上已经不可接受。向量数据库通过近似最近邻(ANN)索引算法,将搜索复杂度降低到 O(log N) 甚至 O(1)。
本文系统对比主流向量数据库,深入解析索引算法原理,并给出生产部署的工程实践。
核心概念
向量相似度度量
余弦相似度 (Cosine Similarity):
sim(A, B) = A·B / (|A| x |B|)
范围: [-1, 1]
适用: 文本语义检索(对向量长度不敏感)
内积 (Inner Product / Dot Product):
sim(A, B) = A·B = sum(a_i * b_i)
范围: (-inf, +inf)
适用: 已归一化的向量(等价于余弦相似度)
欧氏距离 (Euclidean / L2):
dist(A, B) = sqrt(sum((a_i - b_i)^2))
范围: [0, +inf]
适用: 图像特征、空间位置检索
索引算法家族
| 算法 | 类型 | 时间复杂度 | 空间开销 | 精度 | 适用场景 |
|---|---|---|---|---|---|
| Flat/Brute | 精确搜索 | O(N) | O(1) | 100% | 小数据集 (<10K),基线对比 |
| IVF | 倒排索引 | O(N/K) | O(N) | 高 | 中等数据集,内存受限 |
| HNSW | 图索引 | O(log N) | O(N x M) | 极高 | 大数据集,延迟敏感 |
| ScaNN | 量化+分区 | O(sqrt(N)) | O(N/4) | 高 | 超大数据集,吞吐优先 |
| DiskANN | 磁盘索引 | O(log N) | 磁盘为主 | 高 | 十亿级数据集,成本敏感 |
索引算法深度解析
HNSW(Hierarchical Navigable Small World)
HNSW 是目前最广泛使用的 ANN 索引算法,核心思想是构建多层跳表式的图结构:
Layer 3 (最稀疏): [A] ─────────────── [F]
│ │
Layer 2: [A] ── [C] ────── [F] ── [H]
│ │ │ │
Layer 1: [A] [B] [C] [D] [E] [F] [G] [H]
│ │ │ │ │ │ │ │
Layer 0 (最稠密): 所有节点全连接(受限于 M 参数)
搜索过程(查找最近邻 of query Q):
1. 从 Layer 3 的入口点 A 开始
2. 在当前层贪心搜索最近节点
3. 到达当前层的局部最优后,下降到下一层
4. 在 Layer 0 进行精细搜索
关键参数:
M (最大连接数):
- 控制图的连通度
- 越大 -> 精度越高,索引越大,构建越慢
- 推荐值: 16-64 (默认 16)
ef_construction (构建时搜索宽度):
- 构建索引时的搜索列表大小
- 越大 -> 图质量越高,构建越慢
- 推荐值: 100-500 (默认 200)
ef_search (查询时搜索宽度):
- 搜索时的候选列表大小
- 越大 -> 精度越高,延迟越高
- 推荐值: 50-500 (根据精度要求调整)
IVF(Inverted File Index)
IVF 先将向量空间聚类,搜索时只扫描最近的几个聚类:
构建阶段:
1. 用 K-Means 将 N 个向量分成 nlist 个聚类
2. 每个向量归入最近的聚类中心
搜索阶段:
1. 找到 query 最近的 nprobe 个聚类中心
2. 只在这 nprobe 个聚类内做精确搜索
nlist = sqrt(N) (经验值)
nprobe = nlist/10 (精度/速度平衡)
IVF + PQ(乘积量化)
PQ 将高维向量切分为子空间,每个子空间独立量化,实现 32x-64x 压缩:
原始向量 (768 维, FP32):
[v1, v2, v3, ..., v768] = 3072 bytes
PQ 量化 (M=96 子空间, nbits=8):
将 768 维切成 96 个 8 维子空间
每个子空间用 1 字节编码 (256 个码本)
压缩后: 96 bytes (32x 压缩)
主流数据库对比
四大向量数据库全维度对比
| 维度 | Qdrant | Milvus | Weaviate | pgvector |
|---|---|---|---|---|
| 语言 | Rust | Go + C++ | Go | C (PostgreSQL 扩展) |
| 架构 | 单节点/分布式 | 分布式(云原生) | 单节点/分布式 | 嵌入 PostgreSQL |
| 索引 | HNSW + 量化 | IVF/HNSW/DiskANN/GPU | HNSW + PQ | IVF/HNSW |
| 标量过滤 | 原生(高效) | 原生 | 原生 | SQL WHERE |
| 混合搜索 | 稀疏+稠密原生 | 稀疏+稠密原生 | BM25+向量原生 | 需手动组合 |
| 多租户 | Collection 级别 | Partition 级别 | Tenant 级别 | Schema 级别 |
| 最大向量 | 数十亿(分布式) | 数百亿 | 数十亿 | 数千万 |
| 延迟 (p99) | <10ms | <20ms | <15ms | <50ms |
| 运维复杂度 | 低 | 高(依赖 etcd/MinIO) | 中 | 极低(复用 PG) |
| 许可证 | Apache 2.0 | Apache 2.0 | BSD-3 | PostgreSQL |
| 云托管 | Qdrant Cloud | Zilliz Cloud | Weaviate Cloud | Supabase/Neon |
选型决策树
开始
│
├─ 已有 PostgreSQL,向量规模 < 500 万?
│ └─ YES → pgvector(零额外运维)
│
├─ 需要十亿级+分布式?
│ └─ YES → Milvus(但运维复杂度高)
│
├─ 需要内置 BM25 混合搜索?
│ └─ YES → Weaviate 或 Qdrant
│
├─ 延迟敏感 + 运维简单?
│ └─ YES → Qdrant(Rust 高性能,单二进制)
│
└─ 需要 GPU 加速索引构建?
└─ YES → Milvus(GPU-IVF/GPU-CAGRA)
Qdrant 工程实践
部署与集合创建
from qdrant_client import QdrantClient, models
# Connect to Qdrant
client = QdrantClient(url="http://localhost:6333")
# Create collection with HNSW index
client.create_collection(
collection_name="documents",
vectors_config=models.VectorParams(
size=1536, # OpenAI text-embedding-3-small
distance=models.Distance.COSINE,
on_disk=False, # Keep vectors in RAM
hnsw_config=models.HnswConfigDiff(
m=16, # Max connections per node
ef_construct=200, # Build-time search width
full_scan_threshold=10000, # Switch to brute force below this
),
quantization_config=models.ScalarQuantization(
scalar=models.ScalarQuantizationConfig(
type=models.ScalarType.INT8, # 4x memory reduction
quantile=0.99, # Clip outliers
always_ram=True, # Keep quantized vectors in RAM
),
),
),
# Sparse vectors for hybrid search
sparse_vectors_config={
"bm25": models.SparseVectorParams(
modifier=models.Modifier.IDF,
),
},
# Optimized WAL configuration
optimizers_config=models.OptimizersConfigDiff(
indexing_threshold=20000,
memmap_threshold=50000,
),
)
数据写入与检索
import numpy as np
from qdrant_client import models
# Batch upsert with payload
points = [
models.PointStruct(
id=doc["id"],
vector={
"": embedding, # Dense vector (default name)
"bm25": models.SparseVector( # Sparse vector for BM25
indices=sparse_indices,
values=sparse_values,
),
},
payload={
"title": doc["title"],
"content": doc["content"],
"category": doc["category"],
"created_at": doc["created_at"],
"tenant_id": doc["tenant_id"],
},
)
for doc, embedding, sparse_indices, sparse_values in batch
]
client.upsert(
collection_name="documents",
points=points,
wait=True,
)
# Hybrid search: dense + sparse with RRF fusion
results = client.query_points(
collection_name="documents",
prefetch=[
# Dense vector search
models.Prefetch(
query=query_embedding,
using="",
limit=20,
),
# Sparse BM25 search
models.Prefetch(
query=models.SparseVector(
indices=query_sparse_indices,
values=query_sparse_values,
),
using="bm25",
limit=20,
),
],
# Reciprocal Rank Fusion
query=models.FusionQuery(fusion=models.Fusion.RRF),
# Metadata filtering
query_filter=models.Filter(
must=[
models.FieldCondition(
key="tenant_id",
match=models.MatchValue(value="tenant_001"),
),
models.FieldCondition(
key="category",
match=models.MatchAny(any=["tech", "science"]),
),
],
),
limit=10,
with_payload=True,
score_threshold=0.5,
)
pgvector 工程实践
基础配置
-- Enable extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create table with vector column
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
category TEXT,
tenant_id TEXT NOT NULL,
embedding vector(1536), -- OpenAI embedding dimension
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- HNSW index (recommended for most cases)
CREATE INDEX idx_documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Partial index for multi-tenant isolation
CREATE INDEX idx_documents_tenant_embedding
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200)
WHERE tenant_id = 'tenant_001';
-- Set search parameters
SET hnsw.ef_search = 100;
混合搜索实现
-- Hybrid search: vector similarity + full-text + metadata filter
WITH semantic AS (
SELECT
id,
title,
content,
1 - (embedding <=> $1::vector) AS vector_score
FROM documents
WHERE tenant_id = $2
ORDER BY embedding <=> $1::vector
LIMIT 20
),
fulltext AS (
SELECT
id,
title,
content,
ts_rank(
to_tsvector('english', content),
plainto_tsquery('english', $3)
) AS text_score
FROM documents
WHERE tenant_id = $2
AND to_tsvector('english', content) @@ plainto_tsquery('english', $3)
LIMIT 20
)
-- RRF fusion
SELECT
COALESCE(s.id, f.id) AS id,
COALESCE(s.title, f.title) AS title,
COALESCE(s.content, f.content) AS content,
COALESCE(1.0 / (60 + s.rank), 0) +
COALESCE(1.0 / (60 + f.rank), 0) AS rrf_score
FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY vector_score DESC) AS rank FROM semantic) s
FULL OUTER JOIN (SELECT *, ROW_NUMBER() OVER (ORDER BY text_score DESC) AS rank FROM fulltext) f
ON s.id = f.id
ORDER BY rrf_score DESC
LIMIT 10;
生产部署最佳实践
索引参数调优
HNSW 参数调优指南:
数据规模 M ef_construction ef_search 召回率目标
< 10K 8 100 50 99%+
10K-100K 16 200 100 98%+
100K-1M 16 256 200 97%+
1M-10M 32 300 300 96%+
10M-100M 48 400 400 95%+
> 100M 64 500 500 考虑分片
内存估算公式 (HNSW):
memory ≈ num_vectors x (dim x 4 + M x 2 x 8 + overhead)
示例: 1000 万 x 1536 维:
≈ 10M x (1536 x 4 + 16 x 2 x 8 + 64)
≈ 10M x 6464 bytes
≈ 60 GB
监控指标
| 指标 | 正常范围 | 告警阈值 | 含义 |
|---|---|---|---|
| p99 搜索延迟 | <10ms | >50ms | 索引退化或内存不足 |
| 召回率 | >95% | <90% | 索引参数需调优 |
| 内存使用率 | <80% | >90% | 需要扩容或开启量化 |
| 写入吞吐 | >1000 qps | <100 qps | 索引重建阻塞 |
| 段数量 | <50 | >200 | 需要手动触发合并 |
数据生命周期管理
# Qdrant: Time-based data expiration with payload index
from qdrant_client import models
from datetime import datetime, timedelta
# Create payload index on timestamp field
client.create_payload_index(
collection_name="documents",
field_name="created_at",
field_schema=models.PayloadSchemaType.DATETIME,
)
# Delete expired documents (older than 90 days)
cutoff = (datetime.now() - timedelta(days=90)).isoformat()
client.delete(
collection_name="documents",
points_selector=models.FilterSelector(
filter=models.Filter(
must=[
models.FieldCondition(
key="created_at",
range=models.DatetimeRange(lt=cutoff),
),
],
),
),
)
总结
- pgvector 适合起步:如果已有 PostgreSQL 且数据量在千万级以下,pgvector 是零额外运维成本的安全选择。
- Qdrant 适合中大规模:Rust 实现的高性能,运维简单(单二进制),原生混合搜索,适合大多数 RAG 场景。
- Milvus 适合超大规模:十亿级向量分布式检索,GPU 加速索引,但运维复杂度高。
- 索引选择首选 HNSW:在绝大多数场景下,HNSW 提供了最好的精度/延迟平衡。
- 混合搜索是趋势:纯语义搜索的精度瓶颈需要 BM25 + 向量的融合来突破,RRF 是最简单有效的融合策略。
Maurice | maurice_wen@proton.me