Qdrant 入门系列07 – REST 与 gRPC API 实战

Qdrant 通过两种协议提供相同的操作。REST 默认监听 6333 端口,浏览器、curl 命令和监控面板通常使用它;gRPC 默认监听 6334 端口,适用于延迟要求较高的生产服务。二者使用同一套存储、查询语义和认证机制,选择的关键在于调用场景与连接方式。Python 客户端只需先准备较新的 Python 环境,其余依赖由 SDK 安装。

本文介绍启用 API 密钥认证的 Qdrant 1.18.1 环境,依次演示 curl REST 请求、Python 异步客户端、原生 Python gRPC 存根和 Go 客户端如何操作相同数据。文中的执行结果与延迟均为原文测试记录,不代表译稿另行进行了环境测试。

原文测试环境:2026 年 5 月,Ubuntu 24.04.4 LTS、Qdrant 1.18.1、qdrant-client 1.18.0、Go 1.22.2、grpcio 1.80。

何时选择 REST,何时选择 gRPC

两种协议的性能特性不同。这里的 REST 使用 HTTP/1.1 与 JSON;每次请求需要建立或复用 TCP/TLS 连接,并完成 JSON 编解码。gRPC 使用 HTTP/2 和二进制 protobuf 消息,可在同一连接上多路复用调用,省去 JSON 转换。

比较项 REST gRPC
传输格式 HTTP/1.1 上的 JSON HTTP/2 上的 protobuf
默认端口 6333 6334
连接开销 不使用连接池时可能反复建连 同一连接复用多个 RPC 流
浏览器支持 支持,Web UI 使用 REST 通常需要 gRPC-Web 代理
工具 curl、Postman、各种 HTTP 库 生成的存根或对应 SDK
单次读取延迟 可能受 JSON 与连接往返开销影响 二进制消息与热连接通常开销较低
并发吞吐 使用 keep-alive 时表现良好 多路复用适合大量并发调用

临时探索、仪表板、Shell 调试以及希望直接使用通用 HTTP 工具的服务,适合 REST。批量写入数百万个点、延迟敏感的推理链路,以及偏好强类型客户端的 Go、Rust 服务,可以考虑 gRPC。具体差异仍应以自身工作负载的测量为准。

启用 API 密钥认证的测试环境

原文在一台 Ubuntu 24.04 主机上用 Docker 运行 Qdrant 1.18.1,同时开放两种协议,并启用 API 密钥。在自己的测试环境中可按下面的命令准备:

mkdir -p /opt/qdrant/storage /opt/qdrant/snapshots
sudo chown -R 1000:1000 /opt/qdrant

sudo docker run -d --name qdrant --restart=always \
  -p 6333:6333 -p 6334:6334 \
  -e QDRANT__SERVICE__API_KEY=mySecretApiKey-2026 \
  -v /opt/qdrant/storage:/qdrant/storage \
  -v /opt/qdrant/snapshots:/qdrant/snapshots \
  qdrant/qdrant:v1.18.1

容器启动后,验证密钥检查确实生效:不携带请求头的请求应被拒绝,携带密钥后应正常返回。

curl -o /dev/null -w "HTTP=%{http_code}\n" \
    http://localhost:6333/collections
# HTTP=401

curl -o /dev/null -w "HTTP=%{http_code}\n" \
    -H "api-key: mySecretApiKey-2026" \
    http://localhost:6333/collections
# HTTP=200

两种协议使用相同密钥。REST 将密钥放在 HTTP 的 api-key 请求头中,gRPC 则放在同名元数据中,不需要另一套密码文件或令牌签发服务。

使用 REST 创建、列出、查看和删除集合

先把重复使用的值保存为 Shell 变量,之后只需在一处修改:

export API="http://localhost:6333"
export KEY="mySecretApiKey-2026"
export H_AUTH="-H api-key: ${KEY}"
export H_JSON="-H Content-Type: application/json"

创建使用 384 维向量和余弦距离的集合:

curl -sS -X PUT "${API}/collections/docs" \
  -H "api-key: ${KEY}" -H "Content-Type: application/json" \
  -d '{"vectors": {"size": 384, "distance": "Cosine"}}' | jq

响应包含创建结果及操作耗时。这里的时间由服务器测量,不包含完整客户端往返:

