Calendar 第 5 天

通用查询API

设想一下:一位顾客在您商店的搜索栏中输入“皮夹克”。您希望显示在语义上匹配该风格的商品——例如,即使没有明确提及“皮夹克”,轰炸机夹克也能浮现出来——但您也需要执行您的业务规则。只显示价格低于 200 美元的产品,只显示有库存的商品,只显示过去一年内发布的夹克。传统上,您会发起搜索,收集结果,然后应用过滤器和“粘合代码”。借助 Qdrant 的通用查询 API,所有这些都可以在一个声明性请求中完成。

使用 RRF 并行运行密集 + 稀疏检索

首先,您并行从多个源检索候选对象并融合它们的排名。下面,我们通过使用互惠排名融合 (Reciprocal Rank Fusion) 将 BGE 模型的密集语义与 SPLADE 的稀疏关键词匹配融合,以合并这两个列表。

from qdrant_client import QdrantClient, models
import os

client = QdrantClient(url=os.getenv("QDRANT_URL"), api_key=os.getenv("QDRANT_API_KEY"))

# For Colab:
# from google.colab import userdata
# client = QdrantClient(url=userdata.get("QDRANT_URL"), api_key=userdata.get("QDRANT_API_KEY"))

response = client.query_points(
    collection_name="products",
    prefetch=[
        models.Prefetch(
            query=dense_vector,
            using="dense_bge",
            limit=20
        ),
        models.Prefetch(
            query=sparse_vector,
            using="sparse_splade",
            limit=20
        )
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),
    limit=10
)

Qdrant 同时发送两个预取,通过互惠排名融合这两个排名列表,并返回您的前十个满足语义和关键词相关性的产品。

密集检索 + ColBERT 重排序

虽然上一课展示了如何直接使用 ColBERT 进行检索,但在生产系统中,重排序是更常见的模式。原因很实际:ColBERT 在整个大型集合上进行暴力 MaxSim 评分可能很慢。相反,您可以结合两种优势——快速近似搜索以缩小候选范围,然后对这个较小的集合进行精确的令牌级评分。

您创建一个包含两个向量字段的集合:一个具有 HNSW 索引以提高速度的密集向量,和一个禁用 HNSW 以提高精度的 ColBERT 多向量。

client.create_collection(
    collection_name="articles",
    vectors_config={
        # Fast HNSW-indexed dense retrieval
        "bge-dense": models.VectorParams(
            size=384,
            distance=models.Distance.COSINE,
        ),
        # Precise multivector reranking (HNSW disabled to save RAM)
        "colbert": models.VectorParams(
            size=128,
            distance=models.Distance.COSINE,
            multivector_config=models.MultiVectorConfig(
                comparator=models.MultiVectorComparator.MAX_SIM
            ),
            hnsw_config=models.HnswConfigDiff(m=0),
        )
    }
)

现在,您使用 HNSW 索引的密集字段快速检索 100 个候选对象,然后应用 ColBERT 的 MaxSim 评分对这 100 个进行重排序,并选择最好的十个。

from fastembed import LateInteractionTextEmbedding, TextEmbedding

# Encode with both models
dense = TextEmbedding("BAAI/bge-small-en-v1.5")
dense_query_vector = next(dense.query_embed(["what is the policy?"])).tolist()

colbert = LateInteractionTextEmbedding("colbert-ir/colbertv2.0")
colbert_query_multivector = next(colbert.query_embed(["what is the policy?"])).tolist()

# Fast retrieval + precise reranking in one call
response = client.query_points(
    collection_name="articles",
    prefetch=[
        models.Prefetch(
            query=dense_query_vector,
            using="bge-dense",
            limit=100
        )
    ],
    query=colbert_query_multivector,
    using="colbert",
    limit=10
)

在幕后,Qdrant 从 HNSW 索引的密集字段中获取 100 个最近的点,然后仅对这 100 个候选对象应用 ColBERT 多向量的 MaxSim 延迟交互分数,返回得分最高的十个结果。这种两阶段方法提供了 ColBERT 的精度,同时保持查询延迟对于大规模部署是实用的。

全局和预取特定过滤器

最后,您在有意义的地方分层添加过滤。在下面的片段中,您在查询级别指定全局过滤器——价格低于 200 美元和发布日期在 2023 年 1 月 1 日之后——这些过滤器会自动传播到所有预取。然后,您在密集搜索上添加一个额外的预取特定过滤器,只检索有库存且属于“夹克”类别的产品。

response = client.query_points(
    collection_name="products",
    prefetch=[
        models.Prefetch(
            query=dense_query_vector,
            using="bge-dense",
            limit=100,
            filter=models.Filter(
                must=[
                    models.FieldCondition(
                        key="in_stock",
                        match=models.MatchValue(value=True)
                    ),
                    models.FieldCondition(
                        key="category",
                        match=models.MatchValue(value="jackets")
                    )
                ]
            )
        ),
        models.Prefetch(
            query=sparse_query_vector,
            using="sparse-splade",
            limit=100
        )
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),
    filter=models.Filter(
        must=[
            models.FieldCondition(
                key="price",
                range=models.Range(lt=200.0)
            ),
            models.FieldCondition(
                key="release_date",
                range=models.DatetimeRange(
                    gte="2023-01-01T00:00:00Z"
                )
            )
        ]
    ),
    limit=10
)

全局过滤器(price 和 release_date)自动应用于两个预取。第一个预取添加了额外的约束(in_stock 和 category),而第二个预取仅使用全局过滤器。这消除了在每个预取中重复通用过滤器的需要。所有过滤都在检索阶段高效地发生——没有单独的后处理步骤。整个管道——混合检索、过滤、融合和重排序——都在一个 API 调用中执行。

通用查询 API 结构

通用查询 API 通过简单的声明性接口实现复杂的搜索模式。

  • 预取阶段:针对不同的向量字段并行执行多个搜索。每个预取都可以有自己的过滤器、限制和使用的向量类型。
  • 融合阶段:使用互惠排名融合 (RRF) 或基于分布的分数融合 (DBSF) 等算法组合来自多个预取的结果。
  • 重排序阶段:或者使用更强的评分器(如 ColBERT)对来自单个预取的候选对象进行重排序。融合和重排序是大多数管道中备选的最后步骤。
  • 过滤:在查询级别全局应用过滤器(传播到所有预取)或添加预取特定过滤器以对单个搜索施加额外约束。

这种架构消除了对多个 API 调用、客户端结果合并和复杂编排代码的需求。所有操作都在服务器端以单个优化请求完成。

下一步

在我们的下一课中,您将把这些构建块组合到一个个性化推荐管道中。您将学习如何从三种不同的向量表示中检索候选对象,融合它们的信号,使用 ColBERT 进行重排序,应用用户分段过滤器,并在不编写一行“粘合代码”的情况下提供高度相关的建议。