向量数据库怎么选?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. 用统一接口隔离基础设施
fromdataclassesimportdataclassfromtypingimportProtocol@dataclass(frozen=True)classVectorRecord:record_id:strtenant_id:strdocument_id:strvector:tuple[float,...]text:strclassVectorStore(Protocol):defupsert(self,records:list[VectorRecord])->None:"""写入或更新向量记录。"""defsearch(self,query_vector:tuple[float,...],tenant_id:str,allowed_documents:frozenset[str],top_k:int,)->list[VectorRecord]:"""权限过滤必须是接口契约的一部分。"""defdelete_document(self,tenant_id:str,document_id:str)->int:"""删除文档的全部派生向量。"""业务层依赖该接口,而不是散落某个 SDK 的调用。换库并不自动容易,但至少边界明确、测试可复用。
5. 一个内存实现验证契约
frommathimportsqrtdefcosine(left:tuple[float,...],right:tuple[float,...])->float:iflen(left)!=len(right)ornotleft:raiseValueError("向量维度不一致")denominator=sqrt(sum(x*xforxinleft))*sqrt(sum(x*xforxinright))return0.0ifdenominator==0elsesum(x*yforx,yinzip(left,right))/denominatorclassInMemoryVectorStore:def__init__(self)->None:self._records:dict[str,VectorRecord]={}defupsert(self,records:list[VectorRecord])->None:forrecordinrecords:self._records[record.record_id]=recorddefsearch(self,query_vector:tuple[float,...],tenant_id:str,allowed_documents:frozenset[str],top_k:int,)->list[VectorRecord]:candidates=[recordforrecordinself._records.values()ifrecord.tenant_id==tenant_idandrecord.document_idinallowed_documents]returnsorted(candidates,key=lambdaitem:(-cosine(query_vector,item.vector),item.record_id),)[:top_k]defdelete_document(self,tenant_id:str,document_id:str)->int:targets=[keyforkey,valueinself._records.items()ifvalue.tenant_id==tenant_idandvalue.document_id==document_id]forkeyintargets:delself._records[key]returnlen(targets)6. 可复验的权限与删除测试
deftest_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_idforiteminresult]==["a"]assertstore.delete_document("tenant-a","doc-1")==1assertnotstore.search((1.0,0.0),"tenant-a",frozenset({"doc-1"}),10)同一契约测试应对每个真实适配器运行。
7. PoC 应该测什么
fromdataclassesimportdataclass@dataclass(frozen=True)classBenchmarkResult:candidate:strdataset_version:strrecall_at_10:floatp95_ms:floatfiltered_p95_ms:floatwrite_per_second:floatrestore_verified:booldefmeets_gate(result:BenchmarkResult)->bool:"""门槛是示例,必须按业务 SLA 修改。"""return(result.recall_at_10>=0.90andresult.filtered_p95_ms<=200andresult.restore_verified)PoC 使用相同向量、相同过滤条件、相同硬件预算和预热规则。除了性能,还要实际演练备份恢复、扩容、升级、删除和监控。
8. 对抗性审查
- 不用未过滤的公开 Benchmark 代替自己的查询;
- 检查权限过滤是在索引内执行还是查询后裁剪;
- 删除文档必须删除所有副本和缓存;
- 记录 Embedding 模型与维度;
- 索引近似搜索要报告召回损失;
- 评估总成本:软件、硬件、人员和迁移;
- 避免业务代码绑定厂商特有字段。
9. 总结
小柒的建议是从最小运维成本开始:本地实验可用 FAISS/Chroma;已有 PostgreSQL 且规模合适时优先评估 pgvector;确需独立分布式向量平台时再评估 Milvus。最终结论必须由同场 PoC 证据决定。