{
  "result": true,
  "status": "ok",
  "time": 0.267212598
}

列出集合,再读取新集合详情。get_collection 返回完整配置,包括分片数量与载荷是否存储在磁盘上;接手已有集群时,这些信息尤其有用。

curl -sS -H "api-key: ${KEY}" "${API}/collections" | jq
curl -sS -H "api-key: ${KEY}" "${API}/collections/docs" \
  | jq '{status:.result.status, points_count:.result.points_count, config:.result.config.params}'

详情请求可以同时回答三个问题:集合是否健康(status)、包含多少点(points_count),以及向量维度、距离度量、分片数如何配置。监控脚本也可以读取这些字段。

使用 REST 写入、批处理、读取和删除点

Upsert 可在一次请求中插入或更新多个点。每个点包含 ID、向量和可选载荷。需要等待更新操作完成时使用 wait=true;大批量流式写入则可以选择异步提交。

curl -sS -X PUT "${API}/collections/docs/points?wait=true" \
  -H "api-key: ${KEY}" -H "Content-Type: application/json" \
  -d '{
    "points": [
      {"id": 1, "vector": [0.13, 0.84, ... 384 floats ...],
       "payload": {"title": "Intro to vectors", "category": "basics", "price": 0}},
      {"id": 2, "vector": [...],
       "payload": {"title": "HNSW deep dive", "category": "index", "price": 29}},
      {"id": 3, "vector": [...],
       "payload": {"title": "Filter cookbook", "category": "filters", "price": 19}}
    ]
  }' | jq

Qdrant 返回操作 ID。wait=true 表示等待更新操作应用完成,不能据此断言后台 HNSW 索引构建也已全部完成:

{
  "result": {"operation_id": 1, "status": "completed"},
  "status": "ok",
  "time": 0.019022392
}

原文测试环境的 REST 请求体上限为 32 MiB。5,000 个 384 维浮点向量加上载荷,被序列化为约 37 MB JSON 后,会触发 JSON payload (37314329 bytes) is larger than allowed (limit: 33554432 bytes)。可先改为每批 1,000 个点,并根据载荷大小控制批次与背压。32 MiB 是该配置下的限制,不是不可调整的协议上限。

按 ID 读取点,再精确统计数量:

curl -sS -X POST "${API}/collections/docs/points" \
  -H "api-key: ${KEY}" -H "Content-Type: application/json" \
  -d '{"ids": [1, 2, 3], "with_payload": true, "with_vector": false}' | jq

curl -sS -X POST "${API}/collections/docs/points/count" \
  -H "api-key: ${KEY}" -H "Content-Type: application/json" \
  -d '{"exact": true}' | jq

按过滤器删除时,无需自行枚举 ID,直接把载荷条件交给服务器。以下示例删除价格低于 20 的所有点:

curl -sS -X POST "${API}/collections/docs/points/delete?wait=true" \
  -H "api-key: ${KEY}" -H "Content-Type: application/json" \
  -d '{"filter": {"must": [{"key": "price", "range": {"lt": 20}}]}}' | jq

下面的终端截图展示了相同环境中的查询响应:只返回载荷,并对连续 10 次调用进行延迟测量。

Qdrant 1.18.1 REST query_points 查询响应

使用现代 query_points API 搜索

query_points 是替代旧 /search 接口的统一查询方式,支持向量、过滤器、返回数量以及是否包含载荷和向量。结果位于 .result.points[],而非旧接口常见的 .result[],编写 jq 过滤表达式时要留意:

curl -sS -X POST "${API}/collections/docs/points/query" \
  -H "api-key: ${KEY}" -H "Content-Type: application/json" \
  -d '{
    "query": [... 384 floats ...],
    "limit": 3,
    "with_payload": true,
    "filter": {"must": [{"key": "category", "match": {"value": "index"}}]}
  }' | jq '.result.points[] | {id, score, payload}'

Web UI 的 Console 使用相同端点和请求体。在实例的 /dashboard/#/console 页面输入 API 密钥,粘贴请求并点击 RUN,即可在浏览器中排查过滤器为什么没有返回结果。

Web UI 中 docs 集合的 384 维向量与点详情

另一个读取接口 scroll 不需要查询向量,可以按页遍历集合。导出数据、回填字段或抽取测试样本时都很实用。limit 控制页大小,响应中的 next_page_offset 作为下一页的起点:

