自从我们发布有关如何使用Qdrant构建混合搜索系统的原始文章已经超过一年。这个想法很简单:结合不同搜索方法的结果以改善检索质量。在2023年,您仍需要使用额外的服务来提供词汇搜索能力并结合所有中间结果。从那时起,情况发生了变化。一旦我们引入了对稀疏向量的支持,额外的搜索服务变得过时,但您仍然需要在您的端结合不同方法的结果。
Qdrant 1.10 引入了一个新的查询 API,允许您通过组合不同的搜索方法来构建搜索系统,以提高检索质量。 一切现在都在服务器端完成,您可以专注于为您的用户构建最佳搜索体验。在本文中,我们将向您展示如何利用新的 查询 API 构建混合搜索系统。
介绍新的查询API
在Qdrant,我们认为向量搜索能力远不止于简单的最近邻搜索。这就是为什么我们为不同的搜索用例提供了独立的方法,如 search、 recommend 或 discover。通过最新的发布,我们很高兴地介绍新的查询API,它将所有这些方法组合成一个单一的端点,并且还支持创建嵌套的多阶段查询,可以用于构建复杂的搜索管道。
如果您是现有的 Qdrant 用户,您可能有一个想要改进的搜索机制,无论是稀疏的还是密集的。进行任何更改之前,应先进行适当的效果评估。
您的搜索系统有多有效?
如果不测量质量,则实验没有意义。你还怎么比较哪种方法更适合你的用例?最常用的方法是使用标准指标,例如 precision@k,MRR,或 NDCG。现有的库,例如 ranx,可以帮助你实现这一点。我们需要有真实的测试数据集才能计算这些,但整理数据集是一个单独的任务。
from ranx import Qrels, Run, evaluate
# Qrels, or query relevance judgments, keep the ground truth data
qrels_dict = { "q_1": { "d_12": 5, "d_25": 3 },
"q_2": { "d_11": 6, "d_22": 1 } }
# Runs are built from the search results
run_dict = { "q_1": { "d_12": 0.9, "d_23": 0.8, "d_25": 0.7,
"d_36": 0.6, "d_32": 0.5, "d_35": 0.4 },
"q_2": { "d_12": 0.9, "d_11": 0.8, "d_25": 0.7,
"d_36": 0.6, "d_22": 0.5, "d_35": 0.4 } }
# We need to create both objects, and then we can evaluate the run against the qrels
qrels = Qrels(qrels_dict)
run = Run(run_dict)
# Calculating the NDCG@5 metric is as simple as that
evaluate(qrels, run, "ndcg@5")
可用的嵌入选项与查询 API
支持每个点的多个向量在Qdrant中并不是什么新鲜事,但引入查询API使其更加强大。1.10版本支持多向量,从而允许您将嵌入列表视为一个单一实体。利用此功能的方法有很多,其中最突出的是对延迟交互模型的支持,例如 ColBERT。与每个文档或查询只有一个嵌入的方式不同,这类模型为文本的每个标记创建一个单独的嵌入。在搜索过程中,最终得分是基于查询和文档的标记之间的交互计算的。与交叉编码器不同,文档嵌入可以预先计算并存储在数据库中,这使得搜索过程变得更快。如果您对细节感兴趣,请查看 我们来自Jina AI的朋友撰写的关于ColBERT的文章。

除了多向量,您可以使用常规的稠密向量和稀疏向量,并尝试使用更小的数据类型来减少内存使用。命名向量可以帮助您存储不同维度的嵌入,如果您使用多个模型来表示您的数据,或者想利用套娃嵌入,这非常有用。

构建混合搜索没有单一的方法。设计的过程是一个探索性的练习,你需要测试各种设置并测量它们的有效性。构建一个良好的搜索体验是一个复杂的任务,最好保持数据驱动,而不仅仅依赖直觉。
融合与重排序
我们可以区分构建混合搜索系统的两种主要方法:融合和重新排名。前者是基于每种方法返回的分数,结合不同搜索方法的结果。这通常涉及一些标准化,因为不同方法返回的分数可能在不同的范围内。在此之后,有一个公式,它取 relevancy measures 并计算出最终的分数,我们随后用来重新排序文档。Qdrant 内置支持倒数排名融合方法,这是该领域的事实标准。

重新排序,另一方面,是关于从不同搜索方法中获取结果并根据一些额外处理对它们进行重新排序,这些处理使用文档的内容,而不仅仅是分数。这种处理可能依赖于一个额外的神经模型,比如跨编码器,这在整个数据集上使用会非常低效。这些方法实际上只有在用于通过更快的搜索方法返回的较小候选子集时才是适用的。延迟交互模型,如ColBERT,在这种情况下更有效,因为它们可以在不需要访问所有文档的情况下对候选项进行重新排序。

