Article

向量数据库Qdrant

更新于:2026-07-16

第一章:Qdrant 概述与核心概念

1.1 什么是向量数据库

概念名称说明注意事项
向量数据库专为高效存储、索引和检索高维向量(embedding)设计的数据库系统。不适用于传统结构化数据的复杂事务处理;主要用于相似性搜索场景。
Embedding(嵌入)将文本、图像、音频等非结构化数据通过模型(如 BERT、CLIP)转换为数值向量。向量维度通常为几十到几千维;质量直接影响检索效果。
相似性搜索根据向量之间的距离(如余弦相似度)查找最相近的条目。需选择合适的距离度量方式;高维空间存在”维度灾难”问题。
近似最近邻(ANN)在可接受误差范围内快速找到近似最近邻,而非精确最近邻,以提升性能。Qdrant 使用 HNSW 等算法实现 ANN;牺牲少量精度换取大幅速度提升。

1.2 Qdrant 简介与特性

概念名称说明注意事项
Qdrant开源、高性能、支持过滤的向量搜索引擎,用 Rust 编写,提供 gRPC 和 REST API。开源版本功能已非常完整;企业版提供额外安全与管理功能。
高性能检索基于 HNSW 图索引,支持毫秒级响应,适用于实时应用。性能受向量维度、索引参数、硬件影响;需合理配置。
Payload 过滤支持在向量搜索时附加结构化元数据(如标签、类别、时间)进行条件过滤。Payload 字段需提前建立索引以加速过滤;否则会全扫描。
多语言客户端官方提供 Python、TypeScript、Rust 等 SDK,社区支持 Go、Java 等。推荐使用官方维护的客户端以确保兼容性。
云原生支持支持 Docker、Kubernetes 部署,提供 Qdrant Cloud 托管服务。本地开发推荐 Docker 快速启动;生产环境建议配置持久化卷。

1.3 Qdrant 与其他向量数据库对比(如 Pinecone、Weaviate、Milvus)

框架名称所属类别用途适用场景注意事项
Qdrant开源向量数据库高效向量存储与带过滤的相似性搜索需要自托管、强过滤能力、低延迟的语义搜索或 RAG 应用社区活跃,文档完善;企业功能需付费
Pinecone托管向量数据库全托管向量检索服务快速上线、无需运维、适合初创团队或原型验证闭源;成本随数据量/请求量线性增长
Weaviate向量+图数据库结合语义搜索与知识图谱需要将向量与实体关系联合建模的场景(如智能问答、知识库)架构较重;学习曲线陡峭
Milvus开源向量数据库超大规模向量检索(十亿级)工业级 AI 平台、需要分布式集群与高吞吐的场景部署复杂;依赖较多组件(如 etcd, MinIO)

1.4 核心术语解释(Point、Vector、Payload、Collection、Distance Metric 等)

概念名称说明注意事项
CollectionQdrant 中的顶层容器,类似关系数据库中的”表”,用于组织一组向量数据。创建时需指定向量维度和距离度量;不可修改(需重建)。
PointCollection 中的基本数据单元,包含一个向量和可选的 Payload 元数据。每个 Point 必须有唯一 ID(整数或字符串)。
Vector浮点数组,表示数据的嵌入向量,如 [0.23, -0.45, ..., 0.89]所有 Point 的向量维度必须一致;不支持稀疏向量(截至 v1.10)。
Payload附加在 Point 上的 JSON 兼容元数据,如 {"category": "tech", "score": 9.5}可用于过滤、排序;建议对高频查询字段建立索引。
Distance Metric衡量两个向量相似性的函数,Qdrant 支持 Cosine、Dot、Euclidean。Cosine 最常用(忽略向量长度);Dot 需归一化才等价于 Cosine。
HNSWHierarchical Navigable Small World,Qdrant 默认使用的 ANN 索引结构。构建索引需额外时间和内存;可通过 m、ef_construct 参数调优。
ShardCollection 的分片单元,用于水平扩展和并行查询。单机模式下通常只有一个分片;集群模式可配置多个。

第二章:环境搭建与快速入门

2.1 安装 Qdrant(Docker / Binary / Cloud)

步骤名称操作细节注意事项
使用 Docker 安装执行命令:docker pull qdrant/qdrant,然后运行:docker run -p 6333:6333 qdrant/qdrant需提前安装 Docker;默认数据存储在内存中,重启丢失(如需持久化见 2.2)
使用二进制文件安装从 GitHub Releases 下载对应平台的压缩包(如 qdrant-x86_64-unknown-linux-gnu.tar.gz),解压后运行 ./qdrant适用于无 Docker 环境;需手动管理进程和配置文件
使用 Qdrant Cloud访问 https://cloud.qdrant.io/ 注册账号,创建项目并获取 API URL 和密钥免运维;免费额度有限;适合快速验证或生产托管

2.2 启动本地 Qdrant 实例

步骤名称操作细节注意事项
启动带持久化的 Docker 实例创建目录 mkdir ./qdrant_storage,运行:docker run -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant数据将保存在本地 ./qdrant_storage 目录,重启容器后保留
自定义配置启动下载默认配置 config.yaml,修改参数(如 storage_pathcors),挂载配置:docker run -p 6333:6333 -v $(pwd)/config.yaml:/qdrant/config/config.yaml qdrant/qdrant配置文件路径必须为 /qdrant/config/config.yaml
验证服务是否启动成功访问 http://localhost:6333 或执行 curl http://localhost:6333成功返回 JSON 包含 "title": "qdrant - vector search engine"

2.3 使用 REST API 与 Qdrant 交互

方法名称语法(HTTP)用途代码示例注意事项
获取服务信息GET /检查 Qdrant 是否运行curl http://localhost:6333无需认证;返回版本和状态信息
列出所有 CollectionGET /collections查看已有集合curl http://localhost:6333/collections返回 JSON 结构包含 collections 列表
创建 CollectionPUT /collections/{collection_name}创建新集合见下方代码块vectors.size 必须为正整数;distance 可选 Cosine/Dot/Euclidean
插入 PointPUT /collections/{name}/points批量插入向量数据见下方代码块id 可为整数或字符串;vector 长度必须匹配集合定义
搜索 PointPOST /collections/{name}/points/search执行相似性搜索见下方代码块limit 控制返回结果数量;可添加 filter 字段进行条件过滤

创建 Collection:

