参考:
Qdrant官网

Embedding(向量化)

Embedding = 把一段文字(或图片/音频)转换成一个固定长度的数字数组(向量),比如 [0.02, -0.13, 0.87, ...],通常是 384、768、1536 维。

这个转换由一个专门训练过的模型完成(如 OpenAI 的 text-embedding-3、BGE、M3E),不是简单的编码/加密。

转换的核心规则:语义相近的文字 → 向量在空间中的位置也相近。

为什么"挠家具"和"抓沙发"字面不同,向量却能接近?

因为 Embedding 模型是在海量文本上训练的,学到了"挠"和"抓"、"家具"和"沙发"在大量上下文中经常互换使用、含义相近。模型把这种"共现规律"编码进了向量的每一维数字里——你可以粗略理解成,向量的每一维代表某种隐含的语义特征(不是人能直接读懂的具体维度,而是模型学出来的抽象特征)。

原始文字
  ↓ (Embedding 模型, 如 BGE/OpenAI)
向量 [0.02, -0.13, ...]
  ↓ (存入)
向量数据库 (Qdrant的Collection)
  ↓ (查询时)
用户问题 → 同一个Embedding模型 → 查询向量
  ↓
在数据库里找"距离最近"的存储向量
  ↓
返回对应的原文

Embedding 模型 ≠ 向量数据库。Embedding 模型(如 BGE)负责"把文字变成向量",Qdrant 只负责"存向量+快速找最近的向量",两者是分开的组件,Qdrant 本身不做向量化。

向量数据库不存"文字的意思",存的就是一堆数字数组,"意思"隐含在数字的相对位置关系里。

Qdrant 核心概念

  1. Collection(集合)= 存放一类向量的容器,类似关系数据库里的"表"。一个 Collection 里的所有向量必须维度相同、用同一种距离度量

  2. Point(点)= Collection 里的一条记录,结构是 {id, vector, payload}

  3. Distance(距离度量)= 判断两个向量"有多相似"的计算方式,Qdrant 支持三种:

  • Cosine(余弦相似度)—— 只看方向,不看长度,文本 Embedding 最常用

  • Euclidean(欧氏距离)—— 看空间直线距离

  • Dot(点积)—— 同时受方向和长度影响,某些模型(如做过归一化的)会指定用它

创建 Collection 时必须指定:向量维度(跟你用的 Embedding 模型输出维度一致,比如 BGE-base 是 768 维)+ 距离度量方式

为什么 Collection 要求维度和距离度量统一?

因为"距离计算"本身是数学运算,必须在同一个坐标系下才有意义——你不能拿一个 768 维的向量去跟一个 1536 维的向量算余弦相似度。同理,如果一部分数据用 Cosine 存、一部分用 Euclidean 算,"距离"这个概念就失去了统一标准,排序结果没有可比性。这也意味着:一旦你换了 Embedding 模型(维度变了),旧 Collection 里的向量就不能直接复用了,得重新向量化。

Qdrant 实例 (一个数据库服务)
   │
   ├── Collection A (维度768, Cosine)  ← 比如"产品文档"
   │      ├── Point 1 {id, vector, payload}
   │      ├── Point 2 {id, vector, payload}
   │      └── ...
   │
   └── Collection B (维度1536, Cosine)  ← 比如"客服对话"
          └── ...

Collection ≠ 数据库(Database)。Qdrant 一个实例本身相当于"数据库",Collection 才对应"表"

维度不是随便定的,必须和你选用的 Embedding 模型输出维度严格一致,写错了插入数据会直接报错

Payload 过滤 (Filter)

  • Filter 语法核心三件套:must(必须满足,相当于 AND)、should(满足其一即可,相当于 OR)、must_not(必须不满足)

  • 过滤和向量搜索同时发生,不是"先搜后筛",这样即使目标数据向量相似度不是最高,只要满足过滤条件,也不会被漏掉

  • 常用条件类型:MatchValue(精确匹配)、Range(数值范围,如年份/价格区间)、MatchAny(多值匹配,如标签在某个列表里)