curl -sS -X POST "${API}/collections/docs/points/scroll" \
  -H "api-key: ${KEY}" -H "Content-Type: application/json" \
  -d '{"limit": 10, "with_payload": true, "with_vector": false}' | jq

通过 REST 管理快照和查看集群状态

先为集合创建快照,再列出现有快照。名称由服务器生成并包含时间戳,后续若要下载,应保存创建响应中的名称:

SNAP=$(curl -sS -X POST "${API}/collections/docs/snapshots" \
  -H "api-key: ${KEY}" | jq -r '.result.name')
echo "snapshot=$SNAP"

curl -sS -H "api-key: ${KEY}" "${API}/collections/docs/snapshots" | jq

集群端点会报告分布式模式是否启用。单节点部署返回 "status": "disabled" 属于正常结果;多节点部署则会显示节点 ID 与共识状态。

curl -sS -H "api-key: ${KEY}" "${API}/cluster" | jq
{
  "result": {"status": "disabled"},
  "status": "ok",
  "time": 0.000002499
}

Python 异步 REST 客户端与耗时测量

Python 客户端封装了两种协议。AsyncQdrantClient 的 REST 路径基于 HTTPX,支持 async/await,也能通过 asyncio.gather 并发调用。安装包含 gRPC 支持的依赖,便于切换协议:

python3 -m venv venv && source venv/bin/activate
pip install "qdrant-client>=1.18.0"

下面的基准脚本先插入 5,000 个随机点,每批 1,000 个,避免超过上述 JSON 限制;随后对同一批数据分别执行 100 次顺序 REST 异步查询与 100 次 gRPC 同步查询。

import asyncio, random, statistics, time
from qdrant_client import AsyncQdrantClient, QdrantClient, models

API_KEY = "mySecretApiKey-2026"
random.seed(7)
QUERY = [round(random.random(), 4) for _ in range(384)]

async def bench_rest_async():
    c = AsyncQdrantClient(url="http://localhost:6333", api_key=API_KEY)
    times = []
    for _ in range(100):
        t0 = time.perf_counter()
        await c.query_points(collection_name="docs", query=QUERY, limit=5)
        times.append((time.perf_counter() - t0) * 1000)
    await c.close()
    return times

def bench_grpc_sync():
    c = QdrantClient(host="localhost", grpc_port=6334,
                     prefer_grpc=True, https=False, api_key=API_KEY)
    times = []
    for _ in range(100):
        t0 = time.perf_counter()
        c.query_points(collection_name="docs", query=QUERY, limit=5)
        times.append((time.perf_counter() - t0) * 1000)
    c.close()
    return times

当 Qdrant 没有启用 TLS 时,需要显式传入 https=False。设置 api_key 后客户端可能默认使用 HTTPS,对明文 HTTP/2 监听器连接会出现 Tls handshake failed: SSL_ERROR_SSL: WRONG_VERSION_NUMBER。这属于客户端与服务器传输配置不一致。

原文测试中,gRPC 单次读取的 p50 延迟约为 REST 的一半,p95 差距更大:

Seeded 'docs' with 5000 points

REST async (HTTP/1.1)     n=100  min=  2.20  p50=  2.40  p95=  2.98  max= 29.83  ms
gRPC sync (HTTP/2)        n=100  min=  0.97  p50=  1.11  p95=  1.38  max=  9.53  ms

REST async 100-in-parallel  total=158.3 ms  ~632 req/s

可以得到两点认识。首先,REST 单次查询的中位延迟约 2.4 毫秒,对多数 Web 请求已足够低。其次,启用 HTTP/1.1 keep-alive 并采用异步并发后,原文中一个 Python 进程仍实现了每秒 632 次纯搜索请求。该结果取决于测试环境,不能直接当作生产容量承诺。

Python REST 异步与 gRPC 同步查询基准结果

直接使用原生 Python gRPC 存根

qdrant-client 包附带生成的 protobuf 存根。直接导入后可绕过高层封装,自行控制通道选项、截止时间,或接入已有 gRPC 服务体系。

