📝 本文首发于 栏轩·阁
欢迎访问阅读原文,获取更好的阅读体验。
什么是 Qdrant?为什么需要它?
Qdrant 是一个高性能向量数据库,专为 AI 时代的语义搜索和相似度匹配而生。
传统的数据库(MySQL、PostgreSQL)擅长精确匹配 —— 你问"价格等于 100 的商品",它立刻答出来。但当你问"和这段文字意思相似的内容"时,它就无能为力了。原因是传统数据库基于关键词匹配,无法理解语义。
Qdrant 这类向量数据库的解决思路是:把文本、图片、音视频等数据通过 AI 模型转换成向量(Vector)—— 一组浮点数。语义相似的内容,在向量空间中距离就相近。于是搜索"机器学习入门"就能找到"深度学习基础教程",即使关键词不完全匹配。
核心应用场景
- RAG(检索增强生成):为大语言模型提供外部知识库,解决幻觉问题
- 语义搜索:理解用户意图,返回语义相关的结果
- 图像/音视频相似度检索:以图搜图、音乐识别
- 推荐系统:基于用户行为向量做个性化推荐
- 异常检测:找出与正常模式偏离的数据点
Qdrant vs pgvector vs Redis 向量搜索
| 定位 | 专业向量数据库 | PostgreSQL 插件 | 缓存数据库的向量扩展 |
| 部署 | 独立服务(Docker/云) | 依附于 PostgreSQL | 依附于 Redis |
| API 类型 | gRPC + REST | SQL | Redis 协议 |
| 过滤能力 | 强(支持嵌套过滤、geo、范围) | 中(SQL WHERE) | 中 |
| 性能 | 高(专门优化的 HNSW 索引) | 中 | 高(纯内存) |
| 持久化 | 磁盘 + 内存混合 | 磁盘 | 取决于配置 |
| 集群/分布式 | 原生支持 | 基于 PostgreSQL | 基于 Redis Cluster |
| 适用场景 | 专业 AI 应用 | 已有 PG 生态的小团队 | 轻量级向量检索 |
简单说:pgvector 适合"顺便用用向量"、Redis 适合"缓存场景的向量"、Qdrant 才是真正为了向量检索而生的专业选手。
快速开始:Docker 部署
在项目目录下创建 docker-compose.yml:
version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant–server
restart: always
ports:
– "6333:6333" # HTTP REST API(Web 控制台也走这个端口)
– "6334:6334" # gRPC API(Java 等客户端用这个)
volumes:
– ./qdrant_storage:/qdrant/storage
environment:
– QDRANT__SERVICE__GRPC_PORT=6334
– QDRANT__SERVICE__HTTP_PORT=6333
– QDRANT__LOG_LEVEL=INFO
启动:
docker compose up -d
访问控制台:http://localhost:6333/dashboard
端口说明
| 6333 | HTTP/REST | Web 控制台、curl 调试、Python/REST 客户端 |
| 6334 | gRPC | Java、Go、.NET 等高性能客户端 |
存储卷说明
./qdrant_storage:/qdrant/storage 将数据持久化到宿主机。删除容器后数据不会丢失,删除宿主机目录才真正清理。
核心概念:Collection 与 Point
在开始操作之前,先弄清楚 Qdrant 的两个核心概念:
- Collection(集合):类似关系数据库的"表",是存储向量数据的容器。每个 Collection 有自己的配置(向量维度、距离算法等)。
- Point(数据点):类似关系数据库的"行",包含三部分:
- id:唯一标识符(整数或 UUID)
- vector:向量(float[],维度必须与 Collection 配置一致)
- payload:元数据(JSON 对象),用于过滤和展示
为了教学清晰,本文统一使用电影数据集作为贯穿全文的示例。创建一个 movies 集合,存储电影信息,通过向量搜索实现"找相似电影"的功能。
基础操作:Web 控制台实战
Qdrant 的 Web 控制台提供了一个 Console 面板,可以直接执行 HTTP 风格的命令,非常适合学习和调试。
1. 创建 Collection
在 Console 中执行:
PUT /collections/movies
{
"vectors": {
"size": 4,
"distance": "Cosine"
}
}
参数说明
| size | 向量维度,必须与后续插入数据的维度一致 | 正整数(常用 384、768、1024、1536) |
| distance | 距离算法,决定如何计算向量间的相似度 | Cosine(余弦)、Dot(点积)、Euclid(欧氏距离) |
距离算法怎么选?
- Cosine:最常用,关注向量的方向而非长度,适用于文本/语义搜索(如 OpenAI embeddings)
- Dot:考虑方向和长度,适合归一化后的向量
- Euclid:欧氏距离,值越小越相似,适合某些图像检索场景
这里我们设置维度为 4(教学演示用,实际生产通常是 384~1536),距离算法用最通用的 Cosine。
了解一下高级配置(本文不展开):
- multivector_config — 一个 Point 可以存多个向量(如文本的多个段落各自编码),搜索时对每个向量分别打分再聚合,适合多模态或长文档分段召回场景。
- sparse_vectors — 稀疏向量(大部分元素为 0),配合 BM25 等词汇权重算法,与稠密向量做混合搜索(Hybrid Search),能显著提升带精确关键词匹配的召回率。
- hnsw_config — HNSW 索引的参数调优(如 m、ef_construct),影响搜索速度与精度的 trade-off,生产环境调优时绕不开。
2. 插入数据
PUT /collections/movies/points
{
"points": [
{
"id": 1,
"vector": [0.85, 0.23, 0.61, 0.74],
"payload": {
"title": "The Matrix",
"genre": "Sci-Fi",
"rating": 8.7,
"year": 1999,
"director": "Lana Wachowski",
"actors": ["Keanu Reeves", "Laurence Fishburne"]
}
},
{
"id": 2,
"vector": [0.81, 0.19, 0.75, 0.11],
"payload": {
"title": "Inception",
"genre": "Sci-Fi",
"rating": 8.8,
"year": 2010,
"director": "Christopher Nolan",
"actors": ["Leonardo DiCaprio", "Joseph Gordon-Levitt"]
}
},
{
"id": 3,
"vector": [0.36, 0.55, 0.47, 0.94],
"payload": {
"title": "The Godfather",
"genre": "Crime",
"rating": 9.2,
"year": 1972,
"director": "Francis Ford Coppola",
"actors": ["Marlon Brando", "Al Pacino"]
}
},
{
"id": 4,
"vector": [0.65, 0.88, 0.32, 0.21],
"payload": {
"title": "Interstellar",
"genre": "Sci-Fi",
"rating": 8.7,
"year": 2014,
"director": "Christopher Nolan",
"actors": ["Matthew McConaughey", "Anne Hathaway"]
}
},
{
"id": 5,
"vector": [0.24, 0.18, 0.72, 0.44],
"payload": {
"title": "The Dark Knight",
"genre": "Action",
"rating": 9.0,
"year": 2008,
"director": "Christopher Nolan",
"actors": ["Christian Bale", "Heath Ledger"]
}
}
]
}
返回结果说明
{
"operation_id": 1,
"status": "acknowledged"
}
| operation_id | 操作编号,可用于追踪 |
| status | acknowledged 表示已接收(异步写入),completed 表示已完成 |
3. Payload 数据类型详解
Payload 是 Qdrant 最强大的特性之一,本质上是一个 JSON 对象,可以表达任意复杂的结构化数据。
支持的数据类型
| Integer | 64 位整数 | "year": 1999、"scores": [85, 92, 78] |
| Float | 64 位浮点数 | "rating": 8.7、"prices": [9.99, 19.99] |
| Bool | 布尔值 | "is_featured": true |
| Keyword | 字符串,用于精确匹配和标签过滤 | "genre": "Sci-Fi"、"tags": ["action", "thriller"] |
| Geo | 地理坐标(经度 + 纬度) | "location": {"lon": 116.40, "lat": 39.91} |
| Datetime | 时间日期(RFC 3339,v1.8.0+) | "release_date": "1999-03-31T00:00:00Z" |
Payload 的两个核心优势
4. 搜索:找相似电影
POST /collections/movies/points/search
{
"vector": [0.75, 0.20, 0.68, 0.15],
"limit": 3,
"with_payload": true
}
参数说明
| vector | 查询向量(必须与 collection 的 size 一致) | [0.75, 0.20, 0.68, 0.15] |
| limit | 返回 top-K 条结果 | 3 |
| with_payload | 是否返回 payload 元数据 | true / false |
| with_vector | 是否返回向量值(通常不需要) | false(默认) |
返回示例
{
"result": [
{"id": 1, "score": 0.96, "payload": {"title": "The Matrix", "genre": "Sci-Fi"}},
{"id": 2, "score": 0.91, "payload": {"title": "Inception", "genre": "Sci-Fi"}},
{"id": 5, "score": 0.82, "payload": {"title": "The Dark Knight", "genre": "Action"}}
],
"status": "ok",
"time": 0.0002
}
score 是相似度分数,值域取决于距离算法。Cosine 范围 [-1, 1],越接近 1 越相似。
5. 从快照导入数据
Qdrant 支持从远程快照文件批量导入数据,适合快速获取测试数据集:
PUT /collections/movies/snapshots/recover
{
"location": "http://snapshots.qdrant.io/midlib-v1.16.0.snapshot"
}
这是 Qdrant 官方提供的 Midjourney 测试数据,512 维向量。导入后可通过以下命令验证数据量:
POST /collections/movies/points/count
核心功能:过滤条件
过滤是向量数据库的"杀手锏"—— 在语义相似度搜索的基础上,附加结构化条件筛选。
创建索引
在进行过滤之前,必须先为要过滤的字段创建索引,否则 Qdrant 会全表扫描,性能极差。
Keyword 类型索引(用于精确匹配):
PUT /collections/movies/index
{
"field_name": "genre",
"field_schema": "keyword"
}
Integer 类型索引(用于范围过滤):
PUT /collections/movies/index
{
"field_name": "year",
"field_schema": {
"type": "integer",
"range": true
}
}
索引类型对照
| keyword | "keyword" | match 精确匹配 |
| integer | {"type": "integer", "range": true} | match + range 范围 |
| float | {"type": "float", "range": true} | range 范围过滤 |
| geo | {"type": "geo"} | geo 地理位置查询 |
| text | "text" | matchText 全文搜索 |
过滤通用语法
Qdrant 的过滤条件遵循统一的 DSL 结构:
POST /collections/movies/points/scroll
{
"filter": {
"must": [
{ "key": "genre", "match": { "value": "Sci-Fi" } }
],
"must_not": [
{ "key": "year", "range": { "lt": 2000 } }
],
"should": [
{ "key": "rating", "range": { "gte": 8.5 } }
]
},
"limit": 10,
"with_payload": true
}
逻辑操作符
| must | 必须满足所有条件 | AND |
| should | 至少满足一个条件即可 | OR |
| must_not | 必须不满足条件 | NOT |
条件匹配器
| match | 精确匹配(等于) | keyword、integer、bool |
| match_text | 文本模糊匹配 | text(需建 text 索引) |
| range | 范围过滤 | integer、float |
| geo_radius | 地理半径查询 | geo |
| has_id | 按 ID 匹配 | ID |
| is_null | 字段不存在 | 任意 |
| nested | 嵌套对象过滤 | 嵌套结构 |
match:精确匹配
POST /collections/movies/points/scroll
{
"filter": {
"must": [
{
"key": "genre",
"match": { "value": "Sci-Fi" }
}
]
},
"limit": 10,
"with_payload": true
}
只会返回 genre 精确等于 “Sci-Fi” 的电影。
range:范围过滤
POST /collections/movies/points/scroll
{
"filter": {
"must": [
{
"key": "year",
"range": { "gte": 2000, "lte": 2020 }
},
{
"key": "rating",
"range": { "gte": 8.5 }
}
]
},
"limit": 10,
"with_payload": true
}
range 参数
| gte | >= |
| gt | > |
| lte | <= |
| lt | < |
组合过滤:向量搜索 + 条件
将过滤条件与向量搜索结合,是向量数据库最核心的使用方式:
POST /collections/movies/points/search
{
"vector": [0.75, 0.20, 0.68, 0.15],
"filter": {
"must": [
{ "key": "genre", "match": { "value": "Sci-Fi" } },
{ "key": "year", "range": { "gte": 2000 } }
]
},
"limit": 3,
"with_payload": true
}
这条命令的语义:找到与目标向量最相似的科幻片(genre=Sci-Fi),且只看 2000 年之后的。
nested:嵌套过滤
当 Payload 中包含嵌套对象或对象数组时,使用 nested 过滤。
先插入带嵌套数据的电影评分记录:
PUT /collections/movies/points
{
"points": [
{
"id": 10,
"vector": [0.45, 0.67, 0.23, 0.89],
"payload": {
"title": "Pulp Fiction",
"reviews": [
{"user": "Alice", "rating": 9, "liked": true},
{"user": "Bob", "rating": 7, "liked": true}
]
}
},
{
"id": 11,
"vector": [0.55, 0.32, 0.78, 0.41],
"payload": {
"title": "Fight Club",
"reviews": [
{"user": "Alice", "rating": 10, "liked": true},
{"user": "Bob", "rating": 5, "liked": false}
]
}
}
]
}
查询:找到 Alice 喜欢(liked=true) 的电影:
POST /collections/movies/points/scroll
{
"filter": {
"must": [
{
"nested": {
"key": "reviews",
"filter": {
"must": [
{ "key": "user", "match": { "value": "Alice" } },
{ "key": "liked", "match": { "value": true } }
]
}
}
}
]
},
"limit": 10,
"with_payload": true
}
嵌套过滤的要点
- nested.key:指定嵌套数组的字段名
- nested.filter:作用于数组内部元素的过滤条件
- 只有数组中存在至少一个元素满足所有条件时,该文档才会被匹配
Collections 页面
Web 控制台的 Collections 页面提供了一个可视化界面,可以:
- 浏览所有 Collection
- 查看 Collection 详情(状态、向量维度、点数、配置参数)
- 编辑配置(优化器参数、HNSW 索引参数)
- 删除 Collection
生产环境中,可视化页面比 Console 更适合日常运维。
Java 客户端实战
有了一定的 web 操作基础后,我们看看如何用 Java 代码实现同样的功能。Qdrant 官方 Java 客户端基于 gRPC 协议(端口 6334)通信,所有 API 都返回 ListenableFuture<T>,调用 .get() 阻塞执行。
Maven 依赖
<dependency>
<groupId>io.qdrant</groupId>
<artifactId>client</artifactId>
<version>1.18.3</version>
</dependency>
<!– 需要额外引入 gRPC Netty 传输层 –>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.68.2</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.68.2</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.68.2</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
注意:Qdrant client v1.16+ 默认引入了 gRPC 依赖但声明为 runtime scope,需要手动添加 compile 依赖。
客户端初始化
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build()
);
如果需要连接 Qdrant Cloud(带 API Key 认证):
QdrantClient cloudClient = new QdrantClient(
QdrantGrpcClient.newBuilder("xyz-example.qdrant.io", 6334, true)
.withApiKey("your-api-key-here")
.build()
);
创建 Collection
使用 VectorParams 构建器,简洁明快:
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
client.createCollectionAsync("movies",
VectorParams.newBuilder()
.setDistance(Distance.Cosine) // 与 web 示例一致
.setSize(4) // 向量维度 4
.build()
).get();
插入数据
使用静态导入的辅助方法,代码比直接构建 proto 对象简洁很多:
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;
import io.qdrant.client.grpc.Points.PointStruct;
List<PointStruct> movies = List.of(
PointStruct.newBuilder()
.setId(id(1))
.setVectors(vectors(0.85f, 0.23f, 0.61f, 0.74f))
.putPayload("title", value("The Matrix"))
.putPayload("genre", value("Sci-Fi"))
.putPayload("rating", value(8.7))
.putPayload("year", value(1999))
.build(),
PointStruct.newBuilder()
.setId(id(2))
.setVectors(vectors(0.81f, 0.19f, 0.75f, 0.11f))
.putPayload("title", value("Inception"))
.putPayload("genre", value("Sci-Fi"))
.putPayload("rating", value(8.8))
.putPayload("year", value(2010))
.build()
);
client.upsertAsync("movies", movies).get();
关于 ID
插入时必须手动指定 ID,Qdrant 没有自增机制。
ID 在同一个 Collection 内必须唯一,重复 ID 写入同一 Collection 会直接覆盖旧数据,不会有任何错误提醒。
| 数据已有业务主键 | 直接用业务 ID | id(movie.getId()) |
| 纯向量入库,无外部 ID | 用 UUID | id(UUID.randomUUID()) |
// UUID 方式示例
import java.util.UUID;
PointStruct movie = PointStruct.newBuilder()
.setId(id(UUID.randomUUID()))
.setVectors(vectors(0.85f, 0.23f, 0.61f, 0.74f))
.putPayload("title", value("The Matrix"))
.build();
value() 方法会自动重载匹配类型:
| String | value("hello") | Keyword |
| long | value(42) | Integer |
| double | value(3.14) | Float |
| boolean | value(true) | Bool |
| List<Value> | value(List.of(…)) | 数组 |
| Map<String, Value> | value(Map.of(…)) | 嵌套对象 |
| Geo 坐标 | value(Map.of("lon", value(116.4), "lat", value(39.9))) | 嵌套 Map |
| 日期时间 | value("1999-03-31T00:00:00Z") | Keyword(可用 range 过滤) |
向量搜索
import io.qdrant.client.grpc.Points.SearchPoints;
import io.qdrant.client.grpc.Points.ScoredPoint;
import io.qdrant.client.grpc.Points.WithPayloadSelector;
List<ScoredPoint> results = client.searchAsync(
SearchPoints.newBuilder()
.setCollectionName("movies")
.addAllVector(List.of(0.75f, 0.20f, 0.68f, 0.15f))
.setLimit(3)
.setWithPayload(WithPayloadSelector.newBuilder()
.setEnable(true).build())
.build()
).get();
for (var sp : results) {
System.out.printf("id=%d, score=%.4f, title=%s%n",
sp.getId().getNum(),
sp.getScore(),
sp.getPayloadMap().get("title"));
}
带过滤条件的搜索
import static io.qdrant.client.ConditionFactory.matchKeyword;
import static io.qdrant.client.ConditionFactory.range;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Common.Range;
List<ScoredPoint> results = client.searchAsync(
SearchPoints.newBuilder()
.setCollectionName("movies")
.addAllVector(List.of(0.75f, 0.20f, 0.68f, 0.15f))
.setFilter(Filter.newBuilder()
.addMust(matchKeyword("genre", "Sci-Fi"))
.addMust(range("year",
Range.newBuilder().setGte(2000).build()))
.build())
.setLimit(3)
.setWithPayload(WithPayloadSelector.newBuilder()
.setEnable(true).build())
.build()
).get();
通用 API 模式速查
| 查询 Collection 信息 | getCollectionInfoAsync(name) | 获取状态、点数、配置 |
| 列举所有 Collection | listCollectionsAsync() | 返回名称列表 |
| 检查是否存在 | collectionExistsAsync(name) | 返回 boolean |
| 统计点数 | countAsync(name) | 返回 Long,简洁直接 |
| 按 ID 查询 | retrieveAsync(name, ids, …) | 支持批量查询 |
| 按 ID 删除 | deleteAsync(name, ids) | 支持批量 |
| 按条件删除 | deleteAsync(name, filter) | 通过 Filter 匹配 |
| 更新 Payload | setPayloadAsync(…) | 添加/更新字段 |
| 覆盖 Payload | overwritePayloadAsync(…) | 完全替换 |
| 删除 Payload 字段 | deletePayloadAsync(…) | 指定字段名 |
| 创建别名 | createAliasAsync(alias, collection) | 读写分离 |
总结
本文从零开始走通了 Qdrant 的完整链路:Docker 部署 → Web 控制台可视化操作 → Java 客户端编码。
记住几条要点:
Qdrant 的生态还在快速演进,v1.12+ 加入了 query API(本文用的 queryAsync),v1.13+ 加入了离散索引支持。建议关注官方 Release Notes 跟进新特性。
网硕互联帮助中心




评论前必须登录!
注册