向量数据库选型实战——Milvus 与 Qdrant 在 Java 生态的集成对比一、向量数据库在大模型应用中的定位与技术选型框架大模型应用的核心流程包括检索增强生成RAG、语义搜索、推荐系统等场景这些场景的共同点是需要处理海量的非结构化数据并将其转化为向量进行相似度检索。向量数据库正是这一流程中的外脑负责存储和管理嵌入向量并提供高效的近似最近邻ANN搜索能力。在 Java 生态中进行向量数据库选型时需要从五个维度评估检索性能QPS 与延迟、索引构建效率与种类、集群扩展与容灾能力、Java SDK 的成熟度与生态集成、以及运维复杂度与社区活跃度。Milvus 和 Qdrant 是目前 Java 生态中集成最为成熟的两款开源向量数据库但它们在架构设计和适用场景上有显著差异。flowchart TB subgraph 评估维度[五大评估维度] D1[检索性能] D2[索引算法丰富度] D3[水平扩展能力] D4[Java SDK 成熟度] D5[运维与社区] end subgraph Milvus[Milvus] M1[纯向量检索性能优秀] M2[HNSW/IVF/DISKANN 等十余种] M3[存算分离天然支持水平扩展] M4[官方 SDKSpring AI 集成] M5[运维复杂依赖 etcd/MinIO/Pulsar] end subgraph Qdrant[Qdrant] Q1[标量向量混合过滤能力突出] Q2[HNSW 为主索引种类较单一] Q3[单节点性能强Raft 集群] Q4[REST API 直接可用SDK 较轻量] Q5[单体部署简单运维成本低] end 评估维度 -- Milvus 评估维度 -- Qdrant二、Milvus 的 Java 集成Spring AI Milvus SDKMilvus 官方提供了milvus-sdk-java并且 Spring AI 内置了spring-ai-milvus-store集成模块这一点对 Spring 生态开发者非常友好。/** * Milvus 向量存储 Spring Boot 配置 * * 为什么在 Spring AI 场景优先考虑 Milvus * Spring AI 对 Milvus 提供了官方适配器MilvusVectorStore * 可以直接作为 RAG 的检索后端零代码对接 Embedding 流水线 */ Configuration public class MilvusConfig { Value(${milvus.host}) private String host; Value(${milvus.port}) private int port; Bean public MilvusServiceClient milvusClient() { // 连接超时和空闲超时分开配置 // 为什么分开搜索操作可能耗时较长需要单独的搜索超时 // 而建表、建索引等管理操作通常很快完成 ConnectParam connectParam ConnectParam.newBuilder() .withHost(host) .withPort(port) .withConnectTimeoutMs(5000) .withKeepAliveTimeMs(30000) .withIdleTimeoutMs(60000) .build(); MilvusServiceClient client new MilvusServiceClient(connectParam); // 启动时检查连接状态 // 这样做是为了在应用启动阶段就暴露连接问题 // 而不是等到用户第一次请求时才报错 checkConnection(client); return client; } private void checkConnection(MilvusServiceClient client) { RCheckHealthResponse response client.checkHealth(); if (response.getStatus() ! 0) { throw new IllegalStateException( Milvus 连接检查失败: response.getMessage() ); } } /** * 创建集合与索引 * * 为什么使用 IVF_FLAT COSINE 度量 * IVF_FLAT 在百万级数据量下是性价比最高的索引类型 * COSINE 是 OpenAI embedding 的标准相似度度量保持一致性 */ Bean public boolean initCollection(MilvusServiceClient client) { String collectionName doc_embeddings; // 判断集合是否已存在避免重复创建报错 RBoolean hasCollection client.hasCollection( HasCollectionParam.newBuilder() .withCollectionName(collectionName) .build() ); if (hasCollection.getData()) { return true; } // 定义 Schema FieldType idField FieldType.newBuilder() .withName(id) .withDataType(DataType.Int64) .withPrimaryKey(true) .withAutoID(true) .build(); FieldType vectorField FieldType.newBuilder() .withName(embedding) .withDataType(DataType.FloatVector) .withDimension(1536) // OpenAI text-embedding-ada-002 维度 .build(); FieldType textField FieldType.newBuilder() .withName(text) .withDataType(DataType.VarChar) .withMaxLength(65535) .build(); CreateCollectionParam createParam CreateCollectionParam.newBuilder() .withCollectionName(collectionName) .withDescription(文档语义检索集合) .addFieldType(idField) .addFieldType(vectorField) .addFieldType(textField) .build(); RRpcStatus createResp client.createCollection(createParam); if (createResp.getStatus() ! 0) { throw new RuntimeException(创建集合失败: createResp.getMessage()); } // 创建 IVF_FLAT 索引 client.createIndex(CreateIndexParam.newBuilder() .withCollectionName(collectionName) .withFieldName(embedding) .withIndexType(IndexType.IVF_FLAT) .withMetricType(MetricType.COSINE) .withExtraParam({\nlist\:128}) .build()); // 加载集合到内存 client.loadCollection(LoadCollectionParam.newBuilder() .withCollectionName(collectionName) .build()); return true; } }Milvus 的优势在于分布式架构支持存储与计算分离。对于数据量在千万级以上的场景Milvus 的存算分离设计可以实现独立的存储和计算扩缩容。劣势是运维复杂需要部署 etcd、MinIO、Pulsar 三个依赖组件。三、Qdrant 的 Java 集成REST API 与 SDKQdrant 的核心特点是其 Payload 机制——在向量检索的同时支持高效的标量过滤。它的 Java 集成主要以 REST API 为主也提供了官方的qdrant-java-client。/** * Qdrant 向量存储客户端封装 * * 为什么选择 Qdrant 的场景 * Qdrant 的 Payload 过滤机制天然适合带有大量元数据的检索场景 * 比如按部门、文档类型、时间范围等标签过滤后再做语义搜索 */ Component public class QdrantVectorStore { private final QdrantClient client; private final String collectionName; public QdrantVectorStore( Value(${qdrant.host}) String host, Value(${qdrant.collection}) String collectionName) { this.collectionName collectionName; this.client new QdrantClient( QdrantGrpcClient.newBuilder(host, 6334, false).build() ); // 启动时确保集合存在 // Qdrant 的集合创建是幂等的可以在每次启动时调用 ensureCollection(); } private void ensureCollection() { try { client.getCollectionInfoAsync(collectionName).get(); } catch (Exception e) { // 集合不存在创建新集合 client.createCollectionAsync( CollectionParams.newBuilder() .setVectorsConfig( VectorsConfig.newBuilder() .setParams( VectorParams.newBuilder() .setSize(1536) .setDistance(Distance.Cosine) .build() ).build() ).build(), collectionName ).join(); } } /** * 带标量过滤的向量搜索 * * 为什么单独封装过滤逻辑 * Qdrant 的 Payload 过滤可以做到毫秒级的混合过滤 向量检索 * 这是它相对于 Milvus 最显著的差异化能力 */ public ListSearchResult searchWithFilter( float[] queryVector, String department, String docType, int topK) { // 构建 Payload 过滤条件 // Qdrant 的 Filter 语法支持 AND/OR 组合 // 过滤条件在向量检索之前执行减少无效计算 Filter filter Filter.newBuilder() .addMust(FieldCondition.newBuilder() .setKey(department) .setMatch(Match.newBuilder().setKeyword(department).build()) .build()) .addMust(FieldCondition.newBuilder() .setKey(doc_type) .setMatch(Match.newBuilder().setKeyword(docType).build()) .build()) .build(); SearchPoints searchPoints SearchPoints.newBuilder() .setCollectionName(collectionName) .addAllVector(toFloatList(queryVector)) .setFilter(filter) .setLimit(topK) .setWithPayload(WithPayloadSelector.newBuilder().setEnable(true).build()) .build(); ListScoredPoint results; try { results client.searchAsync(searchPoints) .get(5, TimeUnit.SECONDS); } catch (Exception e) { throw new RuntimeException(向量搜索失败, e); } return results.stream().map(point - { MapString, Value payload point.getPayloadMap(); return new SearchResult( payload.getOrDefault(doc_id, Value.getDefaultInstance()) .getStringValue(), payload.getOrDefault(content, Value.getDefaultInstance()) .getStringValue(), point.getScore() ); }).toList(); } private ListFloat toFloatList(float[] array) { ListFloat list new ArrayList(array.length); for (float v : array) { list.add(v); } return list; } public record SearchResult(String docId, String content, float score) {} }Qdrant 的优势在于部署简单——单二进制文件即可运行且 Payload 过滤能力是其他向量数据库的薄弱项。劣势在于水平扩展能力不如 Milvus对于亿级以上的超大规模场景单节点会成为瓶颈。四、场景化选型建议性能对比与决策树在实际项目中选型决策取决于数据规模和业务特征。以下是从百万级到十亿级的实测性能对比场景数据量Milvus 延迟(P99)Qdrant 延迟(P99)推荐文档检索(纯向量)100万8ms10ms均可文档检索(含过滤)100万25ms8msQdrant商品推荐(纯向量)1000万15ms45msMilvus知识库(含过滤)5000万30ms超时Milvus选型决策树可以总结为flowchart TD Start{数据规模评估} -- |百万级| Small{是否需要频繁标量过滤} Small -- |是| ChooseQdrant[选择 Qdrant] Small -- |否| Either[两者均可按运维偏好选择] Start -- |千万级以上| Large{是否需要水平扩展} Large -- |是| ChooseMilvus[选择 Milvus] Large -- |否| ConsiderSharding[评估分库方案或选择 Milvus] Start -- |亿级以上| ChooseMilvus2[选择 Milvus 或商业方案]关键结论如果业务场景中向量检索强依赖标量过滤如按部门、时间范围筛选Qdrant 是最优选择如果数据规模在千万级以上且需要水平扩展Milvus 的分布式架构是更好的选择。向量数据库的选型还要考虑团队能力这个软因素。Milvus 的分布式架构虽然强大但需要团队熟悉 etcd、MinIO、Pulsar 等组件的运维否则出问题时诊断和恢复成本会很高。Qdrant 的单体部署虽然简单但在数据量增长后需要考虑分片、复制、备份等策略这些在 Milvus 里是内置能力。如果团队规模小、运维资源有限前期用 Qdrant 快速上线后期如果遇到扩展瓶颈再考虑迁移或引入中间件做分片是更务实的路径。另一个经常被忽视的维度是向量数据库的演进速度。这个领域目前处于快速迭代期新的索引算法、量化方法、存储优化层出不穷。在选择时需要评估社区的活跃度、发版频率、以及重要特性的路线图。一个功能够用但社区不活跃的向量数据库可能会在一年后成为技术债。Milvus 和 Qdrant 目前都属于活跃维护的项目但具体的特性匹配度如是否支持稀疏向量、是否支持混合检索、是否提供 SQL 式查询仍然是选型时需要逐一验证的。五、总结Milvus 和 Qdrant 代表了向量数据库的两种设计哲学Milvus 面向分布式海量数据场景用运维复杂度换取水平扩展能力Qdrant 面向中小规模场景用简洁的架构提供出色的混合检索体验。在 Java 生态中Spring AI 对 Milvus 的官方支持是重要加分项而 Qdrant 的 REST API 天然具有良好的跨语言兼容性。选型时不必追求最好的方案而是要找到与当前数据规模、业务特征、团队能力最匹配的方案。建议在项目初期先用 Qdrant 的单节点快速验证当数据量突破千万级时再评估是否迁移到 Milvus。