向量数据库怎么选?Chroma、FAISS、Milvus 与 pgvector 对比
系列:从零构建企业 RAG 知识库(第 8 篇)
1. 先纠正一个概念
FAISS 更准确地说是向量相似度搜索库,不是完整的网络数据库;Chroma 面向 AI 应用开发体验;Milvus 是专门的分布式向量数据库;pgvector 是 PostgreSQL 的向量扩展。
产品能力和版本会变化,最终选型必须用目标版本、数据规模和部署环境做 PoC,不能只看本文或营销表格。
2. 先问业务约束
- 数据量和增量写入速度;
- 查询 QPS 与 P95 延迟;
- 过滤条件是否复杂;
- 是否已有 PostgreSQL 运维能力;
- 是否需要分布式扩展和高可用;
- 备份、恢复、监控和权限要求;
- 团队能承担多少新基础设施。
3. 稳定的定位对比
| 候选 | 更适合的起点 | 主要优势 | 重点验证 |
|---|---|---|---|
| FAISS | 本地实验、离线索引、单机服务 | 算法丰富、嵌入应用灵活 | 持久化、元数据过滤、多实例同步需自行设计 |
| Chroma | 原型和中小型 AI 应用 | 上手快、文档/元数据抽象友好 | 目标部署模式、并发、备份和升级 |
| Milvus | 大规模、专用向量平台 | 分布式扩展、索引与检索能力完整 | 运维复杂度、资源成本、过滤性能 |
| pgvector | 已使用 PostgreSQL、关系与向量联合查询 | 事务、SQL、权限体系可复用 | 数据规模、索引参数、写入与查询竞争 |
"百万向量一定选某产品"不是事实。向量维度、过滤选择性、索引类型、硬件和召回目标都会改变结果。
4. 用统一接口隔离基础设施
python
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class VectorRecord:
record_id: str
tenant_id: str
document_id: str
vector: tuple[float, ...]
text: str
class VectorStore(Protocol):
def upsert(self, records: list[VectorRecord]) -> None:
"""写入或更新向量记录。"""
def search(
self,
query_vector: tuple[float, ...],
tenant_id: str,
allowed_documents: frozenset[str],
top_k: int,
) -> list[VectorRecord]:
"""权限过滤必须是接口契约的一部分。"""
def delete_document(self, tenant_id: str, document_id: str) -> int:
"""删除文档的全部派生向量。"""
业务层依赖该接口,而不是散落某个 SDK 的调用。换库并不自动容易,但至少边界明确、测试可复用。
5. 一个内存实现验证契约
python
from math import sqrt
def cosine(left: tuple[float, ...], right: tuple[float, ...]) -> float:
if len(left) != len(right) or not left:
raise ValueError("向量维度不一致")
denominator = sqrt(sum(x * x for x in left)) * sqrt(
sum(x * x for x in right)
)
return 0.0 if denominator == 0 else sum(
x * y for x, y in zip(left, right)
) / denominator
class InMemoryVectorStore:
def __init__(self) -> None:
self._records: dict[str, VectorRecord] = {}
def upsert(self, records: list[VectorRecord]) -> None:
for record in records:
self._records[record.record_id] = record
def search(
self,
query_vector: tuple[float, ...],
tenant_id: str,
allowed_documents: frozenset[str],
top_k: int,
) -> list[VectorRecord]:
candidates = [
record for record in self._records.values()
if record.tenant_id == tenant_id
and record.document_id in allowed_documents
]
return sorted(
candidates,
key=lambda item: (-cosine(query_vector, item.vector), item.record_id),
)[:top_k]
def delete_document(self, tenant_id: str, document_id: str) -> int:
targets = [
key for key, value in self._records.items()
if value.tenant_id == tenant_id
and value.document_id == document_id
]
for key in targets:
del self._records[key]
return len(targets)
6. 可复验的权限与删除测试
python
def test_store_contract_filters_tenant_and_deletes() -> None:
store = InMemoryVectorStore()
store.upsert([
VectorRecord("a", "tenant-a", "doc-1", (1.0, 0.0), "A"),
VectorRecord("b", "tenant-b", "doc-2", (1.0, 0.0), "B"),
])
result = store.search(
(1.0, 0.0),
tenant_id="tenant-a",
allowed_documents=frozenset({"doc-1", "doc-2"}),
top_k=10,
)
assert [item.record_id for item in result] == ["a"]
assert store.delete_document("tenant-a", "doc-1") == 1
assert not store.search((1.0, 0.0), "tenant-a", frozenset({"doc-1"}), 10)
同一契约测试应对每个真实适配器运行。
7. PoC 应该测什么
python
from dataclasses import dataclass
@dataclass(frozen=True)
class BenchmarkResult:
candidate: str
dataset_version: str
recall_at_10: float
p95_ms: float
filtered_p95_ms: float
write_per_second: float
restore_verified: bool
def meets_gate(result: BenchmarkResult) -> bool:
"""门槛是示例,必须按业务 SLA 修改。"""
return (
result.recall_at_10 >= 0.90
and result.filtered_p95_ms <= 200
and result.restore_verified
)
PoC 使用相同向量、相同过滤条件、相同硬件预算和预热规则。除了性能,还要实际演练备份恢复、扩容、升级、删除和监控。
8. 对抗性审查
- 不用未过滤的公开 Benchmark 代替自己的查询;
- 检查权限过滤是在索引内执行还是查询后裁剪;
- 删除文档必须删除所有副本和缓存;
- 记录 Embedding 模型与维度;
- 索引近似搜索要报告召回损失;
- 评估总成本:软件、硬件、人员和迁移;
- 避免业务代码绑定厂商特有字段。
9. 总结
小柒的建议是从最小运维成本开始:本地实验可用 FAISS/Chroma;已有 PostgreSQL 且规模合适时优先评估 pgvector;确需独立分布式向量平台时再评估 Milvus。最终结论必须由同场 PoC 证据决定。