curl -X PUT http://localhost:6333/collections/test_collection \
  -H "Content-Type: application/json" \
  -d '{"vectors": {"size": 4, "distance": "Cosine"}}'

插入 Point:

curl -X PUT http://localhost:6333/collections/test_collection/points \
  -H "Content-Type: application/json" \
  -d '{"points": [{"id": 1, "vector": [0.1,0.2,0.3,0.4], "payload": {"city": "Berlin"}}]}'

搜索 Point:

curl -X POST http://localhost:6333/collections/test_collection/points/search \
  -H "Content-Type: application/json" \
  -d '{"vector": [0.15,0.25,0.35,0.45], "limit": 3}'

2.4 使用 Python 客户端连接 Qdrant

方法名称语法用途代码示例注意事项
安装客户端pip install qdrant-client安装官方 Python SDKpip install qdrant-client需 Python ≥3.7
初始化客户端from qdrant_client import QdrantClient连接本地 Qdrant 实例client = QdrantClient("localhost", port=6333)默认端口 6333;支持 https 和 API key(云服务)
连接 Qdrant Cloudclient = QdrantClient(url="https://xxx.us-east-1.aws.cloud.qdrant.io", api_key="your_key")连接托管服务见下方代码块api_key 在 Cloud 控制台获取;必须使用 HTTPS
检查连接状态client.get_collections()验证连接是否成功collections = client.get_collections(); print(collections)返回 CollectionInfo 列表;若连接失败抛出异常

连接 Qdrant Cloud 示例:

client = QdrantClient(
    url="https://abcd.us-east-1.aws.cloud.qdrant.io",
    api_key="sk-xxxx"
)

2.5 创建第一个 Collection 并插入数据

方法名称语法用途代码示例注意事项
创建 Collectionclient.create_collection(collection_name, vectors_config=VectorParams(size=dim, distance=Distance.COSINE))定义向量集合结构见下方代码块size 必须与后续插入向量长度一致;Distance 枚举值:COSINE, DOT, EUCLID
插入单个 Pointclient.upsert(collection_name, points=[PointStruct(id=1, vector=[...], payload={})])插入一条带元数据的向量见下方代码块id 必须唯一;payload 为 dict
批量插入 Pointsclient.upsert(collection_name, points=point_list)高效插入多条数据见下方代码块建议批量大小 100~1000;过大可能超时
验证插入结果client.scroll(collection_name, limit=5)查看已插入的数据retrieved = client.scroll("demo", limit=5); print(retrieved[0])scroll 用于遍历;search 用于相似性查询

创建 Collection:

from qdrant_client.models import VectorParams, Distance

client.create_collection(
    collection_name="demo",
    vectors_config=VectorParams(size=4, distance=Distance.COSINE)
)

插入单个 Point:

from qdrant_client.models import PointStruct

client.upsert(
    collection_name="demo",
    points=[PointStruct(id=1, vector=[0.1,0.2,0.3,0.4], payload={"name": "Alice"})]
)

批量插入 Points:

points = [
    PointStruct(id=i, vector=[i*0.1]*4, payload={"idx": i})
    for i in range(1, 6)
]
client.upsert("demo", points=points)

第三章:Collection 管理

3.1 Collection 的创建与配置参数详解

方法名称语法(Python 客户端)用途代码示例注意事项
create_collectionclient.create_collection(collection_name, vectors_config, shard_number=None, replication_factor=None, write_consistency_factor=None, on_disk_payload=False, hnsw_config=None, optimizers_config=None, wal_config=None)创建新 Collection,支持完整配置见下方代码块vectors_config 必填;其他参数为可选,用于性能调优;创建后无法修改 size 和 distance
VectorParamsVectorParams(size=int, distance=Distance)定义向量基本结构VectorParams(size=384, distance=Distance.DOT)size 必须 > 0;distance 取值:COSINE / DOT / EUCLID
HnswConfigDiffHnswConfigDiff(m=None, ef_construct=None, full_scan_threshold=None, max_indexing_threads=None, on_disk=None)配置 HNSW 索引参数HnswConfigDiff(m=16, ef_construct=100, on_disk=True)m 控制图连接数(默认 16);ef_construct 影响索引质量(默认 100);on_disk=True 可降低内存占用
OptimizersConfigDiffOptimizersConfigDiff(deleted_threshold=None, vacuum_min_vector_number=None, default_segment_number=None, max_segment_size=None, memmap_threshold=None, indexing_threshold=None)控制段合并与内存策略OptimizersConfigDiff(memmap_threshold=10000)memmap_threshold:超过此向量数使用磁盘映射;调整可平衡内存与性能

创建 Collection 完整示例:

from qdrant_client.models import VectorParams, Distance

client.create_collection(
    collection_name="articles",
    vectors_config=VectorParams(size=768, distance=Distance.COSINE),
    shard_number=2,
    on_disk_payload=True
)

3.2 Collection 的更新与删除

方法名称语法用途代码示例注意事项
update_collectionclient.update_collection(collection_name, optimizer_config=None, collection_params=None)更新 Collection 的优化器或参数(有限支持)见下方代码块不能修改向量维度或距离度量;主要用于调整索引触发阈值等运行时参数
delete_collectionclient.delete_collection(collection_name)删除整个 Collection 及其所有数据client.delete_collection("temp_data")操作不可逆;删除后释放磁盘和内存资源;若 Collection 不存在,抛出异常(可捕获)
collection_existsclient.collection_exists(collection_name)检查 Collection 是否存在(v1.9+)exists = client.collection_exists("articles")推荐在创建前检查,避免重复创建错误

更新 Collection 示例:

from qdrant_client.models import OptimizersConfigDiff

client.update_collection(
    "articles",
    optimizer_config=OptimizersConfigDiff(indexing_threshold=50000)
)

3.3 Collection 的元信息查询

方法名称语法用途代码示例注意事项
get_collectionclient.get_collection(collection_name)获取 Collection 的完整元信息info = client.get_collection("articles"); print(info.config.params.vectors.size)返回包含 vectors、hnsw_config、optimizers 等配置的对象
get_collectionsclient.get_collections()列出所有 Collection 名称及简要信息见下方代码块返回 CollectionInfo 列表;适用于监控或管理界面
retrieve_collection_statsclient.http.client.points_count(collection_name)(间接方式)获取 Point 总数等统计信息官方暂未提供直接 stats APIQdrant 暂无内置”统计信息”接口;可通过 count 方法获取精确数量(见第四章)