为什么不使用线性组合?
通常建议使用全文搜索和向量搜索得分来形成线性组合公式,以重新排名结果。过程如下:
final_score = 0.7 * vector_score + 0.3 * full_text_score
然而,我们甚至没有考虑这样的设置。为什么?这些分数并不能使问题具有线性可分性。我们使用了BM25分数以及余弦向量相似度,将它们作为二维空间中的点坐标。图表显示了这些点是如何分布的:

将Qdrant和BM25得分分布映射到二维空间。它清楚地表明相关对象和非相关对象在该空间中并不是线性可分的,因此使用这两种得分的线性组合不会为我们提供一个适当的混合搜索。
相关和不相关的项目混合在一起。 线性公式无法区分它们。 因此,这不是解决问题的方法。
在Qdrant中构建混合搜索系统
最终,任何搜索机制也可能是一个重新排名机制。你可以用稀疏向量预取结果,然后用稠密向量重新排名,或者反过来。如果你有套娃嵌入,你可以从用最低维度的稠密向量过采样候选者开始,然后逐渐通过用更高维的嵌入重新排名来减少候选者的数量。没有什么可以阻止你结合融合和重新排名。
让我们更进一步,构建一种混合搜索机制,该机制结合了来自 Matryoshka 嵌入、稠密向量和稀疏向量的结果,然后使用晚期交互模型对它们进行重新排序。与此同时,我们将引入额外的重新排序和融合步骤。

我们的搜索管道由两个分支组成,每个分支负责检索一部分文档,这些文档最终我们希望使用后期交互模型重新排序。让我们先连接到Qdrant,然后构建搜索管道。
from qdrant_client import QdrantClient, models
client = QdrantClient("http://localhost:6333")
使用Matryoshka嵌入的所有步骤可以在查询API中指定为嵌套结构:
# The first branch of our search pipeline retrieves 25 documents
# using the Matryoshka embeddings with multistep retrieval.
matryoshka_prefetch = models.Prefetch(
prefetch=[
models.Prefetch(
prefetch=[
# The first prefetch operation retrieves 100 documents
# using the Matryoshka embeddings with the lowest
# dimensionality of 64.
models.Prefetch(
query=[0.456, -0.789, ..., 0.239],
using="matryoshka-64dim",
limit=100,
),
],
# Then, the retrieved documents are re-ranked using the
# Matryoshka embeddings with the dimensionality of 128.
query=[0.456, -0.789, ..., -0.789],
using="matryoshka-128dim",
limit=50,
)
],
# Finally, the results are re-ranked using the Matryoshka
# embeddings with the dimensionality of 256.
query=[0.456, -0.789, ..., 0.123],
using="matryoshka-256dim",
limit=25,
)
同样,我们可以构建搜索管道的第二个分支,该分支使用密集和稀疏向量检索文档,并使用相互排名融合方法对它们进行融合:
# The second branch of our search pipeline also retrieves 25 documents,
# but uses the dense and sparse vectors, with their results combined
# using the Reciprocal Rank Fusion.
sparse_dense_rrf_prefetch = models.Prefetch(
prefetch=[
models.Prefetch(
prefetch=[
# The first prefetch operation retrieves 100 documents
# using dense vectors using integer data type. Retrieval
# is faster, but quality is lower.
models.Prefetch(
query=[7, 63, ..., 92],
using="dense-uint8",
limit=100,
)
],
# Integer-based embeddings are then re-ranked using the
# float-based embeddings. Here we just want to retrieve
# 25 documents.
query=[-1.234, 0.762, ..., 1.532],
using="dense",
limit=25,
),
# Here we just add another 25 documents using the sparse
# vectors only.
models.Prefetch(
query=models.SparseVector(
indices=[125, 9325, 58214],
values=[-0.164, 0.229, 0.731],
),
using="sparse",
limit=25,
),
],
# RRF is activated below, so there is no need to specify the
# query vector here, as fusion is done on the scores of the
# retrieved documents.
query=models.FusionQuery(
fusion=models.Fusion.RRF,
),
)
第二个分支可以称为混合,因为它结合了稠密向量和稀疏向量的结果进行融合。然而,没有什么能阻止我们构建更加复杂的搜索管道。
以下是目标调用Query API在Python中看起来的样子:
client.query_points(
"my-collection",
prefetch=[
matryoshka_prefetch,
sparse_dense_rrf_prefetch,
],
# Finally rerank the results with the late interaction model. It only
# considers the documents retrieved by all the prefetch operations above.
# Return 10 final results.
query=[
[1.928, -0.654, ..., 0.213],
[-1.197, 0.583, ..., 1.901],
...,
[0.112, -1.473, ..., 1.786],
],
using="late-interaction",
with_payload=False,
limit=10,
)
选择是无穷无尽的,新的查询API为您提供了灵活性,可以尝试不同的设置。您很少需要构建如此复杂的搜索管道,但知道在需要时可以做到这一点是好的。
经验教训:多向量表示
许多人已经开始构建混合搜索系统,并向我们提出问题和反馈。 我们见到了许多不同的方法,但一个反复出现的想法是利用 多向量表示与 ColBERT 风格的模型作为重新排序步骤,在使用单向量密集和/或稀疏方法检索候选项之后。 这反映了该领域的最新趋势,因为单向量方法仍然是最有效的,但多向量更好地捕捉文本的细微差别。

