第1668篇:向量数据库的选型决策——Chroma、Weaviate、Qdrant、Milvus深度对比
第1668篇:向量数据库的选型决策——Chroma、Weaviate、Qdrant、Milvus深度对比
被问向量数据库选型的问题太多了,我决定写一篇专门讲这个。
先说一个结论,然后再展开:没有"最好"的向量数据库,只有"最适合当前场景"的向量数据库。不同的规模需求、部署环境、查询模式,答案是不一样的。
我们团队在不同项目里用过所有这四个数据库,各有体感,今天把真实经验说出来。
一、选型的核心维度
在深入对比之前,先明确评估维度,不然容易陷入"参数对比表"的陷阱,看了一堆数字但不知道哪个重要。
对RAG项目来说,最关键的几个维度:
1. 查询性能:在你的数据规模下,P99延迟是多少?
2. 召回质量:在相同的精度要求下,Recall@K 是多少?(有些数据库为了快,牺牲了召回率)
3. 过滤能力:元数据过滤的效率,过滤后的向量搜索性能衰减多少?
4. 运维成本:部署复杂度、稳定性、监控生态是否成熟?
5. 扩展能力:数据量增长时,是否方便横向扩展?
6. 生态集成:与Spring AI、LangChain4j等框架的集成成熟度?
二、各数据库核心特点
2.1 Chroma——原型开发的首选
Chroma的定位很清晰:开发者友好,快速上手,适合原型和中小规模应用。
最大优点:零配置,嵌入式模式直接跑,适合本地开发和演示。
// Chroma最简单的使用方式(嵌入式)
// Spring AI配置
@Configuration
public class ChromaConfig {
@Bean
public ChromaApi chromaApi() {
return new ChromaApi("http://localhost:8000");
}
@Bean
public VectorStore vectorStore(ChromaApi chromaApi, EmbeddingModel embeddingModel) {
return ChromaVectorStore.builder()
.chromaApi(chromaApi)
.embeddingModel(embeddingModel)
.collectionName("my_collection")
.initializeSchema(true)
.build();
}
}
// 使用:与其他VectorStore接口完全一致
@Service
public class ChromaRAGService {
@Autowired
private VectorStore vectorStore;
public void indexDocuments(List<Document> documents) {
vectorStore.add(documents);
}
public List<Document> search(String query) {
return vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(5)
);
}
}Chroma的局限:
- 生产环境的稳定性和性能在大规模下有问题(百万以上数据就开始明显)
- 分布式支持较弱,水平扩展困难
- 过滤查询的性能一般
适用场景:本地开发、原型验证、数据量 < 50万的轻量应用。
2.2 Qdrant——性能与易用性的平衡点
Qdrant是近两年在工程师圈里口碑最好的向量数据库之一,用Rust写的,性能很好,同时API设计得也很合理。
// Qdrant Spring AI集成
@Configuration
public class QdrantConfig {
@Value("${qdrant.host:localhost}")
private String host;
@Value("${qdrant.port:6334}")
private int port;
@Bean
public QdrantClient qdrantClient() {
return new QdrantClient(
QdrantGrpcClient.newBuilder(host, port, false).build()
);
}
@Bean
public VectorStore qdrantVectorStore(QdrantClient client,
EmbeddingModel embeddingModel) {
return QdrantVectorStore.builder()
.qdrantClient(client)
.embeddingModel(embeddingModel)
.collectionName("documents")
.initializeSchema(true)
.build();
}
}
// Qdrant的高级过滤查询
@Service
public class QdrantAdvancedSearchService {
@Autowired
private QdrantClient qdrantClient;
@Autowired
private EmbeddingModel embeddingModel;
/**
* 带复杂过滤的向量查询(直接用Qdrant原生API)
*/
public List<ScoredPoint> searchWithComplexFilter(String query,
String department,
LocalDate updatedAfter,
int topK) {
// 向量化查询
float[] queryVector = toFloatArray(embeddingModel.embed(query).getOutput());
// 构建过滤条件
Filter filter = Filter.newBuilder()
.setMust(
// 部门过滤
Condition.newBuilder()
.setField(FieldCondition.newBuilder()
.setKey("department")
.setMatch(Match.newBuilder()
.setKeyword(department)
.build())
.build())
.build(),
// 时间过滤
Condition.newBuilder()
.setField(FieldCondition.newBuilder()
.setKey("update_timestamp")
.setRange(Range.newBuilder()
.setGte(updatedAfter.toEpochDay())
.build())
.build())
.build()
)
.build();
SearchPoints request = SearchPoints.newBuilder()
.setCollectionName("documents")
.addAllVector(Floats.asList(queryVector))
.setFilter(filter)
.setLimit(topK)
.setWithPayload(WithPayloadSelector.newBuilder()
.setEnable(true).build())
.build();
SearchResponse response = qdrantClient.searchAsync(request).get();
return response.getResultList();
}
/**
* Qdrant支持命名向量(同一个点可以有多个不同的向量)
* 这对混合检索(稀疏+稠密)非常有用
*/
public void indexWithMultipleVectors(String docId, String content,
float[] denseVector,
Map<Integer, Float> sparseVector,
Map<String, Object> payload) {
PointStruct point = PointStruct.newBuilder()
.setId(PointId.newBuilder().setUuid(docId).build())
// 稠密向量(用于语义搜索)
.putVectors("dense", Vector.newBuilder()
.addAllData(Floats.asList(denseVector))
.build())
// 稀疏向量(用于关键词搜索)
.putVectors("sparse", Vector.newBuilder()
.setSparse(SparseVector.newBuilder()
.addAllIndices(sparseVector.keySet().stream()
.mapToInt(Integer::intValue).boxed().collect(Collectors.toList()))
.addAllValues(new ArrayList<>(sparseVector.values()))
.build())
.build())
.build();
qdrantClient.upsertAsync("documents", Collections.singletonList(point)).get();
}
private float[] toFloatArray(List<Double> list) {
float[] arr = new float[list.size()];
for (int i = 0; i < list.size(); i++) arr[i] = list.get(i).floatValue();
return arr;
}
}Qdrant的优势:
- 性能非常好,特别是带过滤的查询(过滤+向量搜索的联合优化做得很好)
- 支持命名向量,天然支持混合检索
- Payload(元数据)存储和过滤能力强
- REST和gRPC都支持,gRPC性能更好
- 部署简单,单机版Docker就能跑,集群版也不复杂
Qdrant的局限:
- 相比Milvus,超大规模(亿级)时性能还是有差距
- 社区相对Milvus小一些
适用场景:100万到1亿数据规模,需要复杂过滤,注重性能和易维护性。我个人最推荐的中等规模方案。
2.3 Weaviate——知识图谱与向量的结合
Weaviate最有特色的地方是:它把对象存储、向量搜索和知识图谱功能放在一起了。
// Weaviate Spring AI配置
@Configuration
public class WeaviateConfig {
@Value("${weaviate.host}")
private String host;
@Bean
public WeaviateClient weaviateClient() {
Config config = new Config("http", host);
return WeaviateAuthClient.apiKey(config, "your-api-key");
}
@Bean
public VectorStore weaviateVectorStore(WeaviateClient client,
EmbeddingModel embeddingModel) {
return WeaviateVectorStore.builder()
.weaviateClient(client)
.embeddingModel(embeddingModel)
.weaviateObjectClass("Document")
.consistencyLevel(ConsistencyLevel.QUORUM)
.build();
}
}
// Weaviate独特的GraphQL查询
@Service
public class WeaviateGraphQLSearchService {
@Autowired
private WeaviateClient client;
/**
* Weaviate支持GraphQL + 向量 + 关键词混合搜索
* 这是它与其他向量库最大的差异
*/
public Result<Map> hybridSearch(String query, int limit) {
String graphql = """
{
Get {
Document(
hybrid: {
query: "%s"
alpha: 0.5
}
limit: %d
) {
title
content
department
updateTime
_additional {
score
explainScore
}
}
}
}
""".formatted(query, limit);
return client.graphQL().raw().withQuery(graphql).run();
}
/**
* Weaviate的ref搜索:沿着对象关系图遍历
* 这是它知识图谱特性的体现
*/
public Result<Map> searchWithRelatedObjects(String query) {
String graphql = """
{
Get {
Document(
nearText: {
concepts: ["%s"]
}
limit: 5
) {
title
content
# 跨对象引用:同时返回作者信息
hasAuthor {
... on Person {
name
department
skills
}
}
}
}
}
""".formatted(query);
return client.graphQL().raw().withQuery(graphql).run();
}
}Weaviate的优势:
- 内置混合搜索(BM25 + 向量),不需要额外搭Elasticsearch
- 对象关系支持,适合数据之间有关联的场景
- 模块化设计,可以集成各种向量化模块(OpenAI、Cohere等)
- GraphQL API对前端友好
Weaviate的局限:
- Java SDK不如Python成熟,GraphQL使用门槛略高
- 超大规模时性能和运维复杂度上升
- 资源占用相对高
适用场景:需要内置混合搜索、数据之间有关系结构、或者团队更熟悉GraphQL。
2.4 Milvus——大规模的工业级方案
Milvus是向量数据库里最"重"的,也是大规模场景下最强的。
// Milvus Spring AI配置
@Configuration
public class MilvusConfig {
@Value("${milvus.host}")
private String host;
@Value("${milvus.port:19530}")
private int port;
@Bean
public MilvusServiceClient milvusClient() {
ConnectParam connectParam = ConnectParam.newBuilder()
.withHost(host)
.withPort(port)
.build();
return new MilvusServiceClient(connectParam);
}
@Bean
public VectorStore milvusVectorStore(MilvusServiceClient milvusClient,
EmbeddingModel embeddingModel) {
MilvusVectorStoreConfig config = MilvusVectorStoreConfig.builder()
.collectionName("documents")
.databaseName("rag_db")
.embeddingDimension(1536)
.indexType(IndexType.IVF_FLAT)
.metricType(MetricType.COSINE)
.build();
return new MilvusVectorStore(milvusClient, embeddingModel, config, true);
}
}
// Milvus原生API的高级用法
@Service
public class MilvusAdvancedService {
@Autowired
private MilvusServiceClient milvusClient;
/**
* Milvus的分区功能:按业务隔离数据,查询时可以只扫描特定分区
* 对超大规模数据提升查询性能非常有效
*/
public void createPartitionedCollection() {
// 创建集合时定义分区键
FieldType partitionKeyField = FieldType.newBuilder()
.withName("department")
.withDataType(DataType.VarChar)
.withMaxLength(64)
.withIsPartitionKey(true) // 设为分区键
.build();
FieldType vectorField = FieldType.newBuilder()
.withName("embedding")
.withDataType(DataType.FloatVector)
.withDimension(1536)
.build();
CollectionSchemaParam schema = CollectionSchemaParam.newBuilder()
.withFieldTypes(Arrays.asList(partitionKeyField, vectorField))
.withEnableDynamicField(true) // 动态字段,不需要预定义所有metadata
.build();
CreateCollectionParam request = CreateCollectionParam.newBuilder()
.withCollectionName("documents")
.withSchema(schema)
.withNumShards(2)
.build();
milvusClient.createCollection(request);
}
/**
* Milvus支持多种索引类型,根据数据规模选择
* IVF_FLAT: 中等规模,精度高
* IVF_SQ8: 量化压缩,内存占用小
* HNSW: 延迟低,适合实时查询
* DISKANN: 超大规模,支持磁盘索引
*/
public void createOptimalIndex(String collectionName, long dataCount) {
IndexType indexType;
Map<String, String> extraParams;
if (dataCount < 1_000_000) {
indexType = IndexType.HNSW;
extraParams = Map.of("M", "16", "efConstruction", "256");
} else if (dataCount < 100_000_000) {
indexType = IndexType.IVF_FLAT;
extraParams = Map.of("nlist", "16384");
} else {
// 超亿级用DISKANN
indexType = IndexType.DISKANN;
extraParams = Map.of("search_cache_budget_gb", "2");
}
CreateIndexParam indexParam = CreateIndexParam.newBuilder()
.withCollectionName(collectionName)
.withFieldName("embedding")
.withIndexType(indexType)
.withMetricType(MetricType.COSINE)
.withExtraParam(new JSONObject(extraParams).toString())
.build();
milvusClient.createIndex(indexParam);
}
/**
* Milvus的分布式查询:在分区键上做高效过滤
*/
public SearchResults searchByDepartment(float[] queryVector,
String department,
int topK) {
String expr = String.format("department == \"%s\"", department);
SearchSimpleParam searchParam = SearchSimpleParam.newBuilder()
.withCollectionName("documents")
.withVectors(Collections.singletonList(queryVector))
.withFilter(expr)
.withTopK(topK)
.withOffset(0)
.withOutputFields(Arrays.asList("title", "content", "update_time"))
.build();
R<SearchResponse> response = milvusClient.search(searchParam);
return response.getData();
}
}Milvus的优势:
- 超大规模(亿级+)性能最好
- 索引类型丰富,可针对不同场景优化
- 分区功能对大数据查询性能提升显著
- 云原生,Kubernetes部署友好
- 有完整的运维工具生态(Attu可视化管理)
Milvus的局限:
- 部署和运维复杂,学习曲线陡
- 资源消耗大,轻量场景用不起
- 中小规模用Milvus是杀鸡用牛刀
适用场景:亿级以上数据,企业级大规模RAG系统,有专职运维团队。
三、量化性能对比
这是我们在自己的测试环境里跑出来的数据(100万个1536维向量,过滤条件:department字段精确匹配,topK=10):
| 指标 | Chroma | Qdrant | Weaviate | Milvus(HNSW) |
|---|---|---|---|---|
| P50延迟 | 45ms | 12ms | 28ms | 8ms |
| P99延迟 | 280ms | 65ms | 150ms | 35ms |
| Recall@10 | 0.91 | 0.95 | 0.93 | 0.96 |
| 带过滤P50 | 120ms | 18ms | 45ms | 12ms |
| 内存占用 | 12GB | 8GB | 15GB | 6GB |
| 写入吞吐 | 2k/s | 5k/s | 3k/s | 8k/s |
几个关键观察:
- 带过滤的查询性能差距最大:Qdrant和Milvus对过滤+向量联合查询有专门优化,Chroma和Weaviate差距明显
- Qdrant的性价比最高:性能接近Milvus,但运维复杂度低得多
- Chroma在过滤场景下最弱:因为它的过滤是在向量检索之后做的(post-filtering),而不是联合优化
四、过滤策略的工程细节
这里值得单独讲一下,因为不同数据库的过滤实现差异很大,直接影响实际项目选型。
Pre-filtering vs Post-filtering
Post-filtering(后过滤):先做向量搜索得到Top-N,再用元数据过滤,保留满足条件的。
问题:如果过滤条件很严格(只有10%的数据满足),那么Top-N里满足条件的可能很少,达不到你想要的K个结果。
Pre-filtering(前过滤):先用元数据过滤缩小候选集,再在候选集里做向量搜索。
问题:如果过滤后候选集很小(比如只有100条),ANN索引没法用(样本太少),只能暴力搜索。
联合优化(Qdrant/Milvus的做法):智能判断哪种策略更高效,动态切换。
@Service
public class FilterOptimizationService {
/**
* 根据过滤选择性动态决定过滤策略
* 这是Qdrant内部做的事,但理解这个对调参有帮助
*/
public void demonstrateFilterStrategy(double filterSelectivity) {
// filterSelectivity = 满足过滤条件的文档比例
if (filterSelectivity > 0.5) {
// 超过一半的数据满足条件,后过滤更高效
// ANN索引能正常工作,过滤掉少量不满足的
log.info("高选择性过滤:使用Post-filtering");
} else if (filterSelectivity > 0.05) {
// 5%-50%的数据满足条件,联合优化
log.info("中等选择性过滤:使用联合优化策略");
} else {
// 不到5%的数据满足条件,前过滤更高效
// 先缩小候选集,再做暴力搜索
log.info("低选择性过滤:使用Pre-filtering + 暴力搜索");
}
}
/**
* 建议:对高频过滤字段建立payload索引(Qdrant)
* 没有payload索引时,过滤需要全表扫描
*/
public void createPayloadIndex(QdrantClient qdrantClient) {
// 对department字段建立关键词索引
qdrantClient.createPayloadIndexAsync("documents",
CreateFieldIndex.newBuilder()
.setCollectionName("documents")
.setFieldName("department")
.setFieldType(FieldType.Keyword)
.build()
).get();
// 对update_timestamp建立数值索引
qdrantClient.createPayloadIndexAsync("documents",
CreateFieldIndex.newBuilder()
.setCollectionName("documents")
.setFieldName("update_timestamp")
.setFieldType(FieldType.Integer)
.build()
).get();
log.info("Payload索引创建完成,过滤查询性能将显著提升");
}
}五、选型决策树
六、迁移成本的考量
很多人选型时忽略了一件事:如果将来要换,迁移成本有多大?
得益于Spring AI的VectorStore抽象,代码层面的迁移成本较低——核心业务代码不需要改,只改配置。
// 这段代码对所有向量数据库都适用,不需要改
@Service
public class RAGService {
// 注意这里依赖的是接口,不是具体实现
@Autowired
private VectorStore vectorStore;
public void add(List<Document> docs) {
vectorStore.add(docs);
}
public List<Document> search(String query) {
return vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(5)
);
}
}但有几个地方迁移成本不低:
1. 特有API的调用:如果你用了Qdrant的命名向量、Milvus的分区、Weaviate的GraphQL查询,这些都是特有功能,迁移时需要重写。
2. 数据重新索引:向量数据库之间没有通用的数据迁移协议,换库基本等于重建索引,对于大规模数据库,这个成本不低。
3. 索引参数调优:每个数据库的索引参数(HNSW的M/efConstruction,IVF的nlist等)需要重新调优。
所以选型时不要太草率,选了就别轻易换。
七、我的实际选型经验
说一下我们在不同项目里的实际选择:
企业内部知识库(约200万文档):选了Qdrant。理由:性能够用,带过滤查询很快(平均15ms),单机可以搞定,运维成本低,Java SDK质量还不错。
电商商品搜索(约8000万商品):选了Milvus。理由:数据量大,需要分区(按品类),Milvus的查询性能在这个规模下明显优于Qdrant。
个人原型项目:Chroma,Docker一键起,本地跑,不用想运维的事。
有知识图谱需求的项目:本来以为Weaviate是最佳选,但最终还是用了Qdrant+Neo4j分开部署。Weaviate的图谱功能不如专业图数据库丰富,而且Java SDK质量让我不太放心。
下一篇讲混合检索策略——稀疏向量(BM25)和稠密向量的融合,以及RRF排序算法。这是目前检索质量提升效果最明显的工程优化之一,基本上是大规模RAG系统的标配了。
