向量数据库总览
### 朴素方法的问题 想象 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对比
### 先看相同点:同门流派 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 是什么 **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与gRPC双协议
### 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 是什么 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元数据过滤
### 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 谓词先圈定候选集——作用在**候选集裁剪层** - 口诀:**混合检索管"像不像",复杂过滤管"属不属于"**;生产查询常是"先过滤圈范围 → 混合检索算相关 → 重排求精"三层漏斗
Qdrant用法实战
### 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