第2046篇:向量数据库选型——Milvus、Qdrant、pgvector怎么选
大约 6 分钟
第2046篇:向量数据库选型——Milvus、Qdrant、pgvector怎么选
适读人群:需要为RAG系统选择向量存储方案的工程师 | 阅读时长:约20分钟 | 核心价值:深度对比三种向量存储方案的性能、运维复杂度和适用场景
做RAG选型的时候,向量数据库是绕不开的决策。Milvus、Qdrant、pgvector三个方案各有说法,网上的对比文章大多是理论分析,缺少工程视角的判断。
我在不同项目里都用过,来说说实际使用感受。
三种方案的基本认知
先建立一个基本认知框架:
我的核心观点:不要过早引入复杂的向量数据库。大多数RAG应用,pgvector就够了。
pgvector:最低成本的方案
如果你的项目已经在用PostgreSQL,pgvector几乎是零成本的选择:
-- 安装扩展(PostgreSQL 11+支持)
CREATE EXTENSION IF NOT EXISTS vector;
-- 创建存储表
CREATE TABLE document_embeddings (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(768) NOT NULL, -- 向量维度与embedding模型对应
metadata JSONB,
source_file VARCHAR(500),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- HNSW索引(推荐,查询快,适合在线服务)
-- m: 每个节点的最大连接数,越大精度越高,但内存和构建时间增加
-- ef_construction: 构建时的候选集大小,越大精度越高
CREATE INDEX idx_embedding_hnsw
ON document_embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- IVFFlat索引(备选,内存占用小,适合内存紧张场景)
-- lists: 聚类中心数,通常设为 向量总数/1000
CREATE INDEX idx_embedding_ivfflat
ON document_embeddings
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);/**
* pgvector的Java操作(使用Spring Data + 原生SQL)
*/
@Repository
public class PgVectorRepository {
private final JdbcTemplate jdbcTemplate;
/**
* 存储向量
*/
public void save(String content, float[] embedding, Map<String, String> metadata) {
String vectorStr = Arrays.toString(embedding)
.replace('[', '[').replace(']', ']');
jdbcTemplate.update("""
INSERT INTO document_embeddings (content, embedding, metadata)
VALUES (?, ?::vector, ?::jsonb)
""",
content,
vectorStr,
toJson(metadata)
);
}
/**
* 相似度检索
* <=> 是余弦距离(越小越相似)
* <#> 是内积(越大越相似,需要归一化向量)
* <-> 是欧氏距离
*/
public List<SearchResult> search(float[] queryEmbedding, int topK, double minScore) {
String vectorStr = Arrays.toString(queryEmbedding);
return jdbcTemplate.query("""
SELECT
content,
metadata,
1 - (embedding <=> ?::vector) AS similarity_score
FROM document_embeddings
WHERE 1 - (embedding <=> ?::vector) > ?
ORDER BY embedding <=> ?::vector
LIMIT ?
""",
new Object[]{vectorStr, vectorStr, minScore, vectorStr, topK},
(rs, rowNum) -> new SearchResult(
rs.getString("content"),
parseJson(rs.getString("metadata")),
rs.getDouble("similarity_score")
)
);
}
/**
* 带元数据过滤的检索
* pgvector的一大优势:可以和普通SQL条件组合
*/
public List<SearchResult> searchWithFilter(
float[] queryEmbedding,
String sourceFile,
int topK) {
String vectorStr = Arrays.toString(queryEmbedding);
return jdbcTemplate.query("""
SELECT
content,
metadata,
1 - (embedding <=> ?::vector) AS similarity_score
FROM document_embeddings
WHERE source_file = ?
ORDER BY embedding <=> ?::vector
LIMIT ?
""",
new Object[]{vectorStr, sourceFile, vectorStr, topK},
(rs, rowNum) -> new SearchResult(
rs.getString("content"),
parseJson(rs.getString("metadata")),
rs.getDouble("similarity_score")
)
);
}
}pgvector的适用边界:
| 向量数量 | 查询延迟(p99) | 是否推荐 |
|---|---|---|
| < 10万 | < 5ms | 强烈推荐 |
| 10万-100万 | 10-30ms | 推荐 |
| 100万-500万 | 50-200ms | 可用,需要调优 |
| > 500万 | > 200ms | 建议迁移 |
Qdrant:性能和功能的平衡点
Qdrant是Rust写的,性能好,且有很多pgvector没有的功能(Payload过滤器、Named Vector等):
/**
* Qdrant Java集成
* 使用官方Java SDK
*/
@Configuration
public class QdrantConfig {
@Bean
public QdrantClient qdrantClient() {
return new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build()
);
}
}
@Repository
@RequiredArgsConstructor
@Slf4j
public class QdrantVectorRepository {
private final QdrantClient qdrantClient;
private static final String COLLECTION = "documents";
/**
* 初始化集合(collection)
*/
public void createCollection(int vectorDimension) throws ExecutionException, InterruptedException {
qdrantClient.createCollectionAsync(
COLLECTION,
VectorsConfig.newBuilder()
.setParams(VectorParams.newBuilder()
.setSize(vectorDimension)
.setDistance(Distance.Cosine)
.build())
.build()
).get();
log.info("Qdrant集合创建完成: {}, 维度: {}", COLLECTION, vectorDimension);
}
/**
* 批量写入(Qdrant推荐批量操作)
*/
public void batchUpsert(List<DocumentVector> documents) throws ExecutionException, InterruptedException {
List<PointStruct> points = documents.stream()
.map(doc -> PointStruct.newBuilder()
.setId(PointId.newBuilder()
.setUuid(doc.getId())
.build())
.setVectors(Vectors.newBuilder()
.setVector(Vector.newBuilder()
.addAllData(toFloatList(doc.getEmbedding()))
.build())
.build())
.putAllPayload(Map.of(
"content", Value.newBuilder().setStringValue(doc.getContent()).build(),
"source", Value.newBuilder().setStringValue(doc.getSource()).build(),
"category", Value.newBuilder().setStringValue(doc.getCategory()).build()
))
.build())
.collect(Collectors.toList());
qdrantClient.upsertAsync(COLLECTION, points).get();
log.info("批量写入完成: {}个向量", documents.size());
}
/**
* Qdrant的强项:向量检索 + Payload过滤器
* 比pgvector的SQL过滤效率更高(HNSW层面的过滤)
*/
public List<ScoredSearchResult> searchWithPayloadFilter(
float[] queryEmbedding,
String category,
int topK) throws ExecutionException, InterruptedException {
// Payload过滤器:在向量检索阶段就过滤,不是事后过滤
Filter categoryFilter = Filter.newBuilder()
.addMust(Condition.newBuilder()
.setField(FieldCondition.newBuilder()
.setKey("category")
.setMatch(Match.newBuilder()
.setKeyword(category)
.build())
.build())
.build())
.build();
List<ScoredPoint> results = qdrantClient.searchAsync(
SearchPoints.newBuilder()
.setCollectionName(COLLECTION)
.addAllVector(toFloatList(queryEmbedding))
.setFilter(categoryFilter)
.setLimit(topK)
.setWithPayload(WithPayloadSelector.newBuilder()
.setEnable(true)
.build())
.build()
).get();
return results.stream()
.map(point -> new ScoredSearchResult(
point.getPayloadOrDefault("content", Value.getDefaultInstance())
.getStringValue(),
point.getScore(),
point.getPayloadOrDefault("source", Value.getDefaultInstance())
.getStringValue()
))
.collect(Collectors.toList());
}
private List<Float> toFloatList(float[] array) {
List<Float> list = new ArrayList<>(array.length);
for (float f : array) list.add(f);
return list;
}
}Qdrant的几个独特功能:
- Named Vectors:同一个文档可以存多个不同维度/模型的向量,不需要建多个集合
- Payload过滤器:过滤在HNSW图层面生效,比事后过滤快很多
- Sparse Vector支持:可以和BM25等稀疏检索结合做混合检索
Milvus:大规模场景的选择
Milvus适合超大数据量和高并发场景,但运维复杂度明显更高:
/**
* Milvus Java集成
* 使用官方Java SDK
*/
@Configuration
public class MilvusConfig {
@Bean
public MilvusClient milvusClient() {
ConnectParam connectParam = ConnectParam.newBuilder()
.withHost("milvus-server")
.withPort(19530)
.build();
return new MilvusServiceClient(connectParam);
}
}
@Repository
@RequiredArgsConstructor
@Slf4j
public class MilvusVectorRepository {
private final MilvusClient milvusClient;
private static final String COLLECTION = "documents";
private static final int VECTOR_DIM = 768;
/**
* 创建Collection(Milvus的表结构定义)
*/
public void createCollection() {
// 定义字段
FieldType idField = FieldType.newBuilder()
.withName("id")
.withDataType(DataType.VarChar)
.withMaxLength(64)
.withPrimaryKey(true)
.withAutoID(false)
.build();
FieldType contentField = FieldType.newBuilder()
.withName("content")
.withDataType(DataType.VarChar)
.withMaxLength(4096)
.build();
FieldType embeddingField = FieldType.newBuilder()
.withName("embedding")
.withDataType(DataType.FloatVector)
.withDimension(VECTOR_DIM)
.build();
CreateCollectionParam createParam = CreateCollectionParam.newBuilder()
.withCollectionName(COLLECTION)
.withShardsNum(2) // 分片数,影响并发写入能力
.addFieldType(idField)
.addFieldType(contentField)
.addFieldType(embeddingField)
.build();
milvusClient.createCollection(createParam);
// 创建索引
CreateIndexParam indexParam = CreateIndexParam.newBuilder()
.withCollectionName(COLLECTION)
.withFieldName("embedding")
.withIndexName("hnsw_index")
.withIndexType(IndexType.HNSW)
.withMetricType(MetricType.COSINE)
.withExtraParam("{\"M\": 16, \"efConstruction\": 64}")
.build();
milvusClient.createIndex(indexParam);
// 加载到内存(Milvus需要显式加载)
LoadCollectionParam loadParam = LoadCollectionParam.newBuilder()
.withCollectionName(COLLECTION)
.build();
milvusClient.loadCollection(loadParam);
log.info("Milvus Collection创建完成: {}", COLLECTION);
}
/**
* 向量检索
*/
public List<SearchResult> search(float[] queryEmbedding, int topK) {
List<List<Float>> vectors = List.of(toFloatList(queryEmbedding));
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(COLLECTION)
.withVectors(vectors)
.withVectorFieldName("embedding")
.withMetricType(MetricType.COSINE)
.withTopK(topK)
.withOutFields(List.of("id", "content"))
.withParams("{\"ef\": 64}") // 查询时的候选集大小,越大越准但越慢
.build();
R<SearchResults> response = milvusClient.search(searchParam);
// 解析结果...
return parseSearchResults(response.getData());
}
}Milvus的运维复杂度:Milvus集群模式需要etcd、MinIO、Pulsar等依赖服务,部署和维护成本比pgvector高得多。除非你的数据量真的到了亿级别,否则这个复杂度很难值回来。
选型决策树
| 方案 | 适用规模 | 运维成本 | 特色功能 |
|---|---|---|---|
| pgvector | < 500万向量 | 极低(PostgreSQL运维) | SQL灵活查询 |
| Qdrant | < 1亿向量 | 低(单二进制部署) | 高性能,Payload过滤 |
| Milvus | > 1亿向量 | 高(多组件依赖) | 水平扩展,企业级 |
我在项目里的真实选择:80%的RAG项目用pgvector,因为大多数项目的文档量根本到不了pgvector的性能瓶颈,而且团队不需要额外学习新的运维技能。只有当数据量或者查询延迟真的成问题了,才考虑迁移到Qdrant。
