常用向量数据库怎么选型?
结论:选型看四个约束——数据规模、过滤复杂度、运维能力、生态耦合,而不是看 benchmark 排名。 已有 PostgreSQL 且数据在千万级以下,pgvector 最省事;要"关键词 + 向量"一体,选 Elasticsearch/OpenSearch; 上亿级或高并发写入,才轮到 Milvus、Qdrant 这类专用库;完全不想碰运维,就走托管服务。
1. 候选清单与定位
| 方案 | 形态 | 强项 | 短板 | 适合 |
|---|---|---|---|---|
| pgvector | PostgreSQL 扩展 | 零新增组件、SQL 过滤、事务一致 | 千万级以上吃紧,索引重建慢 | 已有 PG、百万级 |
| Elasticsearch / OpenSearch | 搜索引擎 | BM25 与 kNN 同库、聚合分析成熟 | 向量性能非专精,堆内存开销大 | 混合检索为主 |
| Milvus | 分布式专用库 | 亿级规模、多种索引、可上 GPU | 依赖 etcd/对象存储,运维重 | 平台型、大规模 |
| Qdrant | 专用库(Rust) | 过滤性能好、部署简单、payload 灵活 | 生态相对年轻 | 中小团队自建 |
| Weaviate | 专用库 | 内置向量化模块、GraphQL 接口 | 资源占用偏高 | 快速搭原型 |
| Chroma | 嵌入式 | 开发体验最好,几行代码起服务 | 规模化与高可用弱 | POC、本地实验 |
| Pinecone / Zilliz Cloud | 托管服务 | 免运维、弹性伸缩 | 成本、数据出域合规 | 无基础设施团队 |
2. 决策树
已有 PostgreSQL?
├─ 是,且片段数 < 1000 万 ──────────▶ pgvector(省一个组件就是省钱)
└─ 否
├─ 需要 BM25 + 向量混合 ─────────▶ Elasticsearch / OpenSearch
├─ 片段数 > 5000 万 或 写入 QPS 高 ─▶ Milvus / Qdrant 集群
├─ 合规不允许数据出域 ───────────▶ 自建 Qdrant / Milvus
└─ 团队没有运维 ─────────────────▶ 托管服务
3. 关键维度横向对比
| 维度 | pgvector | Elasticsearch | Milvus | Qdrant |
|---|---|---|---|---|
| 典型规模 | 百万~千万 | 千万~亿 | 亿级以上 | 千万~亿 |
| 过滤 + ANN | SQL WHERE,成熟 | filter 上下文,成熟 | 标量过滤,支持 | payload 过滤,强 |
| 混合检索 | 需自己拼 BM25 | 原生 BM25 + kNN | 需外挂稀疏向量 | 支持稀疏向量 |
| 事务 | 完整 ACID | 无跨文档事务 | 无 | 无 |
| 运维复杂度 | 低(复用 PG) | 中 | 高 | 低~中 |
| 主要索引 | HNSW / IVFFlat | HNSW | HNSW/IVF/PQ/DiskANN | HNSW |
数字只是量级参考,真实上限取决于维度、副本数、过滤选择性和硬件,务必用自己的数据压测。
4. 代码:同一件事,两种写法
# Qdrant:建集合 + 建 payload 索引 + 带过滤检索
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
client.recreate_collection(
collection_name="kb",
vectors_config=models.VectorParams(size=1536, distance=models.Distance.COSINE),
)
# 过滤字段一定要建索引,否则过滤退化成逐条扫描
client.create_payload_index("kb", field_name="source", field_schema=models.PayloadSchemaType.KEYWORD)
client.upsert("kb", points=[
models.PointStruct(id=i, vector=vec, payload={"text": txt, "source": src})
for i, (vec, txt, src) in enumerate(zip(vectors, texts, sources))
])
hits = client.search(
collection_name="kb", query_vector=qvec, limit=20, with_payload=True,
query_filter=models.Filter(must=[
models.FieldCondition(key="source", match=models.MatchValue(value="employee-handbook"))
]),
)
-- pgvector:同样的语义,用 SQL 表达,还能直接 JOIN 业务表
SELECT c.id, c.content, 1 - (c.embedding <=> $1) AS score
FROM doc_chunk c
JOIN doc_meta m ON m.source = c.source
WHERE m.tenant_id = $2 AND m.status = 'active'
ORDER BY c.embedding <=> $1
LIMIT 20;
5. 迁移与避坑清单
| 坑 | 说明 |
|---|---|
| 距离度量混用 | 建库用 cosine、查询用 L2,结果全乱;三者(模型、归一化、度量)必须成套迁移 |
| 维度不一致 | 换 embedding 模型必须重建集合,不能只改配置 |
| 过滤字段没索引 | Qdrant 的 payload 索引、Milvus 的标量索引,漏建就是全扫 |
| 写入可见延迟 | 部分库是近实时,写完立刻查可能查不到,压测时要算进去 |
| 删除不回收 | 软删除堆积会让召回和内存一起退化,定期 compaction |
工程上建议抽一个 VectorStore 接口(upsert / query / delete / health),把 SDK 调用收敛到一处,换库时只改适配层。
6. 常见追问
Q:pgvector 到底能撑多大? 百万级很稳;千万级要分区、调 maintenance_work_mem、接受更长的索引构建时间;再往上不如换专用库,别硬扛。
Q:为什么 Elasticsearch 的向量检索不如专用库? 段(segment)结构、堆内存模型和近实时刷新机制不是为向量设计的,图索引构建与合并成本更高。
Q:多租户怎么隔离? 租户少用独立集合,租户多用 payload 字段过滤 + 路由;隔离性和成本要权衡,别一上来就"每租户一集合"。
Q:能不能自己包一层 FAISS? 只有在极端性能需求、且团队愿意自己处理持久化、增删、副本、故障恢复时才考虑,通常不划算。
小结
选型的正确答案往往是最"无聊"的那个:能复用现有基础设施、过滤够用、运维扛得住,就是好选择。把省下的精力投到分块和评估上,回报更高。