|
### 朴素方法的问题
想象 1000 万个文档、每个 1536 维向量:
```python
# ❌ 这样会很慢!
for doc_vector in all_documents: # 1000 万次循环
similarity = cos_sim(query_vector, doc_vector)
```
- ⏰ **性能问题**:线性暴力搜索 O(N×D),无法接受
- 💾 **内存限制**:向量无法全部加载到内存
- 🔧 **缺乏工程能力**:没有索引、元数据过滤、增删改、分布式
### 向量数据库的救赎
向量数据库使用 **近似最近邻(ANN, Approximate Nearest Neighbor)** 算法,用极小的召回损失换取数量级的加速:
- ⚡ **亚秒级检索**:毫秒级响应
- 📈 **可扩展**:支持亿级向量
- 🎯 **高精度**:近似但足够准
- 💪 **生产就绪**:增删改、过滤、监控、分布式
### 与传统数据库/搜索引擎的关系
| 系统 | 检索方式 | 擅长 | 短板 |
|---|---|---|---|
| 关系数据库 | SQL 精确匹配(WHERE) | 结构化条件、事务 | 不懂语义 |
| 搜索引擎(ES) | 倒排索引 + BM25 | 关键词字面匹配 | 词汇鸿沟("插嘴"搜不到"打断") |
| 向量数据库 | ANN 相似度检索 | 语义理解、同义/跨语言 | 精确术语可能偏差 |
> 生产 RAG 的常见形态是**混合检索**:关键词路守精确术语,向量路救词汇鸿沟。
|
|
### 核心概念(以 Qdrant 为例,各家术语大同小异)
| 核心概念 | 类比关系库 | 说明 |
|---|---|---|
| Collection(集合) | 表 table | 一组同维度向量的容器,创建时声明维度与距离度量 |
| Point(点) | 行 row | 一条记录 = id + vector + payload |
| Vector(向量) | 向量字段列 | 定长浮点数组(如 1536 维),由 Embedding 模型生成 |
| Payload(负载) | 其他普通列 | 任意 JSON 元数据,支持过滤、随检索结果带回 |
### 向量库在 RAG 中的位置
```text
━━━━ 离线:建立索引 ━━━━
原始文档 ──▶ 切分 chunk ──▶ Embedding 向量化 ──▶ 向量数据库(向量 + 原文/元数据成对存储)
━━━━ 在线:检索生成 ━━━━
用户提问 ──▶ 问题向量化 ──▶ ANN 相似度检索 Top-K ──▶ 组装提示词 ──▶ LLM 生成回答
```
### 标准分工:向量库 + Embedding 服务
向量数据库只负责**存与查**,**向量化由外部 Embedding 服务完成再传入**:
```python
vectors = get_embeddings(texts) # ① 外部 embedding API 向量化
points = [PointStruct(id=i, vector=v, payload={"text": t})
for i, (t, v) in enumerate(zip(texts, vectors))]
qdrant.upsert(collection_name="kb", points=points) # ② 向量库只管入库
qvec = get_embeddings([query])[0] # ③ 查询同样先向量化
res = qdrant.query_points(collection_name="kb", query=qvec, limit=3)
```
> ⚠️ 铁律:**建库与查询必须用同一个 Embedding 模型**——不同模型的向量空间不同,混用则相似度计算完全失效。
|
|
### 核心 ANN 索引算法
| 索引 | 原理 | 特点 |
|------|------|------|
| **Flat 暴力** | 全量逐一比对 | 100% 准确但慢,适合小库或做基准 |
| **IVF 倒排文件** | 向量空间聚类分桶(nlist 个桶),查询只搜最近的几个桶(nprobe) | 大幅剪枝加速,适合大规模;inverted 即"倒排"之意 |
| **PQ 乘积量化** | 向量切分段分别量化压缩存储 | 省 10 倍以上内存,有精度损失 |
| **HNSW 分层小世界图** | 多层"跳表式"近邻图,从顶层粗定位逐层下沉贪心搜索 | 查询快、召回高;内存占用大、建图慢 |
### 用召回损失换速度
ANN 的本质交易:**放弃"百分之百精确",换取数量级加速**。
```text
暴力 Flat: 100% 召回, O(N) 十万级可用
ANN(HNSW): ~99% 召回, 近似对数复杂度 千万~亿级毫秒级响应
```
> IVF 适合超大规模、PQ 适合内存受限、HNSW 适合高召回低延迟——很多库支持组合(如 IVF+PQ)。Qdrant 默认使用 **HNSW**(详见本章「HNSW向量索引」节)。
|
|
### 三种常用相似度
| 度量 | 公式 | 取值 | 特点 |
|------|------|------|------|
| **余弦相似度** | cos(θ) = A·B / (\|A\|×\|B\|) | -1 ~ 1,越大越相似 | 只看方向不看长度,最常用 |
| **欧氏距离** | d = √Σ(ai-bi)² | ≥ 0,越小越相似 | 对向量长度敏感 |
| **点积** | A·B | 无界 | 计算最快;向量归一化后等价余弦 |
```python
import numpy as np
from numpy import dot
from numpy.linalg import norm
def cos_sim(a, b):
"""余弦相似度:值越大越相似,范围 -1 到 1"""
return dot(a, b) / (norm(a) * norm(b))
def euclidean_distance(a, b):
"""欧氏距离:值越小越相似"""
return norm(np.asarray(a) - np.asarray(b))
```
### Qdrant 中的距离配置
创建 Collection 时声明(建库与查询必须一致):
```python
qdrant.create_collection(
collection_name="demo",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
# distance 可选:COSINE / EUCLID / DOT
)
```
> Qdrant 检索返回的 score 即相似度(COSINE 时 0~1,越大越相似)。
|
|
### 主流向量库:定位·架构·场景
| 数据库 | 定位与架构 | 适用场景与规模 |
|---|---|---|
| **Milvus** | 专业开源分布式向量库,云原生架构,多索引类型+GPU 加速 | **大规模**(千万级至十亿级以上)、高并发、企业级生产环境 |
| **Qdrant** | Rust 编写的高性能向量库,向量检索与结构化数据过滤(Payload)为核心设计 | 中等规模(百万~千万级),尤其适合**复杂标量过滤**与**混合检索**的 RAG/实时检索场景 |
| **Weaviate** | 内置多种模块(如文本向量化),支持 GraphQL,擅长复杂语义与知识图谱类查询 | 需要**复杂查询**、知识图谱或**混合搜索**的企业知识应用(千万级) |
| **Pinecone** | 全托管**云原生 Serverless**,免运维开箱即用 | 快速云上落地、不想维护基础设施的团队 |
| **pgvector** | **PostgreSQL 扩展插件**,为关系库增加向量存储检索 | 团队已深度用 PG、数据量中等(百万级以内)、**不想引入新组件** |
| **Chroma** | 嵌入式向量库,pip 即用 | 学习、原型、小规模应用(百万级) |
| **FAISS** | Meta 出品的相似度搜索**算法库**(非数据库) | 本地实验、高性能研究、自建系统底座 |
| **Elasticsearch** | 倒排+向量双索引的搜索引擎 | 关键词+语义混合检索一站式 |
### 国内使用情况
- **Milvus 是国内企业用得最多的**,可以说是开源向量数据库的"事实标准"
- **pgvector** 凭借与 PostgreSQL 生态的天然融合,在已有 PG 体系的团队中非常受欢迎
### 一句话选型
```text
学习/原型 ──────────────▶ Chroma
本地实验/极致性能 ──────▶ FAISS
生产·百万~千万级+复杂过滤 ▶ Qdrant
生产·亿级 ──────────────▶ Milvus
已有PG·要join ──────────▶ pgvector
关键词+语义混合 ────────▶ Elasticsearch / Weaviate
不想运维 ───────────────▶ Pinecone / Zilliz Cloud
```
|
|
### FAISS vs Chroma:算法引擎 vs 产品
两者的对比本质是**"高性能算法引擎"与"开箱即用的产品"**之争:
| 特性维度 | FAISS | Chroma |
|---|---|---|
| 本质定位 | 高效的相似度搜索**算法库** | 完整、开发者友好的**向量数据库** |
| 核心功能 | 仅提供向量检索的算法实现(索引、搜索) | 完整数据管理:存储、元数据过滤、持久化、API |
| 使用体验 | 极客向:自己管数据、做持久化、维护映射 | 新手友好:pip install 后几行代码上手 |
| 性能表现 | **性能王者**:查询延迟低至 0.19 毫秒,近 2000 QPS | 够用:10 万级向量查询约 20 毫秒 |
| 扩展与规模 | 单机库,无内置分布式 | 百万级后扩展困难,建议迁移专业库 |
> 比喻:**FAISS 是 F1 赛车的发动机,Chroma 是立刻能开上路的家用轿车**。
> (趣闻:Qdrant/Milvus 等不少产品底层索引引擎正是基于 FAISS/HNSW 思想构建)
### 安装复杂度排序(用交通工具比喻)
| 方案 | 复杂度 | 比喻 | 安装成本 |
|---|---|---|---|
| pgvector | ⭐ 极简 | 车库里现成的家用轿车 | `CREATE EXTENSION vector;` 一条 SQL,零新组件 |
| Qdrant | ⭐⭐ 简单 | 高性能跑车 | 单二进制或 Docker,一条命令跑起来 |
| Weaviate | ⭐⭐⭐ 中等 | 功能齐全的房车 | Docker 便捷,但概念多、有学习成本 |
| Milvus | ⭐⭐⭐⭐⭐ 复杂 | 运载巨大的地铁系统 | 存算分离:需连动 etcd + MinIO + Milvus 三组件 |
### 为什么复杂度差这么多?
- **pgvector**:不是独立数据库,是 PG 扩展——不维护任何新服务进程
- **Qdrant / Weaviate**:独立数据库服务,但单二进制/All-in-One 镜像一条命令全跑起来
- **Milvus**:天生为大规模集群设计的云原生分布式系统,"单机版"也是三容器协同
|
|
### 选型三维度(本质取舍:数据规模 × 运维成本 × 功能特性)
1. **看数据量和并发**:千万~亿级首选 Milvus;百万~千万级 Qdrant 性价比高;百万级以下 pgvector/Chroma 足以应对
2. **看现有技术栈**:PG 重度用户选 pgvector 的"零新增组件"优势无可替代;已有 ES/Mongo 优先复用其向量能力
3. **看运维能力**:团队小不想管服务器选 Pinecone(托管);有成熟运维团队选 Milvus/Qdrant 开源自托管
### 八个视角速查
| 视角 | 关键问题 | 倾向选择 |
|---|---|---|
| 数据规模 | 百万级?千万级?亿级? | 百万以下 Chroma/pgvector;百万~千万 Qdrant/Weaviate;千万~亿+ Milvus |
| 团队运维 | 有人运维数据库吗? | 无→Pinecone/Zilliz 托管;有→自托管开源方案 |
| 现有技术栈 | 已有 PG/ES/Mongo? | 优先复用:pgvector / ES dense_vector |
| 数据敏感度 | 能出内网吗? | 敏感→自托管开源(Milvus/Qdrant) |
| 查询复杂度 | 要混合检索/复杂过滤? | 混合→Weaviate/ES;复杂过滤→Qdrant |
| 成本模型 | 订阅费 vs 自运维人力 | 量小 SaaS 划算;量大长期自托管更省 |
| 锁定风险 | 换库/迁库成本? | SaaS 锁定最深;开源+标准 API 迁移成本最低 |
| 性能指标 | 延迟/QPS 硬要求? | 极致性能→FAISS;工程平衡→Qdrant |
### 一页总结
1. **没有最好的向量库,只有最合适的**:选型本质是在数据规模、运维成本、功能特性间取舍
2. **规模定档**:百万以下轻量档(Chroma/pgvector),百万~千万中档(Qdrant/Weaviate),千万以上重型档(Milvus)
3. **生态优先**:已有 PG/ES/Mongo 就先复用,别轻易引入异构系统
4. **可演进原则**:先用轻量方案跑通业务,量级逼近上限再升级;接口层做抽象(检索函数可注入),换库只换实现
|
|
### 先看相同点:同门流派
Qdrant 与 Chroma 都是**面向 RAG/语义检索的开源向量数据库**,核心概念与能力框架高度同构:
| 维度 | 共同点 |
|------|--------|
| 开源协议 | 都是 **Apache License 2.0**——免费可商用、可修改、自托管免费 |
| 数据模型 | 都是 **Collection →(id + vector + 元数据)**;原文/元数据随检索结果带回,RAG 免二次查库 |
| 核心 ANN 索引 | 都以 **HNSW** 为核心索引(Chroma 底层用 hnswlib;Qdrant 自研可过滤 HNSW) |
| 距离度量 | 都支持 **cosine / L2(欧氏)/ 内积(dot)**,建库时声明 |
| 元数据过滤 | 都支持"向量检索 + 结构化条件"组合查询 |
| CRUD | 都支持增删改查与批量写入(add/upsert、update、delete) |
| 客户端生态 | 都有 Python 一等客户端、LangChain / LlamaIndex 官方集成 |
| 部署 | 都可 Docker 部署;起步都**零外部依赖**(Chroma 是 pip,Qdrant 是单二进制) |
### 一句话概括关系
> 两者是"**同门流派、不同段位**"——同一套向量库概念体系,Chroma 把"**轻与易**"做到极致,Qdrant 把"**深与扛造**"做到极致。差异不在"有没有向量检索",而在**过滤深度、规模上限、生产工程能力**。
|
|
### 架构与形态:嵌入式库 vs 独立服务
| 维度 | Chroma | Qdrant |
|---|---|---|
| 产品形态 | **嵌入式优先**:进程内库(import 即用)+ 本地文件持久化;可选 client/server 模式(`chroma run`) | **独立服务优先**:常驻进程(单二进制/Docker),REST/gRPC 访问;也提供本地内存模式(`QdrantClient(":memory:")`)供单测 |
| 实现语言 | Python 生态为主(新版本核心执行层正逐步用 Rust 重写) | **纯 Rust**(服务端与官方客户端) |
| 接口协议 | server 模式走 REST(FastAPI),无 gRPC | **REST(6333)+ gRPC(6334)双协议**(见「REST与gRPC双协议」节) |
| 存储引擎 | SQLite + 本地目录(`PersistentClient(path=...)` 一行持久化) | 自带存储(WAL + 段式存储),数据目录/快照目录独立配置 |
| 部署复杂度 | `pip install chromadb` 即用,零部署 | 下载单二进制或 `docker run`,一条命令起服务 |
| 默认向量化 | **内置默认 embedding**(ONNX MiniLM):不传向量自动编码,演示零配置 | **存查分工**:向量化由外部 Embedding 服务完成(可选集成 FastEmbed 本地模型达到同样效果) |
| 比喻 | 可以立刻开上路的家用轿车 | 高性能跑车(沿用「主流向量库与选型」节的比喻) |
### 形态差异的连锁反应
- **Chroma 进程内**:库与应用同生共死,无网络往返(小库检索极快),但也无法多应用共享、无独立扩缩容
- **Qdrant 常驻服务**:应用与存储解耦,多客户端共享一份向量数据、可独立升级与横向扩展,代价是多一层网络与运维
|
|
### 功能特性对比(差异的主战场)
| 能力 | Chroma | Qdrant |
|---|---|---|
| 元数据过滤 | `where` 简单谓词:`$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin` + `$and/$or`;**无字段索引** | **完整 Filter DSL**:match any/all、range、geo、nested、is_null/is_empty、全文 match text、values_count + **payload 索引** + Filterable HNSW 下推(见「Payload元数据过滤」节) |
| 文档内容条件 | `where_document: $contains`(子串包含) | 全文索引(分词匹配)+ 稀疏向量 BM25 |
| 混合检索 | **无原生支持**(需自己拉两路结果手工融合) | **原生**:稀疏向量 + 稠密向量,服务端 RRF/DBF 融合 |
| 内存优化 | 无量化手段 | **scalar / PQ / binary 量化**(见「HNSW向量索引」节) |
| 多向量/命名向量 | 不支持(每 collection 单一向量) | 支持 named vectors(多模型/多粒度)、multivectors |
| 分布式/高可用 | 单机(无集群能力) | **集群分片 + 副本**(Raft 一致性) |
| 备份恢复 | 拷贝持久化目录 | **快照 API**:集合级/全量、上传恢复(见「Qdrant简介与安装」节) |
| 安全认证 | server 模式默认无认证(新版本提供基础认证选项) | **API key / TLS**,支持安全暴露 |
| 可视化 | 无内置管理界面 | 内置 **Dashboard**(`localhost:6333/dashboard`) |
| 客户端语言 | Python / JS 为主 | Python / JS / Go / Rust / Java / .NET |
### 过滤写法直观对比
```python
# Chroma:where 字典(扁平谓词,无字段索引,不支持 geo/nested)
res = col.query(
query_embeddings=[qvec],
n_results=5,
where={"$and": [
{"category": {"$eq": "外设"}},
{"price": {"$gte": 100, "$lte": 1000}},
]},
)
# Qdrant:Filter DSL(payload 索引 + 可过滤 HNSW 图遍历下推)
res = qdrant.query_points(
collection_name="demo",
query=qvec,
limit=5,
query_filter=Filter(must=[
FieldCondition(key="category", match=MatchValue(value="外设")),
FieldCondition(key="price", range=Range(gte=100, lte=1000)),
]),
)
```
> 过滤条件简单(单字段等值)时两者体验接近;一旦涉及**高频过滤、范围+多值+嵌套组合、租户隔离**,Qdrant 的 payload 索引 + 下推优势就拉开了。
|
|
### 性能与规模
| 维度 | Chroma | Qdrant |
|---|---|---|
| 设计规模 | **百万级以内**(单机),超出后性能下降明显 | 百万~千万级甜点区;**数亿向量**有实际案例 |
| 延迟参考(实测) | 10 万级向量查询约 **20 毫秒** | 数亿向量典型查询 **100 毫秒以内**(HNSW 参数 + 量化调优) |
| 并发扩展 | 进程内/单 server,无分布式 | 集群分片 + 副本,高并发可横向扩展 |
| 内存控制 | 无量化手段,大规模内存吃紧 | scalar/PQ/binary 量化,内存可压到原来的 1/4 甚至更低 |
| 传输效率 | REST/JSON(大向量文本编解码开销) | gRPC/Protobuf 二进制(批量写入吞吐显著更高) |
### 为什么 Qdrant 上限更高
1. **编译型语言**:Rust 无 GC 停顿、无 Python GIL,高并发下延迟稳定
2. **gRPC 二进制协议**:百万级 upsert 时吞吐优势明显
3. **量化 + Filterable HNSW**:规模上去后仍有内存与过滤性能手段
### 公平说明(Chroma 的性能甜点)
- **万级以下小库**:两者都毫秒级响应,差异无感知
- **嵌入式模式零网络往返**:Chroma 进程内检索没有网络开销,小数据量下并不慢
- Chroma 的性能取舍是**易用性优先**——牺牲规模上限换零门槛
> 差距在**数据量、并发数、过滤复杂度**三者上来之后才显现;判断标准很朴素:数据会不会过百万?过滤会不会变复杂?会不会多服务共享向量库?任一为"会",就到 Qdrant 的甜点区了。
|
|
### 同一任务两种写法(建库 → 写入 → 过滤检索)
**Chroma 版(含自动向量化)**:
```python
import chromadb
client = chromadb.PersistentClient(path="./chroma_data") # 持久化只需一个路径参数
col = client.get_or_create_collection("docs",
metadata={"hnsw:space": "cosine"})
# 不传 embeddings 时,Chroma 自动用内置默认模型(ONNX MiniLM)向量化
col.add(
documents=["Llama 2 有 7B、13B、70B 三个版本", "今天天气不错"],
ids=["doc_1", "doc_2"],
)
res = col.query(query_texts=["Llama 2 有多少参数?"], # 查询同样自动向量化
n_results=2,
where={"source": "llama2_paper"})
print(res["documents"])
```
**Qdrant 版(显式维度声明 + 外部向量化 + payload)**:
```python
from qdrant_client import QdrantClient
from qdrant_client.models import (Distance, VectorParams, PointStruct,
Filter, FieldCondition, MatchValue)
qdrant = QdrantClient(url="http://localhost:6333")
if not qdrant.collection_exists("docs"):
qdrant.create_collection("docs",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE))
# 向量化由外部 Embedding 服务完成(维度必须与建库一致)
texts = ["Llama 2 有 7B、13B、70B 三个版本", "今天天气不错"]
vectors = get_embeddings(texts)
points = [PointStruct(id=i, vector=v, payload={"text": t, "source": "llama2_paper"})
for i, (t, v) in enumerate(zip(texts, vectors))]
qdrant.upsert(collection_name="docs", points=points)
qvec = get_embeddings(["Llama 2 有多少参数?"])[0]
res = qdrant.query_points(collection_name="docs", query=qvec, limit=2,
query_filter=Filter(must=[
FieldCondition(key="source", match=MatchValue(value="llama2_paper")),
]))
for pt in res.points:
print(pt.score, pt.payload["text"])
```
### 写法差异的观察点
| 观察点 | Chroma | Qdrant |
|---|---|---|
| 代码行数 | 更少,3 个调用跑通 | 略多(声明维度、构造 PointStruct) |
| 向量化 | 内置默认模型,可不管 | 显式外部传入(可选 FastEmbed) |
| 类型安全 | 字典参数 | **pydantic 模型**(VectorParams/PointStruct/Filter),IDE 补全与静态检查友好 |
| 原文存储 | documents 字段原生存储 | payload 自由结构 |
| 换模型成本 | 换 embedding 函数即可 | 同样外部化,且建库/查询同模型的约束更显式 |
### LangChain 里只差一个类名
```python
from langchain_chroma import Chroma
db = Chroma.from_documents(chunks, embeddings, persist_directory="./chroma_db")
from langchain_qdrant import QdrantVectorStore
db = QdrantVectorStore.from_documents(
chunks, embeddings,
url="http://localhost:6333", collection_name="docs",
)
retriever = db.as_retriever() # 之后统一 retriever.invoke(...),两库无差别
```
|
|
### Chroma 的优势清单
- ✅ **三行代码跑通**:`pip install` 后 PersistentClient + add + query,无需起服务
- ✅ **内置默认 embedding**:不指定模型也能自动向量化,演示/原型最快路径
- ✅ **运维为零**:持久化就是一个目录,备份=拷目录,无快照/集群概念要学
- ✅ **进程内零网络**:嵌入式模式检索无网络往返,万级小库延迟极低
- ✅ **Jupyter 友好**:notebook 里 import 即用,课程/实验首选
### Qdrant 的优势清单
- ✅ **性能与规模**:Rust + gRPC,数亿向量案例,分布式可横向扩展
- ✅ **过滤天花板**:完整 Filter DSL + payload 索引 + Filterable HNSW 下推,复杂过滤低延迟
- ✅ **原生混合检索**:稀疏向量 + BM25,服务端 RRF/DBF 融合,不用自己拼多路
- ✅ **内存工程**:scalar/PQ/binary 量化,大规模下内存可控
- ✅ **生产运维体系**:快照备份/恢复、API key/TLS、集群分片副本、内置 Dashboard
- ✅ **类型安全客户端**:pydantic 模型,静态检查与重构友好
### 一句话定位
> **Chroma:把"能用"做到极致**——原型与轻量应用的零门槛之选。
> **Qdrant:把"好用且扛造"做到极致**——生产 RAG 的工程化之选。
### 决策速查
```text
课程作业 / 个人笔记 / Jupyter 实验 / 十万级以内 ────▶ Chroma
生产 RAG / 复杂过滤 / 混合检索 / 高并发 / 亿级 ────▶ Qdrant
已用 Chroma 跑通,需求升级了 ────────────────────▶ 检索函数注入隔离,换 Qdrant 只换实现
```
### 迁移建议(从 Chroma 到 Qdrant)
1. **裸写代码**:用 RAG_Bot 检索函数注入模式——search_func 参数化,bm25/chroma/qdrant 即插即换,提示词与生成层零改动
2. **框架代码**:LangChain VectorStore 抽象,`from_documents` + `as_retriever` 两家写法几乎一致,换库只改 import 与连接参数
3. **数据搬迁**:两边都是「id + 向量 + 元数据」模型——Chroma `col.get()` 导出 → 构造 PointStruct → Qdrant `upsert` 导入即可(注意 id 规则:Qdrant 要 int/uuid,业务 id 放 payload)
|
|
### Qdrant 是什么
**Qdrant**(读作 quadrant)是 **Rust 编写的开源向量数据库**:
| 项 | 说明 |
|---|---|
| 开源协议 | **Apache License 2.0**——宽松协议,免费可商用、可修改、可再分发(自托管永远免费;Qdrant Cloud 托管服务才付费) |
| 实现语言 | Rust——无 GC 停顿、内存安全,单二进制 **32MB、零依赖** |
| 接口协议 | **REST(6333)/ gRPC(6334)双协议**(详见下节) |
| 向量索引 | **HNSW**(可过滤 HNSW,详见「HNSW向量索引」节) |
| 元数据 | **Payload**:任意 JSON + 丰富过滤谓词(详见「Payload元数据过滤」节) |
| 定位 | 百万~千万级甜点区:复杂标量过滤 + 混合检索的 RAG/实时检索场景 |
- 源码仓库:https://github.com/qdrant/qdrant
- 官方文档:https://qdrant.tech/documentation/
### 核心竞争力一句话
> 普通向量库:只能"找最相似的 N 条"。
> Qdrant:能"在满足 `category=electronics AND price<500 AND in_stock=true AND 国贸5公里内` 的子集里,找和 query 最相似的 10 条"——而且这个过滤是在 HNSW 图索引里做的,不是事后筛。
### 与 ES 部署对比(两个本地服务的实测经验)
| 维度 | ES 7.17.23 | Qdrant 1.19.1 |
|---|---|---|
| 包形态 | tarball(需解压目录树) | **单二进制**(musl 静态链接) |
| 依赖 | 自带 JDK 17 | **零依赖** |
| 配置 | elasticsearch.yml | config.yaml |
| 守护坑 | `-d` 持 stdout 挂管道 | nohup 同款坑,都要三 fd 重定向 |
| 备份 | 快照(需注册 repo) | 快照 API(**零配置**,直接打) |
| 教学结论 | 检索服务化标杆 | 向量库服务化标杆,**轻得多** |
|
|
### 版本与下载(2026-09-05 实机)
| 项 | 值 |
|---|---|
| 版本 | **Qdrant v1.19.1**(最新稳定版) |
| 包 | `qdrant-x86_64-unknown-linux-musl.tar.gz`(musl 静态链接,无 glibc 依赖问题) |
| 大小 | 32MB(解压后单二进制) |
| 安装目录 | `/opt/app/qdrant`(bin/storage/snapshots/config/backups) |
| 端口 | REST **6333** / gRPC **6334** |
### 三种安装方式
```bash
# 方式一:预编译二进制(推荐,最快)
curl -LO https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-unknown-linux-musl.tar.gz
tar -xzf qdrant-x86_64-unknown-linux-musl.tar.gz
./qdrant # 直接运行(默认端口 6333)
# 方式二:Docker
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
# 方式三:源码编译(进阶,一般不需要)
# 需要 Rust 工具链 + clang + cmake + protobuf-compiler
# 实测 v1.19.1:912 个依赖、13 分 22 秒、产物 89.7MB
```
> macOS 下载 darwin 包、Windows 下载 zip 双击 `qdrant.exe`——个人开发/快速测试非常友好。
### 远程中转下载(GitHub 国内直连慢,提速 20 倍)
```bash
REMOTE="xt@144.34.185.72"; RPORT=26225
# ① 远程查询最新版本号(GitHub API 在远程可达)
ssh -p $RPORT $REMOTE "curl -s https://api.github.com/repos/qdrant/qdrant/releases/latest \
| python3 -c \"import json,sys; print(json.load(sys.stdin)['tag_name'])\""
# ② 远程下载 → ③ scp 回传 + 双端 md5 校验
ssh -p $RPORT $REMOTE "cd /tmp && curl -sL -o qdrant.tar.gz \
https://github.com/qdrant/qdrant/releases/download/v1.19.1/qdrant-x86_64-unknown-linux-musl.tar.gz \
&& md5sum qdrant.tar.gz"
scp -P $RPORT $REMOTE:/tmp/qdrant.tar.gz /tmp/qdrant.tar.gz
```
### 安装(解压即用)
```bash
mkdir -p /opt/app/qdrant/{bin,storage,snapshots,config,backups}
tar -xzf /tmp/qdrant.tar.gz -C /opt/app/qdrant/bin
chmod +x /opt/app/qdrant/bin/qdrant
/opt/app/qdrant/bin/qdrant --version # qdrant 1.19.1
```
| 目录 | 职责 |
|---|---|
| `bin/` | 二进制 |
| `storage/` | 数据目录(collection 数据、WAL、索引) |
| `snapshots/` | 快照输出目录 |
| `config/` | config.yaml |
| `backups/` | 备份归档(快照 + storage + config 的 tar.gz) |
|
|
### 配置:config.yaml(显式数据目录)
不指定配置时 Qdrant 用内置默认值(数据落在启动目录的 `./storage`)——**生产必须显式指定**:
```yaml
# /opt/app/qdrant/config/config.yaml
storage:
storage_path: /opt/app/qdrant/storage # 数据目录
snapshots_path: /opt/app/qdrant/snapshots # 快照目录
service:
host: 127.0.0.1 # loopback 绑定(安全默认:qdrant 默认无鉴权,裸绑 0.0.0.0 = 裸奔)
http_port: 6333 # REST
grpc_port: 6334 # gRPC
```
### 启动(守护进程必须三 fd 重定向)
```bash
nohup /opt/app/qdrant/bin/qdrant --config-path /opt/app/qdrant/config/config.yaml \
< /dev/null > /opt/app/qdrant/logs.out 2>&1 &
echo $! > /opt/app/qdrant/qdrant.pid
```
> ⚠️ 与 ES `-d` 守护进程同款坑:不重定向三个 fd 时,外层 `... | tail` 等管道命令会因子进程持有 stdout 而永久挂起。
### 健康检查与客户端连接
```bash
curl -s http://127.0.0.1:6333 # 返回 {"title":"qdrant - vector search engine","version":"1.19.1"}
curl -s http://127.0.0.1:6333/collections # 集合列表
```
Python 客户端(`pip install qdrant-client`):
```python
from qdrant_client import QdrantClient
qdrant = QdrantClient(url="http://127.0.0.1:6333")
print([c.name for c in qdrant.get_collections().collections])
```
> 版本匹配:qdrant-client 1.19.x ↔ server 1.19.x 实测兼容;检索 API 用 `query_points`(旧 `search` 已弃用)。
### 远程 Qdrant 的本地访问:SSH 端口转发(推荐)
`host: 127.0.0.1` 只允许服务器本机访问 → 本地不可直连。建 SSH 隧道把本地端口映射到远程 loopback,**远程零改动**:
```bash
ssh -f -N \
-L 6333:127.0.0.1:6333 \
-L 6334:127.0.0.1:6334 \
-p 26225 \
-o ServerAliveInterval=30 \
-o ExitOnForwardFailure=yes \
xt@远程服务器
```
- `-f` 认证后转入后台;`-N` 纯转发不执行远程命令;注意 **6333 与 6334 两个端口都要转发**
- 之后本地代码直接连 `http://localhost:6333`(REST)/ `localhost:6334`(gRPC)
- 需常驻自动重连可换 autossh
> 备选(不推荐):config.yaml 改 `host: 0.0.0.0` 可直连,但必须同时设 `api_key` + 防火墙/安全组白名单。
|
|
### 数据目录结构
```text
/opt/app/qdrant/
├── storage/ # 数据主目录(collection 数据、WAL、HNSW 索引)
│ └── collections/<name>/
├── snapshots/ # 快照输出(集合级 + 全量)
│ ├── <collection>/xxx.snapshot
│ └── full-snapshot-xxx.snapshot
├── config/config.yaml
└── backups/ # 备份归档(tar.gz)
```
### 备份的正确姿势:先快照、再归档
直接 tar `storage/` 有风险——备份过程中服务可能正在写入。**快照是 Qdrant 提供的一致性保证机制(零配置,直接打)**:
```bash
# ① 集合级快照(单集合)
curl -s -X POST http://127.0.0.1:6333/collections/geeker_kb_vec/snapshots
# ② 全量快照(所有集合 + 配置)
curl -s -X POST http://127.0.0.1:6333/snapshots
# ③ 归档:快照目录 + 存储目录 + 配置
tar -czf /opt/app/qdrant/backups/qdrant_backup_all_$(date +%Y%m%d_%H%M%S).tar.gz \
-C /opt/app/qdrant snapshots storage config
```
### 恢复:上传快照重建集合
```bash
curl -s -X POST http://127.0.0.1:6333/collections/geeker_kb_vec/snapshots/upload \
-H 'Content-Type: multipart/form-data' \
-F "snapshot=@/opt/app/qdrant/snapshots/geeker_kb_vec/xxx.snapshot"
```
### 实测链路
```text
建集合 backup_test + 写入 1 个点
→ snapshot(产出 .snapshot 文件)
→ backup(归档 384K tar.gz)
→ 删除集合
→ restore(上传快照)
→ points_count: 1 ✅ 数据完整恢复
```
### 备份策略建议
| 层级 | 手段 | 频率 |
|---|---|---|
| 日常增量 | 集合快照(快照是原子的,开销小) | 每日/每次入库后 |
| 周期归档 | 快照 + storage + config 打包到 backups/ | 每周 |
| 异地容灾 | 备份归档 rsync 到远程 | 每周/每月 |
|
|
### 一键运维脚本(qdrant_run.sh)
```bash
bash qdrant_run.sh start|stop|status|info # 生命周期
bash qdrant_run.sh snapshot [collection] # 快照(不传名=全量)
bash qdrant_run.sh backup [collection] # 备份(快照+归档,带时间戳)
bash qdrant_run.sh restore <快照文件> <集合名> # 恢复
```
实测输出:
```text
[qdrant] 后台启动 Qdrant(config: /opt/app/qdrant/config/config.yaml)...
[qdrant] 健康检查通过:
version: 1.19.1
pid: 271904
collections: []
```
### systemd 服务化(生产推荐)
直接运行 `./qdrant` 关终端即停——Linux 配 **systemd** 服务、macOS 配 launchd(官方文档有现成模板,改路径即用)。
### 排错速查
| 症状 | 根因 | 处置 |
|---|---|---|
| `Address already in use` | 旧实例未停 | `kill $(cat qdrant.pid)` 或 stop 子命令 |
| 启动后 curl 拒绝连接 | 配置路径错误/端口占用 | `tail -50 /opt/app/qdrant/logs.out` |
| `search` 方法报错 | 客户端新版弃用 | 改用 `query_points` |
| 备份文件损坏 | 备份时正在写入 | 务必**先快照再归档** |
| PointStruct id 报错 | id 必须是 int 或 uuid | 别用字符串 id,把业务 id 放 payload |
|
|
### REST 是什么
REST(Representational State Transfer,表述性状态转移)是一种 **基于 HTTP 的接口风格**:
- **资源即 URL**:每个集合/点都是资源路径,如 `/collections/shop_demo/points`
- **方法即动作**:GET 查询、PUT/POST 创建与写入、DELETE 删除
- **报文即 JSON**:请求/响应都是人类可读的 JSON 文本
### Qdrant 的 REST 端口:6333
`config.yaml` 中 `service.http_port: 6333`,REST API 覆盖全部功能(集合/点/检索/快照/集群管理均可 HTTP 调用):
```bash
# 服务信息与健康检查
curl -s http://127.0.0.1:6333
# {"title":"qdrant - vector search engine","version":"1.19.1"}
# 集合列表
curl -s http://127.0.0.1:6333/collections
# 创建集合
curl -s -X PUT http://127.0.0.1:6333/collections/shop_demo \
-H 'Content-Type: application/json' \
-d '{
"vectors": {"size": 8, "distance": "Cosine"}
}'
# 写入点(upsert)
curl -s -X PUT 'http://127.0.0.1:6333/collections/shop_demo/points?wait=true' \
-H 'Content-Type: application/json' \
-d '{
"points": [
{
"id": 1,
"vector": [0.1, 0.2, 0.3, 0.0, 0.1, 0.2, 0.3, 0.0],
"payload": {"name": "机械键盘 K87", "category": "外设"}
}
]
}'
# 向量检索 + 元数据过滤
curl -s -X POST http://127.0.0.1:6333/collections/shop_demo/points/query \
-H 'Content-Type: application/json' \
-d '{
"query": [1.0, 0.0, 1.0, 0.1, 0.1, 0.1, 0.1, 0.1],
"limit": 3,
"filter": {
"must": [
{
"key": "category",
"match": {"value": "外设"}
}
]
}
}'
```
### REST 的生态优势
- **零门槛调试**:curl / Postman / 浏览器直接调,返回 JSON 直接可读
- **语言无关**:任何能发 HTTP 的语言/工具都能用(Python requests、Go、Shell 脚本……)
- **自带 Web UI**:浏览器打开 `http://localhost:6333/dashboard`,可视化浏览集合、点、配置并测试检索
```python
# Python 也能直接走 REST(notebook 快照备份即用 requests)
import requests
r = requests.post("http://127.0.0.1:6333/collections/shop_demo/snapshots")
print(r.json()["result"]) # 快照名、大小、创建时间
```
|
|
### gRPC 是什么
gRPC 是 Google 开源的**高性能 RPC(远程过程调用)框架**,与 REST 是两种风格的接口协议:
| 特性 | 说明 |
|---|---|
| 传输层 | **HTTP/2**:二进制分帧、多路复用(一条连接并发多个请求)、头部压缩 |
| 报文格式 | **Protobuf**:二进制序列化,比 JSON 体积小、编解码快 |
| 接口契约 | **.proto 文件**(IDL 接口描述语言):强类型定义服务与方法,客户端 stub 由工具生成 |
| 流式 | 支持一元、服务端流、客户端流、双向流四种调用模式 |
### Qdrant 的 gRPC 端口:6334
`config.yaml` 中 `service.grpc_port: 6334`。**qdrant-client 对 gRPC 做了透明封装**——同一套 Python API,加一个参数即切换:
```python
from qdrant_client import QdrantClient
# 默认走 REST(6333)
rest_client = QdrantClient(url="http://127.0.0.1:6333")
# prefer_grpc=True:同一套 API 自动走 gRPC
grpc_client = QdrantClient(url="http://127.0.0.1:6333", prefer_grpc=True)
# 代码零改动:upsert / query_points / scroll 全部同样调用
res = grpc_client.query_points(collection_name="shop_demo", query=qvec, limit=3)
```
> 原理:`prefer_grpc=True` 时客户端先用 REST 获取服务信息,随后在 gRPC 端口(默认 grpc_port=6334,即 6333+1)建立长连接通道;也可显式传 `grpc_port` 参数。
### gRPC 为什么快
1. **Protobuf 二进制**:向量是浮点数组,JSON 文本编码("0.123456")比二进制大数倍且要解析;Protobuf 直接二进制打包
2. **HTTP/2 多路复用**:批量 upsert 百万点时,一条 TCP 连接并发跑多个流,免去 REST 每请求的连接开销
3. **长连接**:RPC 语义天然面向持久连接,减少握手成本
> 对"大批量写入 + 高 QPS 检索"的生产负载,gRPC 吞吐优势明显;小规模使用差异可忽略。
|
|
### 双协议对比
| 维度 | REST(6333) | gRPC(6334) |
|---|---|---|
| 报文 | HTTP/1.1 + JSON 文本 | HTTP/2 + Protobuf 二进制 |
| 可读性 | ✅ 人可直接读写的 JSON | ❌ 二进制,需 proto 定义与工具 |
| 调试 | curl/浏览器/Dashboard 直接调 | 需 grpcurl 等工具或代码 |
| 性能 | 够用;大向量时文本编解码开销大 | 高吞吐:二进制紧凑 + 多路复用 + 长连接 |
| 语言支持 | 任何 HTTP 语言 | 需各语言 gRPC stub(官方维护 Python/Go/JS/Rust/Java…) |
| 适用 | 脚本、调试、管理操作、Web 直连 | 大批量写入、高 QPS 检索的生产链路 |
### 怎么选(实践建议)
```text
调试 / 运维脚本 / 快照备份 ──▶ REST(curl 一行搞定)
Web 前端直连 ──▶ REST(浏览器只会 HTTP)
批量灌库(百万级 upsert) ──▶ gRPC(吞吐显著更高)
线上高频检索服务 ──▶ gRPC(延迟更稳)
Python 快速开发 ──▶ qdrant-client 默认 REST,加 prefer_grpc=True 即切换
```
### 双协议并存的工程价值
- **管理面与数据面分离**:监控脚本用 REST 拿状态,业务链路用 gRPC 跑流量
- **渐进优化**:开发期用 REST 快速迭代,上线前加一个 `prefer_grpc=True` 即完成性能升级,业务代码零改动
- 两端口独立配置(`http_port` / `grpc_port`),可按需只暴露其中一个(如仅内网开 gRPC)
|
|
### HNSW 是什么
HNSW(**H**ierarchical **N**avigable **S**mall **W**orld,分层可导航小世界图)是目前向量检索最主流的 **图索引算法**——Qdrant 的默认向量索引。
### 核心思想:分层近邻图 + 跳表式搜索
把每个向量看作图上一个节点,与最近的 M 个邻居连边;再把这些图**叠成多层**:顶层稀疏(长边,跨度大)、底层稠密(短边,精度高):
```text
Layer 2(最稀疏): A ────────────────── G ← 入口,长边跨大步
│ │
Layer 1(中等): A ──── C ──── E ──── G ← 逐层细化
│ │ │ │
Layer 0(全量点): A─B─C─D─E─F─G─H─I─... ← 所有向量都在这层
```
### 查询过程(贪心下沉)
1. 从顶层入口点出发,贪心走向"离查询向量更近"的邻居,直到该层找不到更近的
2. 下降一层,以上一层终点为起点继续贪心
3. 到达 Layer 0 后继续沿边扩展,收集 Top-K 候选(受 `ef` 候选队列控制)
```text
查询 q:顶层大步跳(快速逼近)→ 中层中步走 → 底层小步搜(精确逼近)
复杂度近似对数级:亿级库毫秒级响应
```
### 与跳表(Skip List)的类比
跳表用"多级索引"加速有序链表查找,HNSW 用"多层近邻图"加速最近邻查找——相当于**空间中的跳表**,这也是名字里 Hierarchical(分层)的由来。
### 为什么召回高
图上"邻居的邻居"也大概率相关——沿边扩展天然覆盖邻近区域;配合足够大的 `ef` 候选队列,召回率可达 99%+(可用 Flat 暴力检索做基准验证)。
|
|
### 三个关键参数
| 参数 | 作用 | 默认 | 调大 | 调小 |
|---|---|---|---|---|
| **M** | 每个节点的最大连接数 | 16 | 召回↑、内存↑、建图慢 | 内存↓、召回↓ |
| **ef_construct** | 建图时候选队列长度 | 100 | 图质量↑、召回↑、建索引更慢 | 建得快 |
| **ef**(查询时) | 查询时候选队列长度 | 按集合大小自适应 | 召回↑、延迟↑ | 延迟↓、召回↓ |
```python
from qdrant_client.models import HnswConfigDiff, SearchParams
# 建图参数:建集合时指定
qdrant.create_collection(
collection_name="demo",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
hnsw_config=HnswConfigDiff(m=32, ef_construct=200),
)
# 查询参数:每次检索即时指定(不动索引)
res = qdrant.query_points(collection_name="demo", query=qvec,
search_params=SearchParams(hnsw_ef=256),
limit=10)
```
### 调优套路
1. **先跑基准**:小库用 Flat 全量比对拿"标准答案",对比 HNSW 结果算召回率
2. **召回不够 → 加 ef**:查询参数即时生效、不动索引(首选手段)
3. **仍不够 → 加 M / ef_construct**:需重建索引,代价大但上限高
4. **延迟太高 → 降 ef**:或上量化
### 内存不够:量化(Quantization)
HNSW 的代价是**内存占用大**(图结构 + 原始向量都要驻内存)。Qdrant 支持量化压缩:
| 量化 | 原理 | 效果 |
|---|---|---|
| Scalar 标量量化 | float32 → int8 | 内存降约 75%,精度略降 |
| Product 乘积量化 | 分段聚类编码 | 内存降幅更大,精度损失更多 |
| Binary 二值量化 | 每维 1 bit | 内存降约 97%,需配合过采样 |
> 实践套路:**量化粗筛 + 原向量精排**——用量化向量快速圈候选,再用原始向量精算 Top-K,兼得内存与精度。有实际案例:Qdrant 索引**数亿个向量**,HNSW 参数(ef_construct、ef)+ 量化调优后,典型查询延迟仍在 **100 毫秒以内**。
|
|
### HNSW vs 其他 ANN 索引
| 索引 | 原理 | 查询速度 | 召回 | 内存 | 建索引 | 适用 |
|------|------|----------|------|------|--------|------|
| **Flat 暴力** | 全量比对 | 慢 O(N) | 100% | 高(只存向量) | 无 | 小库/基准 |
| **IVF 倒排** | 聚类分桶,只搜最近的几个桶 | 快 | 中 | 中 | 快(聚类) | 超大规模 |
| **PQ 乘积量化** | 分段量化压缩 | 中 | 中低 | **极低** | 中 | 内存受限 |
| **HNSW 分层图** | 多层近邻图贪心下沉 | **极快** | **高** | **高** | 慢 | 高召回低延迟 |
### HNSW 的优缺点
- ✅ 查询快:近似对数复杂度,亿级库毫秒级
- ✅ 召回高:图扩展天然覆盖邻域,99%+ 可达
- ✅ 支持增量插入:新点进图不用全量重建(IVF/PQ 常要重训)
- ❌ 内存占用大:图结构 + 向量全内存(靠量化缓解)
- ❌ 建图慢:ef_construct 大时,入库后索引构建耗时明显
- ❌ 删除麻烦:删点只打标记,图上"空洞"需定期重建优化
### Qdrant 中的 HNSW 行为
- **自动构建**:向量总量超过 `full_scan_threshold`(默认 10000,按 KB 计量)后**异步**建 HNSW 索引——写入立即可查(小库直接暴力扫更快),索引建好后自动加速
- **可过滤 HNSW(Filterable HNSW)**:把 Payload 过滤谓词下推到图遍历中,边走图边剪枝——过滤与检索同链路低延迟(详见下节「Payload元数据过滤」)
- 索引进度可通过 collection 信息查看(`status: green` 表示就绪)
|
|
### Payload 是什么
在 Qdrant 里,每条向量(Point)除了向量本身和 ID,还可以挂一个 **JSON 对象**,就叫 Payload:
```json
{
"id": 1,
"vector": [0.1, 0.2, 0.3],
"payload": {
"user_id": "u_123",
"category": "electronics",
"price": 299.00,
"in_stock": true,
"tags": ["sale", "new"],
"created_at": "2026-01-15T10:00:00Z",
"location": {"city": "Beijing", "district": "Haidian"}
}
}
```
- Payload 可以是**任意 JSON 结构**:字符串、数字、布尔、数组、嵌套对象、日期等
- **检索时随结果直接带回**:原文、类别、来源等放 payload,免去二次查库(RAG 的标准做法)
- 类比关系库:**Collection=表、Point=行、Vector=向量列、Payload=其余普通列**
### Payload 索引(让过滤变快)
为用于过滤的字段建 **payload index** 后,过滤走索引而非全扫描:
```python
from qdrant_client.models import PayloadSchemaType
qdrant.create_payload_index(
collection_name="shop_demo",
field_name="category",
field_schema=PayloadSchemaType.KEYWORD, # 关键词精确匹配
)
```
| 字段类型 | field_schema | 支持的谓词 |
|---|---|---|
| 关键词/枚举 | keyword | match / match any |
| 整数 | integer | match / range |
| 浮点 | float | range |
| 日期时间 | datetime | range(时间窗口) |
| 地理位置 | geo | geo_radius / geo_bounding_box |
| 全文 | text | match text |
| 布尔 | bool | match |
> 不建索引也能过滤(Qdrant 退化为全扫描式 payload 检查),但性能差很多——**高频过滤字段务必建索引**。
|
|
### 过滤谓词总览(Filter DSL)
Qdrant 的过滤条件叫 **Filter**,相当于 SQL 的 WHERE 子句。主要谓词类型:
### ① 精确匹配 match
```json
{"key": "category", "match": {"value": "electronics"}}
{"key": "in_stock", "match": {"value": true}}
```
### ② 多值匹配 match any(类似 SQL 的 IN)
```json
{"key": "tags", "match": {"any": ["sale", "new", "hot"]}}
```
注意:这是**数组字段包含任意一个**,不是整个数组相等;反向(全部包含)用 `"all": [...]`。
### ③ 范围过滤 range(数字与日期)
```json
{"key": "price", "range": {"gte": 100, "lt": 500}}
{"key": "created_at", "range": {"gte": "2026-01-01T00:00:00Z"}}
```
支持 `gt / gte / lt / lte`。
### ④ 数组元素个数 values_count
```json
{"key": "tags", "values_count": {"gte": 2}}
```
### ⑤ 地理过滤 geo
```json
{"key": "location", "geo_radius": {
"center": {"lat": 39.9, "lon": 116.4},
"radius": 5000
}}
```
radius 单位为米(圆形范围);另有 `geo_bounding_box` 矩形范围。
### ⑥ 存在性 is_null / is_empty
```json
{"is_null": {"key": "discount"}}
{"is_empty": {"key": "tags"}}
```
### ⑦ 全文匹配 match text
```json
{"key": "description", "match": {"text": "wireless headphone"}}
```
需建 text 索引,按词匹配而非子串。
### ⑧ 嵌套字段(点号路径)
```json
{"key": "location.city", "match": {"value": "Beijing"}}
```
### ⑨ 数组内对象条件 nested
当数组里是**对象**、且多个条件要作用在**同一个对象**上时用 nested(否则"红色"和"XL"可能分属两个不同 item 而误命中):
```json
{
"nested": {
"key": "items",
"filter": {
"must": [
{"key": "items.color", "match": {"value": "red"}},
{"key": "items.size", "match": {"value": "XL"}}
]
}
}
}
```
|
|
### 逻辑组合:must / should / must_not
| 子句 | 语义 | SQL 类比 |
|---|---|---|
| `must` | 全部满足 | AND |
| `should` | 至少满足一个 | OR |
| `must_not` | 都不满足 | NOT |
三者可同时出现,`WHERE a AND b AND (c OR d) AND NOT e` 这类组合可自然表达。
### Python SDK 写法(RAG 中最常用)
```python
from qdrant_client.models import Filter, FieldCondition, MatchValue, MatchAny, Range
res = qdrant.query_points(
collection_name="kb",
query=qvec, # ② 向量检索(语义相关)
query_filter=Filter( # ① 复杂过滤(先圈范围)
must=[
FieldCondition(key="category", match=MatchValue(value="售后")),
FieldCondition(key="price", range=Range(gte=100, lte=1000)),
],
should=[
FieldCondition(key="tags", match=MatchAny(any=["hot", "new"])),
],
must_not=[
FieldCondition(key="status", match=MatchValue(value="下架")),
],
),
limit=3,
)
```
### 同等条件的 REST 写法
```json
{
"filter": {
"must": [
{"key": "category", "match": {"value": "售后"}},
{"key": "price", "range": {"gte": 100, "lte": 1000}}
],
"must_not": [
{"key": "status", "match": {"value": "下架"}}
]
},
"limit": 3
}
```
### 常见过滤需求速查
| 需求 | 写法 |
|---|---|
| 等值 | `MatchValue(value=...)` |
| IN 多值 | `MatchAny(any=[...])` |
| 范围 | `Range(gte=..., lte=...)` |
| 时间窗口 | `Range(gte=起始时间戳)` |
| 多租户隔离 | must: `MatchValue(key="tenant_id")` |
|
|
### 传统做法的痛点(过滤 × 向量检索的结合难题)
| 做法 | 流程 | 问题 |
|---|---|---|
| **Post-filter(先检索后过滤)** | 先取 top-1000 相似向量,再过滤 | 过滤完可能只剩几条,**召回严重不足** |
| **Pre-filter(先过滤后检索)** | 先筛出满足条件的点,再暴力算相似度 | 条件宽松时退化为**全量扫描**,慢 |
### Qdrant 的做法:Filterable HNSW(可过滤 HNSW)
把过滤条件**下推(push down)到 HNSW 图遍历过程中**:
```text
HNSW 图遍历中,每个到达的候选点:
├─ 检查 payload 谓词 ──不满足──▶ 跳过该点(不入结果)
│ 但仍沿它的边继续扩展 ✅
└─ 满足 ──▶ 入候选队列,继续贪心下沉
```
关键设计:**不满足条件的点被跳过,但图上的边仍然通行**——不会因过滤把相邻候选区域整块丢掉。
三种条件强度下的表现:
- 过滤条件**严格**(命中点少)→ 只访问少量点,极快
- 过滤条件**宽松** → 自动退化为正常 HNSW 检索
- 无论哪种,**召回率都有保障**(对比 post-filter 最大的优势)
> 前提:用于过滤的 payload 字段要建 **payload index**——否则也能过滤,但性能差很多。
### 一句话理解
> 向量检索负责"**语义相似**"(像不像),Payload 过滤负责"**结构化条件**"(属不属于)——两者组合在同一次 `query_points` 调用里,过滤在 HNSW 图遍历中完成,**不是先检索再过滤,也不是先过滤再全量扫描**。
|
|
### 典型应用场景
| 场景 | 过滤方式 |
|------|----------|
| 多租户 SaaS | `must: user_id = "u_123"`,所有租户同集合、逻辑隔离 |
| 电商推荐 | 品类 match + 价格 range + 库存 range + 标签 any |
| 权限控制 | `must: acl_group in ["g1","g2"]`——检索结果只含用户有权的内容 |
| 时间窗口 | `range: created_at ≥ 最近30天`(RAG 里"只用新文档回答") |
| 地理围栏 | `geo_radius` 找附近的相似商品/门店 |
| 复杂嵌套 | 订单 items 里 color+size 同时满足(nested) |
### RAG 中的实际形态(NL2SQL 案例)
问"上个月华东地区销售额":必须**先限定** `业务线=销售 AND 时间=2026-08`,再在范围内向量检索"金额"字段——语义再准也不能越界搜到别的业务线:
```python
res = qdrant.query_points(
collection_name="nl2sql_kb",
query=question_vec,
query_filter=Filter(must=[
FieldCondition(key="business", match=MatchValue(value="销售")),
FieldCondition(key="month", match=MatchValue(value="2026-08")),
]),
limit=5,
)
```
### 过滤 vs 混合检索(两个易混概念)
- **混合检索**管"**怎么算相关**":关键词 BM25 + 向量多路召回,RRF 融合排序——作用在**打分层**
- **复杂过滤**管"**在谁里面搜**":WHERE 谓词先圈定候选集——作用在**候选集裁剪层**
- 口诀:**混合检索管"像不像",复杂过滤管"属不属于"**;生产查询常是"先过滤圈范围 → 混合检索算相关 → 重排求精"三层漏斗
|
|
### 0905_qdrant_usage.ipynb 总览
notebook:`/mnt/d/wks/jupyter/ai58/0905_qdrant_usage.ipynb`(py312 实测 0 错误),连接**本地源码编译**的 Qdrant 1.19.1;数据用手工构造的 8 维小向量,**不依赖任何外部 API**,开箱即跑:
```text
连接服务 → 集合管理 → 写入数据 → 向量检索 → 元数据过滤
→ 更新删除 → 滚动浏览 → 快照备份 → 衔接混合检索 RAG
```
### 概念类比(Qdrant vs 关系库)
| 核心概念 | 类比关系库 |
|---|---|
| Collection(集合) | 表 table |
| Point(点) | 行 row(id + vector + payload) |
| Vector(向量) | 向量字段列 |
| Payload(负载) | 其他普通列(元数据) |
### 连接服务
```python
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, PointStruct,
Filter, FieldCondition, MatchValue, Range,
)
qdrant = QdrantClient(url="http://127.0.0.1:6333")
print("连通成功,现有集合:", [c.name for c in qdrant.get_collections().collections])
```
实测输出:
```text
连通成功,现有集合: []
```
### 集合管理(Collection = 建表)
创建集合时必须声明**向量维度**与**距离度量**(COSINE / EUCLID / DOT)——建库与查询必须同维度、同语义空间(同 embedding 模型):
```python
COLL = "shop_demo"
# 演示可重复运行:存在则删除重建
if qdrant.collection_exists(COLL):
qdrant.delete_collection(COLL)
qdrant.create_collection(
collection_name=COLL,
vectors_config=VectorParams(size=8, distance=Distance.COSINE),
)
info = qdrant.get_collection(COLL)
print(f"✅ 集合 {COLL} 创建成功,状态: {info.status}, 配置: cosine × 8维")
```
实测输出:
```text
✅ 集合 shop_demo 创建成功,状态: green, 配置: cosine × 8维
```
|
|
### 写入数据(Point = id + vector + payload)
- `id`:必须是 **int 或 UUID**(业务 id 放 payload)
- `vector`:定长浮点列表(本例手工 8 维小向量,真实场景来自 Embedding API)
- `payload`:任意 JSON 元数据(检索时随点带回)
```python
import numpy as np
# 手工构造 8 条商品(向量示意:前三维近似"品类特征",后五维随机)
SHOP_ITEMS = [
{"id": 1, "name": "机械键盘 K87", "category": "外设", "price": 299, "stock": 42},
{"id": 2, "name": "静音鼠标 M3", "category": "外设", "price": 99, "stock": 8},
{"id": 3, "name": "4K 显示器 U27", "category": "显示", "price": 1599, "stock": 15},
{"id": 4, "name": "笔记本支架 S1", "category": "配件", "price": 89, "stock": 200},
{"id": 5, "name": "降噪耳机 H9", "category": "音频", "price": 899, "stock": 0},
{"id": 6, "name": "音箱 X1", "category": "音频", "price": 399, "stock": 66},
{"id": 7, "name": "人体工学椅 C2", "category": "家具", "price": 1299, "stock": 3},
{"id": 8, "name": "屏幕挂灯 L1", "category": "显示", "price": 199, "stock": 120},
]
def make_vector(item):
"""示意向量:按品类给特征维度赋值,加少量噪声"""
base = {"外设": [1,0,0], "显示": [0,1,0], "配件": [0,0,1],
"音频": [1,0,1], "家具": [0,1,1]}[item["category"]]
noise = np.random.RandomState(item["id"]).rand(5) * 0.3
return (np.array(base + noise.tolist())).tolist()
points = [
PointStruct(id=it["id"], vector=make_vector(it), payload=it)
for it in SHOP_ITEMS
]
qdrant.upsert(collection_name=COLL, points=points)
print(f"✅ 写入 {qdrant.count(COLL).count} 个点") # ✅ 写入 8 个点
```
### 向量检索(query_points)
`query_points(query=向量, limit=TopK)` 返回按相似度排序的 ScoredPoint 列表;**payload 随结果直接带回**,无需二次查库:
```python
# 找一个"音频"特征的商品:向量 [1,0,1,...] 与音频品类最接近
query_vec = make_vector({"id": 999, "category": "音频"})
res = qdrant.query_points(collection_name=COLL, query=query_vec, limit=3)
print("❓ 查询:音频类商品")
for pt in res.points:
print(f" [{pt.score:.4f}] id={pt.id} {pt.payload['name']}({pt.payload['category']})")
```
实测输出:
```text
❓ 查询:音频类商品
[0.9854] id=5 降噪耳机 H9(音频)
[0.9811] id=6 音箱 X1(音频)
[0.7179] id=1 机械键盘 K87(外设)
```
> 检索 API 用 `query_points`(qdrant-client 1.10+,旧 `search` 已弃用);COSINE 距离下 score 为 0~1,越大越相似。
|
|
### 元数据过滤(Filter = WHERE 子句)
**先过滤圈范围、再向量检索算相关**——可过滤 HNSW 在图遍历时直接剪枝,过滤与检索同链路低延迟:
```python
# 只在"有库存"且"价格 100~1000"的范围内检索音频类商品
res = qdrant.query_points(
collection_name=COLL,
query=query_vec,
query_filter=Filter(must=[
FieldCondition(key="category", match=MatchValue(value="音频")),
FieldCondition(key="price", range=Range(gte=100, lte=1000)),
FieldCondition(key="stock", range=Range(gte=1)), # 排除无货
]),
limit=3,
)
print("❓ 查询:音频类 + 价格100~1000 + 有库存")
for pt in res.points:
print(f" [{pt.score:.4f}] {pt.payload['name']} ¥{pt.payload['price']} 库存{pt.payload['stock']}")
```
实测输出:
```text
❓ 查询:音频类 + 价格100~1000 + 有库存
[0.9811] 音箱 X1 ¥399 库存66
💡 降噪耳机 H9(库存0)被 stock 谓词正确过滤掉了
```
### 解读
- 不加 filter 时 Top1 是"降噪耳机 H9"(0.9854);加 filter 后 H9 因库存 0 被**精准剔除**,语义最近的"音箱 X1"浮出
- 三个谓词等价于 SQL 的 `WHERE category='音频' AND price BETWEEN 100 AND 1000 AND stock>=1`
- 过滤条件的完整类型系统见「Payload元数据过滤」节
|
|
### 更新与删除
- `upsert`:同 id 覆盖(幂等,重复执行安全)
- `set_payload` / `overwrite_payload`:只改元数据不动向量
- `delete`:按 id 或按 filter 批量删除
```python
# ① 更新:H9 补货(只改 payload,不动向量)
qdrant.set_payload(COLL, payload={"stock": 30}, points=[5])
# ② retrieve:按 id 精确取回(不走相似度)
pt = qdrant.retrieve(COLL, ids=[5], with_payload=True, with_vectors=False)[0]
print("H9 补货后:", pt.payload)
# ③ 删除:按 id 删除屏幕挂灯
qdrant.delete(COLL, points_selector=[8])
print(f"删除后总数: {qdrant.count(COLL).count}")
```
实测输出:
```text
H9 补货后: {'id': 5, 'name': '降噪耳机 H9', 'category': '音频', 'price': 899, 'stock': 30}
删除后总数: 7
```
### 滚动浏览(scroll = 全表扫描分页)
不带相似度的列表遍历,用于导出/审计/迁移:
```python
records, next_offset = qdrant.scroll(
collection_name=COLL, limit=100, with_payload=True, with_vectors=False,
)
for pt in records:
p = pt.payload
print(f" id={pt.id} {p['name']:12s} {p['category']} ¥{p['price']} 库存{p['stock']}")
```
实测输出(L1 已删,剩 7 条全带 payload):
```text
id=1 机械键盘 K87 外设 ¥299 库存42
id=2 静音鼠标 M3 外设 ¥99 库存8
id=3 4K 显示器 U27 显示 ¥1599 库存15
id=4 笔记本支架 S1 配件 ¥89 库存200
id=5 降噪耳机 H9 音频 ¥899 库存30
id=6 音箱 X1 音频 ¥399 库存66
id=7 人体工学椅 C2 家具 ¥1299 库存3
```
|
|
### 快照备份(REST API)
快照是 Qdrant 的一致性备份机制(**零配置**,直接打):
```python
import requests
QD = "http://127.0.0.1:6333"
r = requests.post(QD + "/collections/" + COLL + "/snapshots")
snap = r.json()["result"]
print(f"✅ 快照已创建: {snap['name']}({snap['size']} 字节)")
# 恢复快照到新集合(演示:改名恢复)
snap_path = f"/opt/app/qdrant/snapshots/{COLL}/{snap['name']}"
with open(snap_path, "rb") as f:
r2 = requests.post(QD + "/collections/" + COLL + "_restored/snapshots/upload",
files={"snapshot": f})
print("恢复结果:", r2.json().get("status"))
print("恢复后集合列表:", [c.name for c in qdrant.get_collections().collections])
print(f"新集合点数: {qdrant.count(COLL + '_restored').count}")
```
实测输出:
```text
✅ 快照已创建: shop_demo-4637732228442497-2026-09-06-00-20-31.snapshot(65536 字节)
恢复结果: ok
恢复后集合列表: ['shop_demo', 'shop_demo_restored']
新集合点数: 7
```
### 与混合检索 RAG 的衔接
Qdrant 在 RAG 中担任**向量路**:
```text
知识库 ──▶ embedding ──▶ Qdrant collection(原文放 payload 随结果带回)
用户提问 ──┬─ BM25 关键词路(进程内,Top-100) ┐
└─ Qdrant 向量路(query_points,Top-100)┴─▶ RRF 融合 ─▶ Top-3 ─▶ 提示词 ─▶ LLM
```
- 架构分工:**Qdrant 专司向量检索,原文放 payload 随结果带回**(免二次查库);BM25 在进程内零依赖
- 完整流程见 `0905_rag_hybrid.ipynb`(BM25 + Qdrant 混合检索版 RAG,"插嘴 vs 打断"词汇鸿沟实测由向量路救回)
- 实测经验:BM25 零分结果融合前必须过滤,否则污染 RRF 排名
### 一句话总结
**Collection 建表 → PointStruct 写入 → query_points 检索 → Filter 圈范围 → 快照备份——Qdrant 的核心 API 就这五个动作。**
|
|
### 现场还原
notebook `0905_qdrant_usage.ipynb` 的建集合代码,把 `size=8` 改成 `size=256` 后再次运行:
```python
COLL = "shop_demo"
# 演示可重复运行:存在则删除重建
if qdrant.collection_exists(COLL):
qdrant.delete_collection(COLL)
qdrant.create_collection(
collection_name=COLL,
vectors_config=VectorParams(size=256, distance=Distance.COSINE),
)
info = qdrant.get_collection(COLL)
print(f"✅ 集合 {COLL} 创建成功,状态: {info.status}, 配置: cosine × 8维")
```
输出仍然是:
```text
✅ 集合 shop_demo 创建成功,状态: green, 配置: cosine × 8维
```
### 三个疑问与答案速览
| # | 疑问 | 答案速览 |
|---|------|----------|
| ① | 改了 size=256,为什么还输出 8维? | **print 里的"cosine × 8维"是写死的字面量文案**,不是从集合配置读出来的——集合确实已是 256 维,只是文案没变 |
| ② | cosine 是余弦相似度,怎么会有"维度相乘"? | **× 不是乘号,是排版分隔符**:"cosine × 8维" = 距离度量是 cosine、向量维度是 8,两个属性并列展示 |
| ③ | 向量维度只由 size 决定吗? | size 是集合的**维度契约(schema 约束)**;真实维度由 **Embedding 模型输出**决定,size 必须与模型对齐,且创建后不可修改 |
|
|
### f-string 里哪些是动态的?
原 print 语句:
```python
print(f"✅ 集合 {COLL} 创建成功,状态: {info.status}, 配置: cosine × 8维")
```
f-string 中**只有两处花括号是动态求值**,其余全部是字面量:
| 片段 | 类型 | 值来源 |
|---|---|---|
| `{COLL}` | 动态 | 变量 COLL → "shop_demo" |
| `{info.status}` | 动态 | 服务端返回的集合状态 → green |
| `cosine × 8维` | **字面量** | 写代码时手工敲进去的文案,**永远不变** |
所以改 `size=256` 只改了**服务端的集合配置**,print 的文案纹丝不动——输出自然还是"8维"。
### 事实核验:集合真的是 256 维吗?
**是**。代码里 `if qdrant.collection_exists(COLL): qdrant.delete_collection(COLL)` 先删旧集合、再按新 size 重建——**服务端此刻的集合就是 cosine × 256 维**,只是打印文案"撒了谎"。用 `get_collection` 读配置即可验证(动态写法见第 5 个 Tab)。
### 教训
> 打印"配置类"信息时别把值写进文案——要么动态读,要么别打印。这种"文案与事实脱节"的输出在排查时会误导人,本例就是典型。
|
|
### × 是分隔符,不是乘法
"cosine × 8维" 里的 × 只是**排版分隔符**(如同"型号 × 颜色"),表达两个并列属性:
```text
cosine × 8维
│ └── 向量维度:每个向量有 8 个分量(size)
└───────── 距离度量:用余弦(COSINE)衡量相似度
```
它与"余弦 × 维度"的乘法运算没有任何关系。
### 顺带讲清:余弦相似度到底算什么
两个向量 A、B 的余弦相似度只看**方向夹角**:
```text
Σ ai·bi ← 分子:点积——对应维度逐对相乘再求和
cos(θ) = ─────────────────
√Σai² · √Σbi² ← 分母:两个向量的模长
```
- 计算中确实有"**对应维度相乘**"(点积的 ai·bi),但这是逐对相乘求和,**不是"cosine 乘以维度数"**
- 维度的作用:维度越多,点积累加的项越多——但余弦结果归一化到 -1~1,与维度多少无关
- Qdrant 中 `Distance.COSINE`:检索返回的 score 等价于余弦相似度,**越大越相似**(常见 0~1)
### 三种度量回顾(建集合时三选一)
| 距离 | score 含义 | 适合 |
|---|---|---|
| COSINE | 越大越相似(夹角越小) | 通用默认,文本 embedding 常用 |
| EUCLID | 越小越相似(直线距离) | 关注绝对距离的场景 |
| DOT | 越大越相似(不归一化) | 向量已归一化时等价余弦且更快 |
|
|
### size 是什么:集合的"维度契约"
`VectorParams(size=N)` 在创建集合时声明:**本集合只接受 N 维向量**——相当于关系库建表时定义列类型:
- 所有 upsert 进来的向量,分量个数必须**恰好等于 N**,否则服务端直接拒绝
- 集合创建后 **size 不可修改**;要改只能删集合重建,并把数据按新维度重新灌入
- notebook 里"存在则删除重建"就是为了演示可重复运行——改 256 能创建成功,靠的正是先删后建
### 真实维度由 Embedding 模型决定
size 不是拍脑袋定的,**必须等于 Embedding 模型的输出维度**(建库与查询用同一模型,维度天然一致):
| Embedding 模型 | 输出维度 |
|---|---|
| text-embedding-3-small(本站 RAG 实战所用) | 1536 |
| text-embedding-3-large | 3072(可用 dimensions 参数降维) |
| bge-large-zh / bge-m3 | 1024 |
| all-MiniLM-L6-v2(Chroma 内置默认模型) | 384 |
| notebook 手工演示向量 | 8(make_vector:3 维品类特征 + 5 维噪声) |
### 本例改 256 的连锁后果
```text
size=8(原版) :make_vector 产出 8 维 ──▶ upsert 成功
size=256(改后) :make_vector 仍是 8 维 ──▶ upsert 报错(维度不匹配)
```
notebook 后续的 `make_vector` 仍生成 8 维向量——集合改成 256 后**写入会直接失败**(除非把 make_vector 同步改成产出 256 维)。这也是教程选 8 维小向量的原因:手工可构造、无需外部 API。
### 一句话
> **size 决定"集合要几维",模型决定"向量是几维",两者必须相等**;维度不匹配 = 契约破裂 = 写入报错。
|
|
### 修正版:配置从服务端动态读
```python
qdrant.create_collection(
collection_name=COLL,
vectors_config=VectorParams(size=256, distance=Distance.COSINE),
)
info = qdrant.get_collection(COLL)
vp = info.config.params.vectors # 单向量配置即 VectorParams
print(f"✅ 集合 {COLL} 创建成功,状态: {info.status}, "
f"配置: {vp.distance.name.lower()} × {vp.size}维")
```
实测输出(文案与事实一致了):
```text
✅ 集合 shop_demo 创建成功,状态: green, 配置: cosine × 256维
```
> 附:枚举直接打印是 `<Distance.COSINE: 'Cosine'>`,用 `.name.lower()` 得到 "cosine" 更整洁。
### 验证实验:维度不匹配的后果
```python
from qdrant_client.models import PointStruct
# 256 维集合写入 8 维向量 → 服务端拒绝
try:
qdrant.upsert(COLL, points=[PointStruct(id=1, vector=[0.1] * 8, payload={})])
except Exception as e:
print(type(e).__name__, "-", e)
```
报错(示意,版本不同措辞略有差异):
```text
ValueError: Invalid vector, expected dim: 256, got: 8
```
### 顺带:status 为什么是 green
| 状态 | 含义 |
|---|---|
| **green** | 一切就绪(含 HNSW 索引优化完成) |
| **yellow** | 优化进行中(如刚灌入数据、HNSW 异步构建中)——**仍可正常读写** |
| **red** | 集合出错,需要处理 |
### 排查清单(改维度时过一遍)
- [ ] size 与 Embedding 模型输出维度一致?
- [ ] 建库与查询用同一个模型?
- [ ] 改过 size 的话:旧集合删了吗?旧向量按新维度重新生成了吗?
- [ ] 打印配置信息时动态读取(`info.config.params.vectors`),别写死文案
|
|
### 误解还原
> "size=8 是集合 point 的个数,创建时指定多少,插入时就得多少条……那后续追加数据怎么办?"
**size 不是点的个数,是每个向量的分量数(维度)**——两个完全独立的概念:
| 概念 | 含义 | 谁决定 | 能否变 |
|---|---|---|---|
| **size(向量维度)** | 每条向量有多少个浮点分量 | 建集合时 `VectorParams(size=N)` 显式声明 | 创建后固定;改 = 删集合重建 |
| **points 点数(数据条数)** | 集合里存了多少条记录 | 由实际写入量决定 | **随时增删、无上限声明** |
> 关系库类比:size 像 `VARCHAR(n)` 的**列类型**(建表时定死);点数像表的**行数**(随时 insert)——建表从来不需要预知行数。
### 为什么容易误解:notebook 里的数字巧合
notebook 恰好有 **8 条商品**(SHOP_ITEMS 长度 8),向量也恰好是 **8 维**(3 维品类特征 + 5 维噪声)——两个 8 撞在一起,看起来就像"size=条数"。把 size 改成 256 后:**点数还是 8 条**,只是每条向量应是 256 维。
### 后续追加数据:随时 upsert,无需预知数据量
```python
# 追加:新 id 直接写入,点数动态增长
qdrant.upsert(COLL, points=[
PointStruct(id=101, vector=vec_101, payload={"name": "新品A"}),
PointStruct(id=102, vector=vec_102, payload={"name": "新品B"}),
])
print(qdrant.count(COLL).count) # 8 → 10
# 更新:同 id 再 upsert 即覆盖(幂等,重复执行安全)
qdrant.upsert(COLL, points=[PointStruct(id=101, vector=vec_new, payload={"name": "新品A-改"})])
# 删除:按 id 或按 filter
qdrant.delete(COLL, points_selector=[102])
```
集合在设计上:
- **不声明容量**:点数从 0 到亿级动态增长,插入时校验的只有"向量长度 == size"
- **索引增量更新**:HNSW 支持增量插入,索引**异步**构建——写入立即可查,索引就绪后加速(见「HNSW向量索引」节)
- **大批量追加**:batch upsert / gRPC 提吞吐;去重与增量同步交给外层机制(如 LangChain Indexing API)
### 一句话
> **size 管"每条长什么样"(维度),写入管"有多少条"(点数)**——追加数据没有任何前置声明,直接 upsert。
|
|
### 维度是集合级契约,不是每条数据各自指定
维度在**建集合时一次性声明**,集合内**所有向量统一同维**;写入时只要求每条 vector 的长度恰好等于 size:
```python
# ① 建集合时声明维度(一次性,全体生效)
qdrant.create_collection(COLL,
vectors_config=VectorParams(size=1536, distance=Distance.COSINE))
# ② 写入时:每条 vector 恰好 1536 个分量(不能每条各自指定维度)
points = [PointStruct(id=i, vector=emb(text), payload={"text": text})
for i, text in enumerate(texts)]
qdrant.upsert(COLL, points=points)
```
### 每条数据的向量从哪来
向量由 **Embedding 模型**生成(外部 API 或本地模型),Qdrant 只负责存与查:
```text
文本 ──Embedding 模型──▶ [0.12, -0.33, ... 共 N 个分量] ──upsert──▶ 集合(size 必须 == N)
```
### 默认值是多少?
| 参数 | 默认 | 说明 |
|---|---|---|
| **size** | **无默认,必须显式指定** | 不指定 = 集合没有向量存储,普通向量 upsert 会失败;维度由所选 Embedding 模型决定(MiniLM=384、bge-m3=1024、text-embedding-3-small=1536……) |
| **distance** | 服务端默认 **Cosine** | 实践中建议与 size 一起显式写出,官方示例均如此 |
> ⚠️ Qdrant **不会**从首个插入的点推断维度——与"建表必须写列类型"同理;不确定维度时,先查 Embedding 模型的输出维度再建集合。
### 进阶:命名向量(一个集合多组维度)
一个集合可同时存多组**不同维度**的向量(不同模型/不同模态),按名字区分:
```python
qdrant.create_collection(COLL, vectors_config={
"text": VectorParams(size=1536, distance=Distance.COSINE),
"image": VectorParams(size=2048, distance=Distance.COSINE),
})
# 写入与检索时按名指定(检索参数 using="text" / "image")
qdrant.upsert(COLL, points=[PointStruct(id=1, vector={"text": v_text, "image": v_img})])
```
> 较新版本(v1.18+)支持建集合后**补加**命名向量(仍需显式 size/distance);但已建向量的 size 不可修改。
### 为什么 Chroma"感觉不用指定"?
Chroma 传 documents 不传 embeddings 时,用**内置默认模型**(MiniLM,384 维)自动向量化——维度被默认模型"藏"起来了;Qdrant 走存查分工,维度必须显式注册进 schema(qdrant-client 可选 FastEmbed 集成,指定 embedding_models 后能达到同样省心的效果)。
### 一句话
> **维度跟着 Embedding 模型走,size 是集合的统一契约**:建库时声明一次、全体同维、错配即拒;点数完全自由,随时追加。
|
|
### 报错现场(2026-09-10 实测)
把 `size` 改为 256 后重跑 notebook:**建集合成功、写入报错**——
```text
── 单元格「二、集合管理」:成功(集合契约已是 256 维,动态打印正常)──
✅ 集合 shop_demo 创建成功,状态: green, 配置: cosine × 256维
── 单元格「三、写入数据」:upsert 被服务端拒绝 ──
UnexpectedResponse: Unexpected Response: 400 (Bad Request)
Raw response content:
b'{"status":{"error":"Wrong input: Vector dimension error: expected dim: 256, got 8"},"time":0.000049078}'
```
### 错误信息解剖
| 片段 | 含义 |
|---|---|
| `b'...'` | Python bytes 字面量——客户端收到的 **REST 原始响应体**(未解析成结构化异常时原样透出) |
| `400 (Bad Request)` / `Wrong input` | 4xx **客户端错误**:请求内容不合法,不是服务端故障 |
| `Vector dimension error` | 错误类别:**维度校验失败**(在写入入口就被拒绝) |
| `expected dim: 256` | 集合契约要求的维度(size=256) |
| `got 8` | 实际收到的向量维度(make_vector 仍产出 3+5=8 维) |
| `"time":0.000049078` | 服务端处理耗时(49 微秒)——校验在入口完成,代价极小 |
### 原因链条:契约改了,向量没跟上
```text
① 建集合改了 size=256 ──▶ 契约变为"只收 256 维"(cell 6 成功,服务端无问题)
② make_vector 没有改 ──▶ 仍产出 3 维品类特征 + 5 维噪声 = 8 维
③ upsert 入口校验拒绝 ──▶ expected dim: 256, got 8
```
> 这正是「谜底③ / 追问①」预判的连锁后果:"集合改成 256 后写入会直接失败(除非 make_vector 同步改)"——本次实测验证。
### 原子性:报错时一条都没写入
Qdrant 的**单批 upsert 是原子操作**——维度校验失败即整批拒绝,`qdrant.count(COLL).count` 为 0,不存在"写了一半"的脏数据。修复后直接重跑即可,无需任何清理。
### 修复(已应用到 notebook):DIM 常量单一来源
维度出现在**三处**,必须完全一致:建集合的 `size`、`make_vector` 的产出、检索时的 `query_vec`(查询向量同样要 256 维,否则 `query_points` 报同款错)。用一个 DIM 常量贯穿:
```python
# ① 单元格「二、集合管理」:定义 DIM,size 引用 DIM
DIM = 256 # 向量维度:建集合、生成向量、查询向量共用(单一事实来源)
qdrant.create_collection(
collection_name=COLL,
vectors_config=VectorParams(size=DIM, distance=Distance.COSINE),
)
```
```python
# ② 单元格「三、写入数据」:make_vector 补齐到 DIM 维
def make_vector(item):
"""示意向量:前三维品类特征 + (DIM-3) 维噪声,共 DIM 维"""
base = {"外设": [1,0,0], "显示": [0,1,0], "配件": [0,0,1],
"音频": [1,0,1], "家具": [0,1,1]}[item["category"]]
noise = np.random.RandomState(item["id"]).rand(DIM - 3) * 0.3
return (np.array(base + noise.tolist())).tolist()
```
查询侧 `query_vec = make_vector({"id": 999, "category": "音频"})` 自动同为 DIM 维——**三处一致,一处修改**。想换维度只改 DIM=8 或 DIM=512,重跑建集合与写入两个单元格即可。
### 备选修复方案
| 方案 | 做法 | 适用 |
|---|---|---|
| **DIM 常量(推荐,已采用)** | size 与 make_vector 共用 DIM | 保留 256 维实验意图,一处改处处改 |
| 改回 8 维 | `size=8`(重跑建集合格,自带删除重建) | 只想复现原教程效果 |
| 由向量反推 | `size=len(make_vector(SHOP_ITEMS[0]))` | 向量定义是"事实源头"、维度常变的场景 |
### 修复后重跑
```text
✅ 写入 8 个点
❓ 查询:音频类商品
[0.9x] id=5 降噪耳机 H9(音频) ... ← 前 3 维品类信号仍在,同品类依然最相似
```
两点说明:
- **只需重跑「三、写入数据」单元格**——集合本来就是 256 维(建集合那步已成功),报错在写入侧;下游单元格顺次重跑即可
- **区分度会被稀释**:256 维中仅 3 维承载品类信号,同/跨品类的余弦差距变小(演示仍够用)。真实场景无此问题——1536 维全部由 Embedding 模型生成,每一维都有语义
### 教训清单
- [ ] 改 size = 改契约:写入与查询的向量必须同步改(用常量 DIM 单一来源)
- [ ] 看到 `expected dim: X, got: Y`:X 是集合契约、Y 是实际向量,对着**向量生成方**改
- [ ] `400 Wrong input` = 请求不合法,先查自己的数据(维度 / id 格式 / 类型),别怀疑服务端
- [ ] 批量 upsert 原子性:失败即全拒,修复后重跑即可,无脏数据
|
Qdrant 官方文档
Qdrant 源码仓库(GitHub)
Qdrant Releases 二进制下载
Qdrant:Filterable HNSW(可过滤 HNSW 原理)
HNSW 论文:Malkov & Yashunin, 2016