假设你从不单独使用延迟交互模型进行检索,而只是用于重排序,这种设置会带来隐含的成本。默认情况下,集合中的每个配置的稠密向量将创建一个对应的HNSW图。即使它是一个多向量。
from qdrant_client import QdrantClient, models
client = QdrantClient(...)
client.create_collection(
collection_name="my-collection",
vectors_config={
"dense": models.VectorParams(...),
"late-interaction": models.VectorParams(
size=128,
distance=models.Distance.COSINE,
multivector_config=models.MultiVectorConfig(
comparator=models.MultiVectorComparator.MAX_SIM
),
)
},
sparse_vectors_config={
"sparse": models.SparseVectorParams(...)
},
)
重新排名将永远不会使用创建的图,因为所有候选项已经被检索。多向量排名只会应用于先前步骤检索到的候选项,因此不需要搜索操作。HNSW变得多余,同时仍然需要执行索引过程,在这种情况下,它会相当繁重。ColBERT类似的模型为每个文档创建数百个嵌入,因此开销是显著的。 为了避免这种情况,您可以禁用此类模型的HNSW图创建:
client.create_collection(
collection_name="my-collection",
vectors_config={
"dense": models.VectorParams(...),
"late-interaction": models.VectorParams(
size=128,
distance=models.Distance.COSINE,
multivector_config=models.MultiVectorConfig(
comparator=models.MultiVectorComparator.MAX_SIM
),
hnsw_config=models.HnswConfigDiff(
m=0, # Disable HNSW graph creation
),
)
},
sparse_vectors_config={
"sparse": models.SparseVectorParams(...)
},
)
您不会注意到搜索性能的任何差异,但是当您将嵌入上传到集合时,资源的使用将显著降低。
一些轶事观察
这两种算法在所有情况下都不是表现最好的。在某些情况下,基于关键词的搜索将是赢家,反之亦然。下表展示了我们在实验过程中在魔杖数据集中找到的一些有趣的例子:
| Query | BM25 Search | Vector Search |
|---|---|---|
| 电子竞技桌 | 桌子 ❌ | 游戏桌 ✅ |
| 冰淇淋盘 | "吃"木墙装饰上的盘子 ❌ | alicyn 8.5 '' 美耐皿甜点盘 ✅ |
| 厚板厨房桌 | 工艺厨房相思木切菜板 ❌ | 实木工业餐桌 ✅ |
| 木质床头柜 | 30英寸床头柜灯 ❌ | 便携式床头边桌 ✅ |
还有一些例子,关键词搜索表现更好:
| Query | BM25 Search | Vector Search |
|---|---|---|
| 电脑椅 | 生动的电脑工作椅 ✅ | 办公室椅 ❌ |
| 64.2 英寸控制台桌 | cervantez 64.2 '' 控制台桌 ✅ | 69.5 '' 控制台桌 ❌ |
尝试 Qdrant 1.10 中的新查询 API
Qdrant 1.10 中引入的新查询 API 是构建混合搜索系统的游戏规则改变者。您无需任何额外的服务即可组合来自不同搜索方法的结果,甚至可以创建更复杂的管道,并直接从 Qdrant 提供它们。
我们的网络研讨会《构建终极混合搜索》带您了解使用 Qdrant 查询 API 构建混合搜索系统的过程。如果您错过了,可以 观看录音,或者 查看笔记本。
如果您有任何问题或需要帮助构建您的混合搜索系统,请随时通过 Discord 与我们联系。

