• 文章
  • 混合搜索革新 - 使用 Qdrant 的查询 API 构建
返回向量搜索手册

混合搜索革新 - 使用 Qdrant 的查询 API 构建

Kacper Łukawski

·

2024 年 7 月 25 日

Hybrid Search Revamped - Building with Qdrant's Query API

自我们发布关于如何使用 Qdrant 构建混合搜索系统的原始文章以来,已经过去一年多了。最初的想法很简单:结合不同搜索方法的结果以提高检索质量。早在 2023 年,你仍需要使用额外的服务来引入词汇搜索能力并组合所有中间结果。从那时起,情况发生了变化。一旦我们引入了对稀疏向量的支持,额外的搜索服务就变得过时了,但你仍然需要在你的端组合来自不同方法的结果。

Qdrant 1.10 引入了新的查询 API,通过结合不同的搜索方法来构建搜索系统,从而提高检索质量。现在一切都在服务器端完成,你可以专注于为你的用户构建最佳搜索体验。在本文中,我们将向你展示如何利用新的查询 API 来构建一个混合搜索系统。

隆重推出新的查询 API

在 Qdrant,我们认为向量搜索能力远不止简单的近邻搜索。这就是为什么我们为不同的搜索用例提供了单独的方法,例如 searchrecommenddiscover。在最新版本中,我们很高兴地引入新的查询 API,它将所有这些方法组合到一个端点中,并且还支持创建嵌套的多阶段查询,可用于构建复杂的搜索管道。

如果你是现有的 Qdrant 用户,你可能有一个正在运行的搜索机制,无论使用稀疏向量还是密集向量,你都想改进它。进行任何更改之前,都应该对其有效性进行适当的评估。

你的搜索系统有多有效?

如果你不衡量质量,任何实验都没有意义。否则,你如何比较哪种方法更适合你的用例呢?最常见的方法是使用标准指标,例如 precision@kMRRNDCG。有一些现有库,例如 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。这种模型家族不是为每个文档或查询创建一个单一嵌入,而是为文本的每个 token 创建一个单独的嵌入。在搜索过程中,最终得分基于查询 token 和文档 token 之间的交互计算。与交叉编码器相反,文档嵌入可以预先计算并存储在数据库中,这使得搜索过程快得多。如果你想了解更多细节,请查看 我们 Jina AI 的朋友写的关于 ColBERT 的文章

Late interaction

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

Multiple vectors per point

构建混合搜索没有单一的方法。设计它的过程是一项探索性实践,你需要测试各种设置并衡量它们的有效性。构建一个合适的搜索体验是一项复杂的任务,最好以数据为导向,而不仅仅依赖直觉。

融合 vs 重排

我们可以区分构建混合搜索系统的两种主要方法:融合和重排。前者是关于组合来自不同搜索方法的结果,完全基于每种方法返回的得分。这通常涉及一些归一化,因为不同方法返回的得分可能范围不同。之后,有一个公式,它接受相关性度量并计算我们稍后用于重新排序文档的最终得分。Qdrant 内置支持倒数排序融合 (Reciprocal Rank Fusion) 方法,这是该领域的事实标准。

Fusion

另一方面,重排是关于获取来自不同搜索方法的结果,并基于使用文档内容(而不仅仅是得分)进行的一些额外处理来重新排序它们。这种处理可能依赖于额外的神经网络模型,例如交叉编码器,如果用于整个数据集,效率会很低。这些方法实际上只适用于在更快搜索方法返回的较小候选子集上使用。在这种情况下,后期交互模型(如 ColBERT)效率高得多,因为它们无需访问集合中的所有文档即可用于对候选进行重排。

Reranking

为什么不是线性组合?

通常有人建议使用全文和向量搜索得分来形成线性组合公式,以对结果进行重排。即:

final_score = 0.7 * vector_score + 0.3 * full_text_score