import grpc
from qdrant_client.grpc import (
    CollectionsStub, ListCollectionsRequest, CreateCollection,
    DeleteCollection, VectorsConfig, VectorParams, Distance,
    PointsStub, UpsertPoints, PointStruct, PointId, Vectors, Vector,
    QueryPoints, Query, VectorInput, DenseVector, Value,
)

API_KEY = "mySecretApiKey-2026"
META = [("api-key", API_KEY)]

channel = grpc.insecure_channel("localhost:6334")
collections = CollectionsStub(channel)
points = PointsStub(channel)

collections.Create(
    CreateCollection(
        collection_name="grpc_demo",
        vectors_config=VectorsConfig(
            params=VectorParams(size=8, distance=Distance.Cosine),
        ),
    ),
    metadata=META,
)

查询必须遵循 protobuf 的嵌套消息结构。特别注意 VectorInput(dense=DenseVector(data=q_vec)):这里需要 DenseVector,不能传入普通 Vector。类型混用会在运行时触发 TypeError: Parameter to initialize message field must be dict or instance of same class: expected DenseVector got Vector。

q_vec = [0.33, 0.95, 0.04, 0.41, 0.55, 0.82, 0.16, 0.71]
qr = points.Query(
    QueryPoints(
        collection_name="grpc_demo",
        query=Query(nearest=VectorInput(dense=DenseVector(data=q_vec))),
        limit=3,
    ),
    metadata=META,
)
for r in qr.result:
    print(f"  id={r.id.num}  score={r.score:.4f}")
print(f"server_time={qr.time*1000:.2f} ms")

原文结果中,服务器处理耗时为 0.29 毫秒,客户端测得总耗时为 0.72 毫秒,差值来自回环链路上的调用与协议开销。跨机器部署后还会加入网络延迟,REST 与 gRPC 的比例需要重新测量。

使用官方 Go SDK 调用 gRPC

Go SDK 在生成的存根之上提供更易用的接口。先初始化模块并安装依赖:

mkdir qdrant-go && cd qdrant-go
go mod init qdrant-go-demo
go get github.com/qdrant/go-client/qdrant@latest

下面的完整客户端创建集合,写入 5 个包含字符串和浮点载荷的点,再查询并返回载荷。UseTLS: false 对应 Python 中的 https=False。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"

    "github.com/qdrant/go-client/qdrant"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    client, _ := qdrant.NewClient(&qdrant.Config{
        Host: "localhost", Port: 6334,
        APIKey: "mySecretApiKey-2026", UseTLS: false,
    })
    defer client.Close()

    _ = client.DeleteCollection(ctx, "go_demo")
    client.CreateCollection(ctx, &qdrant.CreateCollection{
        CollectionName: "go_demo",
        VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
            Size: 8, Distance: qdrant.Distance_Cosine,
        }),
    })

    rng := rand.New(rand.NewSource(42))
    pts := make([]*qdrant.PointStruct, 5)
    for i := range pts {
        v := make([]float32, 8)
        for j := range v { v[j] = rng.Float32() }
        pts[i] = &qdrant.PointStruct{
            Id:      qdrant.NewIDNum(uint64(i)),
            Vectors: qdrant.NewVectors(v...),
            Payload: qdrant.NewValueMap(map[string]any{
                "category": fmt.Sprintf("c%d", i%3),
                "price":    float64(i) * 9.99,
            }),
        }
    }
    wait := true
    client.Upsert(ctx, &qdrant.UpsertPoints{
        CollectionName: "go_demo", Points: pts, Wait: &wait,
    })

    qVec := []float32{0.61, 0.12, 0.95, 0.37, 0.55, 0.07, 0.82, 0.23}
    t0 := time.Now()
    res, _ := client.Query(ctx, &qdrant.QueryPoints{
        CollectionName: "go_demo",
        Query:          qdrant.NewQuery(qVec...),
        Limit:          qdrant.PtrOf(uint64(3)),
        WithPayload:    qdrant.NewWithPayload(true),
    })
    for _, r := range res {
        fmt.Printf("  id=%v score=%.4f payload=%v\n",
            r.GetId().GetNum(), r.GetScore(), r.GetPayload())
    }
    fmt.Printf("wall=%.2f ms\n", float64(time.Since(t0).Microseconds())/1000.0)
}

运行 go run main.go 后,可以看到每个结果的 ID、分数、载荷和本次调用的总延迟:

Go SDK 的 gRPC 查询结果