用户查询:"2024年的财务政策"
     │
     ├─→ 向量化 → 查询向量 → 语义相似度计算
     │
     └─→ 解析出过滤条件 → year = 2024
     │
     └─→ Qdrant 同时应用两者,在"year=2024"的子集内做向量近邻搜索
     │
     └─→ 返回 Top-K
from qdrant_client.models import Filter, FieldCondition, MatchValue

results = client.search(
    collection_name="my_docs",
    query_vector=[0.03, -0.11, 0.85, ...],  # "2024年财务政策"的查询向量
    query_filter=Filter(
        must=[
            FieldCondition(key="year", match=MatchValue(value=2024))
        ]
    ),
    limit=5
)

Filter 不是在应用层写 if 判断筛选,而是传给 Qdrant 服务端,由数据库在检索阶段就应用(性能更好,且不会漏掉排名靠后但符合条件的结果)

payload 字段要能被高效过滤,最好提前建索引(Qdrant 里叫 Payload Index),不建索引在数据量大时过滤会变慢——这是进阶话题,先记住有这个概念

混合检索 (Hybrid Search)

  • 混合检索 = 稠密向量(Dense Vector)+ 稀疏向量(Sparse Vector) 两种检索方式的结果融合,取长补短

  • 稠密向量(我们之前一直在讲的)—— 每一维都有值(如768维全是数字),擅长捕捉语义相似("挠家具"≈"抓沙发")

  • 稀疏向量 —— 大部分维度是0,只有少数维度有值,本质上类似升级版的关键词匹配(如 BM25),擅长捕捉精确词面匹配(比如专有名词、型号、产品代码这类"必须原词命中"的场景)

  • Qdrant 的 1.10 版本引入了统一的 Query API,把稠密向量、稀疏向量、多向量等不同检索方式整合进同一个查询端点,支持在服务器端组合出混合搜索 Qdrant

为什么单用稠密向量不够?

举个例子:用户搜"Qdrant v1.10 更新了什么",稠密向量搜索理解"更新了什么"这种语义没问题,但对"v1.10"这种具体版本号,语义模型未必能精确区分"v1.10"和"v1.9"——它们在语义空间里可能离得很近(都是"版本号"这个概念),但用户明明要的是精确匹配。这时候纯语义搜索会失真,而传统关键词/稀疏向量正好擅长处理这种"必须原词命中"的情况。

反过来,纯稀疏向量(关键词)搜不出"挠家具"→"抓沙发"这种同义改写。所以两者结合,互补短板。

用户查询
   │
   ├─→ 稠密Embedding模型 → 稠密向量 → 语义相似度搜索
   │
   └─→ 稀疏Embedding模型(如BM25/SPLADE) → 稀疏向量 → 关键词精确匹配
   │
   └─→ 两路结果 → 融合算法(RRF 或 DBSF) → 最终排序结果

Qdrant 支持用 RRF(Reciprocal Rank Fusion)或 DBSF 融合稠密、稀疏、多向量的检索结果,也可以用 Formula Query 自定义打分逻辑:

  • RRF:不看两路各自的分数具体多大,只看"排名",把两路结果按排名做加权合并——简单粗暴但很稳健,是最常用的默认选择

  • DBSF:会考虑分数分布,做更精细的归一化融合

混合检索(同一 Collection 内融合稠密+稀疏)≠ 之前提到的"跨 Collection 联合查询"(Qdrant 不原生支持后者,混合检索讲的是前者)

稀疏向量 ≠ 稠密向量降维压缩。稀疏向量是完全不同的表示方式(基于词频/词重要性),不是把768维向量简单变"稀疏"

from qdrant_client.models import Prefetch, FusionQuery, Fusion

results = client.query_points(
    collection_name="my_docs",
    prefetch=[
        Prefetch(query=dense_vector, using="dense", limit=20),   # 稠密向量召回
        Prefetch(query=sparse_vector, using="sparse", limit=20), # 稀疏向量召回
    ],
    query=FusionQuery(fusion=Fusion.RRF),  # 融合两路结果
    limit=5
)