然而,我们甚至没有考虑过这种设置。为什么?这些得分不能使问题线性可分。我们使用 BM25 得分以及余弦向量相似度,将它们都用作二维空间中的点坐标。图表显示了这些点的分布:

A distribution of both Qdrant and BM25 scores mapped into 2D space.

将 Qdrant 和 BM25 得分都映射到二维空间中的分布图。它清楚地显示,相关和不相关对象在该空间中不是线性可分的,因此使用两种得分的线性组合不会给我们带来合适的混合搜索。

相关和不相关的项目都混在一起。任何线性公式都无法区分它们。 因此,这不是解决问题的方法。

在 Qdrant 中构建混合搜索系统

最终,任何搜索机制也可能是一个重排机制。你可以用稀疏向量预取结果,然后用密集向量对它们进行重排,反之亦然。或者,如果你有 Matryoshka 嵌入,你可以先用最低维度的密集向量对候选进行过采样,然后通过用更高维度的嵌入对它们进行重排来逐步减少候选数量。没有什么能阻止你结合融合和重排。

让我们更进一步,构建一个混合搜索机制,它结合了 Matryoshka 嵌入、密集向量和稀疏向量的结果,然后用后期交互模型对它们进行重排。同时,我们将引入额外的重排和融合步骤。

Complex search pipeline

我们的搜索管道由两个分支组成,每个分支负责检索我们最终想要用后期交互模型进行重排的文档子集。首先连接到 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,
    ),
)

第二个分支可能已经被称为混合搜索,因为它结合了密集向量和稀疏向量的结果并进行融合。然而,没有什么能阻止我们构建更复杂的搜索管道。

这是在 Python 中调用目标查询 API 的样子:

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 式模型的多向量表示用作重排步骤。这反映了该领域的最新趋势,因为单向量方法仍然最有效,但多向量能更好地捕捉文本的细微差别。

Reranking with late interaction models

假设你从不单独使用后期交互模型进行检索,而只用于重排,这种设置会带来隐藏成本。默认情况下,集合中每个配置的密集向量(即使是多向量)都会创建一个相应的 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(...)
    },
)

你不会注意到搜索性能有任何差异,但当你将嵌入上传到集合时,资源使用会显著降低。

一些轶事观察

没有哪个算法在所有情况下都表现最佳。在某些情况下,基于关键词的搜索会赢,反之亦然。下表显示了我们在实验期间在 WANDS 数据集中找到的一些有趣的示例:

查询BM25 搜索向量搜索
电竞桌书桌 ❌游戏桌 ✅
冰淇淋盘子木墙装饰上的“吃”盘子 ❌alicyn 8.5 寸三聚氰胺甜点盘 ✅
带厚板的厨房桌工艺厨房相思木切菜板 ❌工业风格实木餐桌 ✅
木床头柜30 寸床头台灯 ❌便携式床边茶几 ✅

关键词搜索表现更好的示例

查询BM25 搜索向量搜索
电脑椅鲜艳的电脑工作椅 ✅办公椅 ❌
64.2 寸玄关桌cervantez 64.2 寸玄关桌 ✅69.5 寸玄关桌 ❌

在 Qdrant 1.10 中试试新的查询 API

Qdrant 1.10 中引入的新查询 API 是构建混合搜索系统的游戏规则改变者。你不需要任何额外的服务来组合来自不同搜索方法的结果,甚至可以创建更复杂的管道并直接从 Qdrant 提供服务。

我们关于《构建终极混合搜索》的网络研讨会带你了解使用 Qdrant 查询 API 构建混合搜索系统的过程。如果你错过了,可以观看录像,或查看笔记本

如果你在构建混合搜索系统时有任何问题或需要帮助,请随时在 Discord 上联系我们。

此页面有用吗?

感谢你的反馈!🙏

很抱歉听到这个。😔 你可以在 GitHub 上编辑此页面,或创建一个 GitHub 问题。