Go SDK 在无 TLS 连接上使用 API 密钥时会给出警告。本地回环测试时可以识别其原因;生产网络应同时使用 TLS,避免密钥与业务数据以明文传输。

重试与背压

网络调用难免失败。常用模式是针对暂时性故障,采用次数有限、带抖动的指数退避;请求格式错误或集合不存在等问题,应先修正请求,不能反复重试。

import random, time
from qdrant_client import QdrantClient
from qdrant_client.http.exceptions import ResponseHandlingException, UnexpectedResponse

def with_retry(fn, *, attempts=5, base_delay=0.1, max_delay=5.0):
    transient = (ResponseHandlingException, ConnectionError)
    for i in range(attempts):
        try:
            return fn()
        except UnexpectedResponse as exc:
            if exc.status_code < 500:
                raise   # 4xx are client errors, do not retry
            if i == attempts - 1:
                raise
        except transient:
            if i == attempts - 1:
                raise
        sleep = min(max_delay, base_delay * (2 ** i))
        sleep += random.uniform(0, sleep * 0.25)  # jitter
        time.sleep(sleep)

该封装覆盖了计划重启时的 503、负载下的临时连接重置,以及 Kubernetes 驱逐 Pod 时的 TCP RST,并避免重复提交确定失败的 400、404 请求。原文把所有 4xx 都归为不可重试过于绝对;遇到 429 等暂时性响应,应按服务端提示和限流策略单独处理。

重试模式与 API 密钥的 401、200 响应对比

需要按密钥执行配额时,可在前端放置 L7 代理,例如 Envoy 或 Nginx 的 limit_req,也可限制客户端并发。一个简单起点是使用 asyncio.Semaphore,把并发数设为集群 CPU 数的约两倍,再根据实际延迟、CPU 与排队情况调整。

容易踩到的五个问题

  • REST 请求体大小限制。 原文环境的 32 MiB 限制使 5,000 个 384 维点的 JSON 批次失败。可减小到每批 1,000 个,或评估 gRPC;实际批次应按字节大小控制。
  • 设置密钥后客户端可能默认启用 TLS。 明文监听器需要 Python 的 https=False 或 Go 的 UseTLS: false。无 TLS 认证警告不能直接忽略于生产网络。
  • VectorInput 需要 DenseVector。 原生 gRPC 查询使用 VectorInput(dense=DenseVector(data=...));Upsert 中的 Vector 不能直接用于这个字段。
  • 查询结果位于 .result.points[]。 从旧 /search 迁移时需要同步修改 jq 路径。
  • Console 的基础路径与 Shell 不同。 原文所测 Web UI 的基础地址包含 /dashboard/。把完整 curl 原样粘贴进 Console 可能导致路径错误,应使用 Console 接受的请求格式,并检查最终请求路径。

根据调用场景逐步决定协议

多数团队会同时使用两种协议:REST 用于运行手册里的 curl、监控面板与浏览器调试,gRPC 用于对延迟敏感的服务链路。可参考以下原则:

  • 浏览器与 Shell 脚本优先使用 REST,工具支持最直接。
  • Go 或 Rust 服务若在关键路径中频繁查询 Qdrant,可评估 gRPC。一次请求扇出多次查询时,每次调用的差异会累积。
  • Python 或 Node.js 服务可先使用 REST。只有性能分析确认 Qdrant 调用显著影响尾延迟时,再评估切换成本与收益;中位数节省约 1 毫秒未必值得额外维护。

两种协议共享存储、安全机制与查询语义,可以按每个调用方的需要选择,无需为此改动服务器端数据。

Avatar photo

关于 木子

Email: muzi@vip.rockylinux.cn 微信:VebinLee QQ: 2306867585
Founder of the Rocky Linux Chinese community, MVP、VMware vExpert、TVP, advocate for cloud native technologies, with over ten years of experience in site reliability engineering (SRE) and the DevOps field. Passionate about Cloud Computing、Microservices、CI&CD、DevOps、Kubernetes, currently dedicated to promoting and implementing Rocky Linux in Chinese-speaking regions.
用一杯咖啡支持我们,我们的每一篇[文档]都经过实际操作和精心打磨,而不是简单地从网上复制粘贴。期间投入了大量心血,只为能够真正帮助到您。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