:截至 Qdrant v1.10,get_collection 是获取元信息的主要方法,返回结构包含:

  • config.params.vectors:向量配置
  • config.hnsw_config:HNSW 索引参数
  • config.optimizer_config:优化器设置
  • points_count:当前 Point 数量(部分版本支持)

列出所有 Collection 示例:

collections = client.get_collections()
for c in collections.collections:
    print(c.name)

3.4 向量维度与距离度量选择(Cosine、Dot、Euclidean)

概念名称说明适用场景注意事项
向量维度(size)向量中浮点数的个数,如 BERT 输出通常为 768 维文本嵌入:384/768/1024;图像特征:512/2048所有 Point 必须严格匹配该维度;维度越高,内存和计算开销越大;不支持动态变更
Cosine 距离衡量向量方向相似性,忽略长度:cos(θ) = (A·B) / (‖A‖‖B‖)文本语义搜索;已归一化的向量最常用;对向量缩放不敏感;Qdrant 中值越小表示越相似(实际存储 1 - cos(θ)
Dot Product(点积)A·B = Σ(Ai × Bi)未归一化的稠密检索;需保留向量幅度信息若向量已 L2 归一化,则 Dot ≈ Cosine;否则可能受向量长度主导,导致偏差
Euclidean(欧氏距离)√Σ(Ai - Bi)²图像特征、坐标数据等几何空间对异常值敏感;高维下区分度下降(“维度灾难”);值越小越相似

距离类型常量(Python):

距离类型常量对应值说明
Distance.COSINE"Cosine"默认推荐
Distance.DOT"Dot"用于未归一化向量
Distance.EUCLID"Euclid"用于几何距离场景

重要提示:

  • 一旦 Collection 创建,无法更改 distance 或 size,需重建。
  • 使用 Dot 时,若希望等效于 Cosine,应在插入前对向量做 L2 归一化:
import numpy as np

vector = np.array([0.3, 0.4, 0.5])
norm_vec = vector / np.linalg.norm(vector)

第四章:数据操作(CRUD)

4.1 插入向量数据(Points)

方法名称语法用途代码示例注意事项
upsertclient.upsert(collection_name, points=[PointStruct(id, vector, payload)])插入或覆盖单个/多个 Point见下方代码块id 可为 int 或 str;若 id 已存在,则覆盖原 Point(全量替换);payload 可为 None
PointStructPointStruct(id=id, vector=vec, payload=payload)构造单个 Point 对象PointStruct(id="doc_1", vector=[0.5]*768, payload={"author": "Alice"})vector 必须是 float 列表;长度必须与 Collection 定义一致

插入 Point 示例:

from qdrant_client.models import PointStruct

client.upsert(
    collection_name="demo",
    points=[PointStruct(id=101, vector=[0.1,0.2,0.3,0.4], payload={"title": "Intro"})]
)

4.2 批量插入与更新

方法名称语法用途代码示例注意事项
upsert(批量)client.upsert(collection_name, points=point_list)一次性插入/更新多个 Point见下方代码块推荐批量大小:100~1000;过大可能导致超时或内存溢出;原子性:部分失败不影响已成功写入的 Point
使用 ID + 向量列表(简化写法)client.upsert(collection_name, points=Batch(ids=[...], vectors=[...], payloads=[...]))更高效的批量写入方式见下方代码块Batch 是官方推荐的大批量写入方式;payloads 元素可为 dict 或 None;向量、ID、Payload 长度必须一致

批量 upsert 示例:

points = [
    PointStruct(id=i, vector=[i*0.1]*4, payload={"idx": i})
    for i in range(1000, 1010)
]
client.upsert("demo", points=points)

使用 Batch 简化写法:

from qdrant_client.models import Batch

client.upsert(
    "demo",
    points=Batch(
        ids=[1, 2],
        vectors=[[0.1,0.2,0.3,0.4], [0.5,0.6,0.7,0.8]],
        payloads=[{"tag": "A"}, {"tag": "B"}]
    )
)

4.3 按 ID 查询与删除 Point

方法名称语法用途代码示例注意事项
retrieveclient.retrieve(collection_name, ids=[id1, id2])按 ID 获取 Point 的向量和 Payload见下方代码块返回顺序不一定与请求 ID 顺序一致;不存在的 ID 被忽略(不报错)
deleteclient.delete(collection_name, points_selector=PointIdsList(points=[id1, id2]))按 ID 删除 Point见下方代码块删除后不可恢复;支持整数和字符串 ID 混合;返回操作状态(如 deleted_count)
PointIdsListPointIdsList(points=[id1, id2, ...])构造 ID 删除选择器PointIdsList(points=[1, 2, "abc"])用于 delete 的 points_selector 参数

按 ID 查询示例:

result = client.retrieve("demo", ids=[101, "doc_1"])
for r in result:
    print(r.id, r.payload)

按 ID 删除示例:

from qdrant_client.models import PointIdsList

client.delete("demo", points_selector=PointIdsList(points=[101, "doc_1"]))

4.4 Payload 字段的增删改查

方法名称语法用途代码示例注意事项
set_payloadclient.set_payload(collection_name, payload={...}, points=[id1, id2])为指定 Point 设置/覆盖 Payload 字段client.set_payload("demo", payload={"status": "published"}, points=[101])会覆盖同名字段;新增字段不会影响其他字段
overwrite_payloadclient.overwrite_payload(collection_name, payload={...}, points=[id1])完全替换 Point 的整个 Payloadclient.overwrite_payload("demo", payload={"title": "New"}, points=[101])原有 Payload 被清空;相当于先删除再设置
delete_payloadclient.delete_payload(collection_name, keys=["key1", "key2"], points=[id1])删除指定 Payload 字段client.delete_payload("demo", keys=["temp_flag"], points=[101])仅删除指定 key;若 key 不存在,静默忽略
clear_payloadclient.clear_payload(collection_name, points=[id1])清空 Point 的所有 Payloadclient.clear_payload("demo", points=[101])Payload 变为空 dict {};向量仍保留

4.5 向量与 Payload 的联合存储策略

概念名称说明最佳实践注意事项
向量存储Qdrant 将向量存储在内存或磁盘(取决于 HNSW on_disk 配置)高频检索向量建议保留在内存;超大规模可启用 on_disk_vectors(v1.9+)向量无法单独更新;需通过 upsert 替换整个 Point
Payload 存储Payload 默认存储在内存;可通过 on_disk_payload=True 存于磁盘大 Payload(如长文本)建议启用 on_disk_payload;小而频繁查询的字段应保留在内存on_disk_payload 会降低过滤性能;创建 Collection 时指定,不可更改
Payload 索引对高频过滤字段建立索引可加速查询使用 client.create_payload_index(collection, field_name, field_type)未索引字段在 filter 时会全扫描;支持 keyword、integer、float、geo 等类型;索引占用额外磁盘空间
联合更新策略向量和 Payload 必须一起 upsert若仅需更新 Payload,仍需提供原向量;可缓存向量避免重复计算Qdrant 不支持”仅更新 Payload”或”仅更新向量”;设计应用层时应保留原始向量副本

Payload 索引创建示例:

from qdrant_client.models import PayloadSchemaType

client.create_payload_index(
    collection_name="articles",
    field_name="category",
    field_schema=PayloadSchemaType.KEYWORD
)

第五章:向量检索与搜索

5.1 基础相似性搜索(search)

方法名称语法用途代码示例注意事项
searchclient.search(collection_name, query_vector=vector, limit=k)执行基础向量相似性搜索见下方代码块query_vector 长度必须匹配 Collection 定义;返回按相似度降序排列的结果;score 含义取决于 distance:Cosine 越大越相似,Euclid 越小越相似
SearchRequest(底层)client.http.points_api.search_points(...)直接调用 REST API(不推荐常规使用)一般通过 client.search 封装即可仅用于调试或特殊需求

search 示例:

result = client.search(
    collection_name="demo",
    query_vector=[0.15, 0.25, 0.35, 0.45],
    limit=3
)
for hit in result:
    print(hit.id, hit.score)

5.2 带 Payload 过滤的条件搜索(filter)

方法名称语法用途代码示例注意事项
search + filterclient.search(collection_name, query_vector=vec, query_filter=Filter(...), limit=k)在向量搜索基础上添加结构化条件过滤见下方代码块must 表示 AND 条件;should 表示 OR;过滤字段建议提前建立 Payload 索引;未索引字段会触发全扫描,性能差
常用过滤条件构造FieldCondition(key="field", match=MatchValue(value="x"))构建不同类型的过滤条件见下方代码块支持 keyword、integer、float、geo、text;MatchAny 用于多值匹配(v1.8+)

带过滤搜索示例:

from qdrant_client.models import Filter, FieldCondition, MatchValue

result = client.search(
    "articles",
    query_vector=[0.1]*768,
    query_filter=Filter(
        must=[FieldCondition(key="category", match=MatchValue(value="tech"))]
    ),
    limit=5
)

多种过滤条件构造:

# 数值范围过滤
FieldCondition(key="year", range=Range(gte=2020, lte=2025))

# 数值阈值过滤
FieldCondition(key="score", range=Range(gte=5.0))

# 多值匹配
FieldCondition(key="tags", match=MatchAny(any=["a", "b"]))

5.3 多向量融合搜索(multi-vector / rerank)

概念/方法名称说明代码示例注意事项
多向量融合(非原生支持)Qdrant 原生不支持单 Point 存储多个向量;需应用层实现融合方案1:对多个 query_vector 分别搜索,再加权合并结果;方案2:在插入前将多向量拼接或加权平均为单向量Qdrant v1.10 不支持 multi-vector 字段;建议在 embedding 阶段完成融合(如 CLIP 图文联合嵌入)
Rerank(重排序)先用 Qdrant 快速召回 top-k,再用精排模型(如 Cross-Encoder)重打分见下方代码块Qdrant 仅负责粗排(ANN);精排需外部模型;适用于 RAG 场景;可结合 LangChain 的 ReRanker 组件

:截至 Qdrant v1.10,官方未提供内置多向量或 rerank API,需在客户端实现。

Rerank 示例:

hits = client.search("docs", query_vec, limit=50)
reranked = cross_encoder.rerank(query, [h.payload["text"] for h in hits])

5.4 搜索参数详解(top_k、score_threshold、with_payload、with_vector)

参数名称语法位置用途代码示例注意事项
limit(即 top_k)client.search(..., limit=10)控制返回结果数量client.search("demo", vec, limit=10)默认值通常为 10;最大值受配置限制(默认 10000)
score_thresholdclient.search(..., score_threshold=0.5)过滤低于阈值的结果client.search("demo", vec, score_threshold=0.8)仅保留 score ≥ threshold 的结果;Cosine 范围 [0,1];Dot/Euclid 需根据数据分布设定
with_payloadclient.search(..., with_payload=True/False/["field1"])控制是否返回 Payloadclient.search("demo", vec, with_payload=["title"])True:返回全部 Payload;False:不返回;列表:仅返回指定字段(节省带宽)
with_vectorclient.search(..., with_vector=True/False)控制是否返回原始向量client.search("demo", vec, with_vector=False)默认 False(节省传输开销);调试或重计算时可设为 True

5.5 分页与游标支持(offset / scroll)

方法名称语法用途代码示例注意事项
search + offsetclient.search(..., offset=10, limit=10)实现简单分页(跳过前 N 条)page2 = client.search("demo", vec, offset=10, limit=10)仅适用于小偏移量(< 1000);大 offset 性能急剧下降(需遍历前 N 条)
scrollclient.scroll(collection_name, scroll_filter=Filter(...), limit=page_size, offset=last_id)基于游标的高效遍历(推荐)见下方代码块offset 为上一批最后一个 Point ID;支持按 Payload 过滤;适合导出或全量遍历
ScrollResponse返回 (List[Record], Optional[PointId])scroll 方法的返回结构records, next_id = client.scroll("demo", limit=5)当 next_id 为 None 时表示已到末尾;Record 包含 id、vector、payload

分页建议:

  • 用户翻页(如第1/2/3页):可用 offset(数据量小时)
  • 数据导出、批处理:必须用 scroll
  • 搜索结果分页:不推荐深度分页,应限制总页数

scroll 示例:

points, next_offset = client.scroll("demo", limit=10)
# 下一页
points2, _ = client.scroll("demo", limit=10, offset=next_offset)

第六章:高级功能

6.1 Payload 索引优化(关键字索引、范围索引)

方法名称语法用途代码示例注意事项
create_payload_indexclient.create_payload_index(collection_name, field_name, field_schema)为指定 Payload 字段创建索引见下方代码块支持 KEYWORD、INTEGER、FLOAT、GEO、TEXT;创建后可加速 filter 查询;索引构建异步进行,不影响写入
delete_payload_indexclient.delete_payload_index(collection_name, field_name)删除指定字段的索引client.delete_payload_index("articles", "temp_tag")删除后该字段过滤将全扫描;释放索引占用的磁盘空间
PayloadSchemaType 枚举KEYWORD / INTEGER / FLOAT / GEO / TEXT指定索引类型PayloadSchemaType.INTEGER # 用于 score 字段TEXT 索引需在配置文件中启用 tokenizer(如 “whitespace”);GEO 字段必须是标准 GeoJSON 格式

创建 Payload 索引示例:

from qdrant_client.models import PayloadSchemaType

client.create_payload_index(
    "articles",
    "category",
    field_schema=PayloadSchemaType.KEYWORD
)

6.2 向量量化与内存优化(HNSW、Product Quantization)

概念/方法名称说明配置方式注意事项
HNSW 索引参数调优通过 m 和 ef_construct 控制图密度与构建质量create_collection 中设置:HnswConfigDiff(m=24, ef_construct=200)m 越大,内存越高,召回率越高;ef_construct 影响索引构建时的搜索深度;默认 m=16, ef_construct=100
on_disk_vectors(v1.9+)将向量存储在磁盘而非内存,降低 RAM 占用create_collection(..., on_disk_vectors=True)仅支持单向量 Collection;检索延迟略增(需磁盘 I/O);适用于超大规模(>1亿点)
Product Quantization(PQ)Qdrant 暂未原生支持 PQ;依赖 HNSW 实现 ANN无直接 API官方路线图中提及未来可能支持;当前内存优化主要靠 on_disk_vectors + HNSW
内存映射阈值(memmap_threshold)超过指定向量数后使用磁盘映射OptimizersConfigDiff(memmap_threshold=20000)避免小集合占用过多内存;与 on_disk_vectors 不冲突

建议:

  • 小于 100 万点:全内存 + 默认 HNSW
  • 100 万~1 亿点:on_disk_vectors=True + 调高 m/ef
  • 超过 1 亿点:考虑分片集群 + SSD 存储

6.3 分片与集群部署(Sharding & Replication)

概念名称说明配置方式注意事项
分片(Sharding)将 Collection 数据水平拆分到多个分片,提升并行查询能力create_collection(..., shard_number=4)单机模式下分片仍存在,但由同一节点管理;集群模式下分片分布到不同节点;创建后不可修改 shard_number
副本(Replication)每个分片可配置多个副本,实现高可用create_collection(..., replication_factor=2)仅在集群模式生效;replication_factor ≥ 2 才具备容错能力;写入需多数副本确认(write_consistency_factor)
write_consistency_factor控制写入成功所需的最小副本数create_collection(..., write_consistency_factor=2)必须 ≤ replication_factor;默认为 1(性能优先);设为 2 可避免脑裂写入
集群部署方式使用多个 Qdrant 节点组成 Raft 共识集群启动时指定 --uri--bootstrap需配置共享存储或云磁盘;官方提供 Kubernetes Helm Chart;节点间通过 gRPC 通信

集群启动示例:

# 节点1
./qdrant --uri http://node1:6335 --bootstrap
# 节点2
./qdrant --uri http://node2:6335 --bootstrap http://node1:6335

6.4 快照备份与恢复

方法名称语法用途代码示例注意事项
create_snapshotclient.create_snapshot(collection_name)创建 Collection 的快照(异步)info = client.create_snapshot("articles"); snapshot_name = info.name快照保存在 storage/snapshots 目录;可跨版本恢复(需兼容);创建期间 Collection 只读
list_snapshotsclient.list_snapshots(collection_name)列出所有快照snapshots = client.list_snapshots("articles")返回快照名、创建时间、大小等信息
delete_snapshotclient.delete_snapshot(collection_name, snapshot_name)删除指定快照client.delete_snapshot("articles", "articles-2026-02-03-09-30.snapshot")释放磁盘空间
restore_from_snapshot需通过 REST API 或文件系统操作从快照恢复数据停止 Qdrant → 复制 snapshot 文件到 storage/snapshots → 启动时添加 --restore-snapshot 参数官方 Python 客户端不支持 restore API;恢复会覆盖同名 Collection;建议在独立实例测试后再生产恢复

快照路径示例:

./qdrant_storage/snapshots/articles/articles-2026-02-03-09-30.snapshot

6.5 权限控制与 API Key 管理(企业版)

功能名称说明配置方式注意事项
API Key 认证限制客户端访问权限(仅企业版)config.yaml 中启用:service.api_key: "your-secret-key"开源版不支持 API Key;所有请求需带 header: api-key: your-secret-key
基于角色的访问控制(RBAC)为不同用户分配只读/读写权限(企业版)通过 Qdrant Cloud 控制台或企业 API 配置开源版无此功能;适用于多租户 SaaS 场景
CORS 与网络隔离限制前端或外部服务访问config.yaml 中配置:cors_origin: ["https://your-app.com"]开源版支持 CORS 配置;生产环境应禁用 *
审计日志记录关键操作(如删除、快照)企业版内置;开源版需自行监控 API 调用企业版提供日志导出与告警

重要提示:

  • 开源版 Qdrant 默认无任何认证机制,暴露公网极危险
  • 生产部署必须:
    1. 通过 Nginx/API 网关加鉴权
    2. 或使用 Qdrant Cloud
    3. 或自行封装代理层

第七章:客户端开发实践

7.1 Python SDK 使用详解

方法/类名称语法用途代码示例注意事项
QdrantClient 初始化QdrantClient(host, port)QdrantClient(url, api_key)连接本地或云服务实例client = QdrantClient("localhost", port=6333)本地开发用 host/port;Qdrant Cloud 必须用 url + api_key
models 模块导入from qdrant_client.models import *使用结构化参数对象from qdrant_client.models import VectorParams, Distance, PointStruct官方推荐使用 models 中的类型,避免字典拼写错误
异步客户端from qdrant_client.async_qdrant_client import AsyncQdrantClient支持 async/await见下方代码块需在 async 函数中使用;性能优于同步客户端(高并发场景)
批量插入(Batch)Batch(ids, vectors, payloads)高效批量写入见下方代码块比列表 of PointStruct 更省内存;payloads 元素可为 None

异步客户端示例:

async_client = AsyncQdrantClient("localhost")
await async_client.search(...)

Batch 批量写入示例:

points = Batch(
    ids=[1, 2],
    vectors=[[0.1]*4, [0.2]*4],
    payloads=[{"a": 1}, {"b": 2}]
)
client.upsert("demo", points=points)

7.2 JavaScript / TypeScript 客户端使用

方法/类名称语法用途代码示例注意事项
QdrantClient 初始化new QdrantClient({ host, port })连接本地实例const client = new QdrantClient({ host: "localhost", port: 6333 });安装:npm install @qdrant/js-client-rest
连接 Cloudnew QdrantClient({ url, apiKey })连接托管服务见下方代码块必须使用 HTTPS;apiKey 在 Cloud 控制台获取
创建 Collectionawait client.createCollection(name, config)定义集合结构await client.createCollection("posts", { vectors: { size: 384, distance: "Cosine" } });vectors 配置必须指定 size 和 distance
插入 Pointawait client.upsert(name, { points })写入数据见下方代码块id 可为 number 或 string;payload 为普通对象
搜索await client.search(name, { vector, limit })执行查询const results = await client.search("posts", { vector: [0.15, 0.25], limit: 3 });返回数组,含 score、id、payload 等字段

JavaScript 连接 Cloud 示例:

const client = new QdrantClient({
    url: "https://xxx.cloud.qdrant.io",
    apiKey: "sk-xxxx"
});

JavaScript 插入 Point 示例:

await client.upsert("posts", {
    points: [{ id: 1, vector: [0.1, 0.2], payload: { title: "JS" } }]
});

7.3 Rust 原生客户端调用

方法/结构体语法用途代码示例注意事项
QdrantClient::from_urlQdrantClient::from_url("http://localhost:6333")创建客户端let client = QdrantClient::from_url("http://localhost:6333");安装依赖:qdrant-client = "1" in Cargo.toml;默认使用 reqwest 异步运行时
异步操作(async)client.create_collection(...).await所有操作均为 async见下方代码块必须在 tokio 或 async-std 环境中运行;错误类型为 qdrant::QdrantError
PointStruct 构造PointStruct { id, vector, payload }构建数据点见下方代码块id 类型为 Option;payload 为 HashMap<String, Value>
搜索请求client.search_points(...).await执行向量搜索见下方代码块SearchPoints 结构体支持 filter、with_payload 等字段

Rust 示例:

// 创建 Collection
client.create_collection(
    CreateCollection {
        collection_name: "rust_demo".to_string(),
        vectors_config: ...
    }
).await?;

// 构造 Point
PointStruct {
    id: Some(PointId::from(1)),
    vector: vec![0.1, 0.2, 0.3, 0.4],
    payload: HashMap::new()
}

// 搜索
let res = client.search_points(
    SearchPoints {
        collection_name: "rust_demo".into(),
        vector: vec![0.15; 4],
        limit: 3,
        ..Default::default()
    }
).await?;

7.4 与 LangChain / LlamaIndex 集成

集成方式语法用途代码示例注意事项
LangChain QdrantQdrant.from_documents(documents, embeddings, url, api_key)将文档自动嵌入并存入 Qdrant见下方代码块自动创建 Collection;需安装 langchain[qdrant];支持 metadata 作为 Payload
LangChain 检索vector_store.similarity_search(query, k=4)执行语义搜索results = vector_store.similarity_search("What is AI?", k=3)底层调用 Qdrant search API;可传入 filter 参数
LlamaIndex QdrantQdrantVectorStore(qdrant_client=client, collection_name=name)作为 LlamaIndex 的向量存储后端见下方代码块需先初始化 QdrantClient;与 LlamaIndex 的 IndexBuilder 集成
混合检索(LangChain + Filter)vector_store.similarity_search(query, filter={"category": "tech"})带条件的 RAG 检索results = vector_store.similarity_search("AI trends", filter={"year": {"$gte": 2023}})filter 语法需符合 Qdrant 规范;字段需建立 Payload 索引

LangChain 集成示例:

from langchain.vectorstores import Qdrant

vector_store = Qdrant.from_documents(
    docs, embeddings,
    url="http://localhost:6333",
    collection_name="lc_demo"
)

LlamaIndex 集成示例:

from llama_index.vector_stores import QdrantVectorStore

vector_store = QdrantVectorStore(client, "llama_demo")
storage_context = StorageContext.from_defaults(vector_store=vector_store)

7.5 异步操作与批量处理最佳实践

实践名称操作细节代码示例(Python)注意事项
异步批量插入使用 asyncio.gather 并发 upsert见下方代码块避免单次过大 batch;控制并发数(如用 asyncio.Semaphore
流式插入(生成器)按需生成 Point 并分批写入见下方代码块内存恒定,适合大数据集;最后一批别忘记 flush
异步搜索并发并行执行多个 query见下方代码块适用于多路召回场景;注意 Qdrant 服务器 CPU/内存瓶颈
错误重试机制对网络错误自动重试见下方代码块推荐使用 tenacity 库;避免因瞬时故障中断流程

批量大小建议:

  • 小内存机器:100~200
  • 高配服务器:500~1000
  • 过大会导致 timeout 或 OOM;可通过监控 Qdrant 的 RAM 和 CPU 调整

异步批量插入示例:

async def insert_batch(client, batch):
    return await client.upsert("demo", points=batch)

tasks = [insert_batch(client, b) for b in batches]
await asyncio.gather(*tasks)

流式插入示例:

def point_generator():
    for i in range(100000):
        yield PointStruct(id=i, vector=[...], payload={})

batch = []
for p in point_generator():
    batch.append(p)
    if len(batch) == 500:
        client.upsert("demo", points=batch)
        batch = []

异步搜索并发示例:

queries = [[0.1]*768, [0.2]*768]
tasks = [client.search("demo", q, limit=5) for q in queries]
results = await asyncio.gather(*tasks)

错误重试示例:

from tenacity import retry, stop_after_attempt

@retry(stop=stop_after_attempt(3))
async def safe_search(client, vec):
    return await client.search("demo", vec)

第八章:生产部署与运维

8.1 单机部署 vs 分布式集群

部署模式说明适用场景注意事项
单机部署(Standalone)单个 Qdrant 实例运行,所有数据存储在本地磁盘或内存开发/测试环境;小型应用(< 1000 万向量);低可用性要求场景无高可用能力;单点故障风险;资源受限时可启用 on_disk_vectors
分布式集群(Cluster)多节点组成 Raft 共识集群,支持分片与副本生产环境;数据量 > 1 亿向量;要求 99.9%+ 可用性需至少 3 节点保证容错;网络延迟影响写入性能;配置复杂,需共享存储或云磁盘
对比维度单机部署分布式集群
部署复杂度低(单容器)高(需协调多个节点)
扩展性垂直扩展(加 CPU/RAM)水平扩展(加节点)
容灾能力支持节点故障自动恢复
运维成本高(需监控、日志聚合等)
推荐数据规模< 50M Points> 50M Points

8.2 Docker Compose 与 Kubernetes 部署方案

部署方式操作细节配置示例(关键部分)注意事项
Docker Compose(单机)编写 docker-compose.yml 启动服务见下方代码块必须挂载 storage 目录实现持久化;默认配置适用于开发
Docker Compose(集群模拟)多服务定义 + 自定义网络见下方代码块仅用于测试集群行为;生产不推荐用 Compose 部署集群
Kubernetes(Helm)使用官方 Helm Chart 部署见下方代码块支持 StatefulSet + PVC;自动配置 headless service 和 Raft URI;需提前配置 StorageClass
Kubernetes(自定义 YAML)手动编写 StatefulSet见下方代码块POD_NAME 需通过 downward API 注入;需配置 headless service 用于节点发现

Docker Compose(单机)示例:

version: '3'
services:
  qdrant:
    image: qdrant/qdrant
    ports:
      - "6333:6333"
    volumes:
      - ./qdrant_storage:/qdrant/storage

Docker Compose(集群模拟)示例:

services:
  qdrant1:
    command: ["--uri", "http://qdrant1:6335", "--bootstrap"]
  qdrant2:
    command: ["--uri", "http://qdrant2:6335", "--bootstrap", "http://qdrant1:6335"]

Kubernetes(Helm)示例:

helm repo add qdrant https://qdrant.github.io/qdrant-helm
helm install qdrant qdrant/qdrant \
  --set replicaCount=3 \
  --set persistence.enabled=true

Kubernetes(自定义 YAML)示例:

apiVersion: apps/v1
kind: StatefulSet
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: qdrant
        args: ["--uri", "http://$(POD_NAME).qdrant-headless:6335", ...]

8.3 监控指标与日志分析(Prometheus / Grafana)

组件说明配置方式注意事项
Prometheus 指标暴露Qdrant 内置 /metrics 端点(需启用)config.yaml 中添加:telemetry.prometheus: true默认端口 6333 的 /metrics;指标前缀:qdrant_
关键监控指标qdrant_collection_vector_index_size(索引大小)、qdrant_request_duration_seconds(请求延迟)、qdrant_memory_usage_bytes(内存占用)在 Prometheus 中查询:rate(qdrant_request_duration_seconds_count[5m])建议设置告警:延迟 > 500ms 或错误率 > 1%
Grafana 仪表盘可视化 Qdrant 性能导入官方 Dashboard ID(如 19473)或自定义官方提供 JSON 模板;需关联 Prometheus 数据源
日志收集Qdrant 输出结构化日志到 stdout使用 Fluentd / Loki / Filebeat 采集容器日志日志级别可通过 RUST_LOG 环境变量控制(如 RUST_LOG=info);错误日志包含 trace_id 便于追踪

最小 Prometheus 配置示例:

scrape_configs:
  - job_name: 'qdrant'
    static_configs:
      - targets: ['qdrant:6333']

8.4 性能调优指南(内存、CPU、磁盘 I/O)

资源类型调优参数推荐值说明
内存memmap_threshold(优化器)10000~50000超过该向量数使用磁盘映射,降低 RAM 占用
内存on_disk_vectorstrue(大数据集)向量存磁盘,牺牲少量延迟换内存
CPUmax_indexing_threads(HNSW)CPU 核数 - 1控制索引构建并发线程数
磁盘 I/O使用 NVMe SSDQdrant 对磁盘随机读写敏感,HDD 性能差
索引质量ef_construct(HNSW)100~200越高召回率越好,但构建越慢
索引密度m(HNSW)16~64越大图连接越多,内存和查询时间增加
批量写入单次 batch size200~1000平衡吞吐与内存;过大易超时
场景推荐配置
高召回率搜索m=32, ef_construct=200, ef=128(搜索时)
低延迟写入memmap_threshold=5000, indexing_threshold=50000
超大规模(>1亿)on_disk_vectors=true + SSD + shard_number=8

8.5 版本升级与兼容性说明

操作步骤操作细节注意事项
备份数据创建快照或复制 storage 目录升级前必须备份;快照可通过 client.create_snapshot() 触发
检查兼容性查阅官方 Release Notes主版本升级(如 v1.x → v2.x)可能破坏兼容性;补丁版本(v1.9.1 → v1.9.2)通常安全
停机升级(单机)1. 停止旧容器;2. 启动新版本容器(挂载相同 storage)Qdrant 支持向前兼容 storage 格式;启动时自动迁移元数据(如有)
滚动升级(集群)逐个替换节点(需 >=3 节点)确保集群健康(quorum 可用);新旧版本需兼容(通常限 minor 版本内)
回滚方案启动旧版本镜像 + 原 storage若新版本启动失败,可回退;快照可用于跨版本恢复(需测试)

兼容性规则(截至 v1.10):

  • 存储格式向后兼容:v1.0+ 的数据可被 v1.10 读取
  • REST/gRPC API 保持稳定,废弃接口会提前标记
  • Python SDK 主版本与 Server 主版本对齐(如 SDK v1.x 对应 Server v1.x)

升级命令示例(Docker):

# 停止旧实例
docker stop qdrant
# 拉取新镜像
docker pull qdrant/qdrant:v1.10.0
# 启动新实例(挂载原数据卷)
docker run -d -p 6333:6333 -v ./qdrant_storage:/qdrant/storage qdrant/qdrant:v1.10.0

第九章:典型应用场景案例

9.1 语义搜索系统构建

步骤名称操作细节代码示例(Python)注意事项
文本嵌入生成使用 Sentence-BERT 等模型将文档转为向量见下方代码块嵌入维度需固定(如 384);批量 encode 提升效率
创建 Collection定义向量结构与 Payload 字段见下方代码块推荐 Cosine 距离;Payload 存原文、URL、时间等元数据
批量插入文档将文档 ID、向量、元数据写入 Qdrant见下方代码块ID 可用 UUID 或业务主键;Payload 中保留原文用于展示
语义搜索用户输入查询,返回相似文档见下方代码块可加 filter 限定来源(如 site:example.com);结果按 score 降序排列

文本嵌入生成:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')
vectors = model.encode(documents)

创建 Collection:

client.create_collection(
    "semantic_search",
    vectors_config=VectorParams(size=384, distance=Distance.COSINE)
)

批量插入文档:

points = [
    PointStruct(id=i, vector=vec, payload={"text": doc, "url": url})
    for i, (vec, doc, url) in enumerate(zip(vectors, docs, urls))
]
client.upsert("semantic_search", points=points)

语义搜索:

query_vec = model.encode("How to use Qdrant?")
results = client.search(
    "semantic_search",
    query_vector=query_vec,
    limit=5,
    with_payload=True
)

9.2 推荐系统中的向量召回

步骤名称操作细节代码示例注意事项
用户/物品向量化通过协同过滤或深度模型生成 user/item embeddingitem_vectors = load_item_embeddings()用户向量可实时生成(如行为序列聚合);物品向量通常离线批量生成
构建物品库将物品向量存入 Qdrant,Payload 包含 ID、类目、价格等见下方代码块若使用 Dot Product,向量应归一化;类目字段建议建 KEYWORD 索引
实时召回根据用户向量检索 Top-K 相似物品见下方代码块必须加类目/价格等业务过滤;召回结果供精排模型二次排序
冷启动处理新用户用热门物品向量代替if not user_has_history: user_vec = get_popular_item_avg_vector()可维护一个”热门池” Collection 供 fallback

构建物品库:

client.create_collection("items", VectorParams(size=128, distance=Distance.DOT))
client.upsert("items", points=[
    PointStruct(id=item_id, vector=vec, payload=item_meta)
])

实时召回:

user_vec = get_user_embedding(user_id)
recs = client.search(
    "items",
    query_vector=user_vec,
    query_filter=Filter(must=[FieldCondition(key="category", match=MatchValue("electronics"))]),
    limit=20
)

9.3 RAG(检索增强生成)架构集成

组件说明集成方式注意事项
文档分块将长文档切分为 200~500 字 chunks使用 LangChain TextSplitter避免跨语义切分;保留 chunk 元信息(页码、标题)
向量存储Qdrant 作为向量数据库LangChain Qdrant.from_documents(...)Collection 名按知识库隔离(如 “kb_finance”);Payload 存 chunk_text + source
检索阶段用户问题 → 嵌入 → Qdrant 搜索retriever = vector_store.as_retriever(search_kwargs={"k": 4})可加 metadata 过滤(如 time > 2023);支持多路召回(混合关键词+向量)
生成阶段将检索结果拼入 Prompt 送入 LLM见下方代码块需控制上下文长度(避免超 token);可对检索结果做 rerank 提升相关性

端到端示例(LangChain):

from langchain.chains import RetrievalQA

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=vector_store.as_retriever(),
    chain_type="stuff"
)
response = qa_chain.run("Qdrant 支持哪些距离度量?")

生成阶段 Prompt 拼接示例:

context = "\n".join([doc.page_content for doc in docs])
prompt = f"基于以下信息回答:{context}\n\n问题:{query}"

9.4 图像/音频特征向量检索

步骤名称操作细节代码示例注意事项
特征提取使用 CNN(图像)或 VGGish(音频)生成 embedding见下方代码块输出向量需 flatten;建议 L2 归一化(便于用 Cosine)
存储多模态数据向量 + Payload(含文件路径、标签、类型)见下方代码块Payload 中存原始文件路径或 URL;tags 字段建索引支持过滤
跨模态搜索用图像查相似图像,或用文本查图像(需统一嵌入空间)见下方代码块跨模态需同一嵌入模型(如 CLIP);不同模型生成的向量不可混用
相似图推荐”以图搜图”功能similar_images = client.search("images", query_vector=target_feat, limit=20)可加 geo 过滤(如”附近景点图片”);返回结果前端展示缩略图

特征提取示例:

import torchvision.models as models

resnet = models.resnet50(pretrained=True)
features = resnet(img_tensor)

存储多模态数据:

client.upsert("media", points=[
    PointStruct(
        id=img_id, vector=feat,
        payload={"type": "image", "path": "/data/img123.jpg", "tags": ["cat", "outdoor"]}
    )
])

跨模态搜索:

# 图像→图像
query_feat = extract_image_feature(query_img)
results = client.search("media", query_vector=query_feat, limit=10)

# 文本→图像(需 CLIP 等多模态模型)

9.5 实时个性化广告投放

步骤名称操作细节代码示例注意事项
广告素材向量化对广告标题、描述、图片联合嵌入ad_vector = clip_model.encode_text(ad_text) + clip_model.encode_image(ad_image)多特征融合(加权平均或拼接);向量代表广告语义
用户兴趣建模实时聚合用户点击/浏览行为生成兴趣向量user_interest = weighted_avg([ad_vectors for ad in recent_clicks])可滑动窗口更新(最近 1 小时行为);新用户用人群画像向量初始化
实时匹配QPS 高的在线服务调用 Qdrant 搜索见下方代码块必须过滤预算耗尽或暂停的广告;设置 score_threshold 避免低质曝光
频次控制在 Payload 中记录曝光次数(应用层实现)见下方代码块Qdrant 不提供频控原语;可结合 Redis 实现全局频控

实时匹配示例:

ads = client.search(
    "ads",
    query_vector=user_interest,
    query_filter=Filter(must=[
        FieldCondition(key="campaign_status", match=MatchValue("active")),
        FieldCondition(key="budget_left", range=Range(gte=0.01))
    ]),
    limit=10,
    score_threshold=0.6
)

频次控制示例:

if ad_id in user_exposure and user_exposure[ad_id] > 3:
    skip ad

性能要求:

  • P99 延迟 < 50ms
  • 单实例支持 > 1000 QPS
  • 建议:on_disk_vectors=false + SSD + HNSW m=32