使用 Qdrant 扩展 PDF 检索规模

scaling-pdf-retrieval-qdrant

时长:30 分钟难度:中等输出:GitHubOpen In Colab

高效的 PDF 文档检索是诸如 (agentic) 检索增强生成 (RAG) 以及许多其他基于搜索的应用中的常见需求。同时,设置 PDF 文档检索几乎总是伴随着额外的挑战。

许多传统的 PDF 检索解决方案依赖于 光学字符识别 (OCR),并结合特定用例的启发式方法来处理诸如表格、图像和图表等视觉复杂元素。这些算法通常不可迁移——即使在同一领域内——因为它们的解析和分块策略是针对任务定制的,劳动密集、容易出错且难以扩展。

视觉大语言模型 (VLLM) 的最新进展,例如 ColPali 及其后继者 ColQwen,开始改变 PDF 检索的方式。这些多模态模型直接将 PDF 页面作为输入,无需预处理。任何可以转换为图像的内容(可以将 PDF 视为文档页面的截图)都可以被这些模型有效处理。VLLM 使用起来简单得多,并在 PDF 检索基准测试(例如 视觉文档检索 (ViDoRe) 基准)中取得了最先进的性能。

VLLM 如何用于 PDF 检索

ColPaliColQwen 这样的 VLLM 会为每个 PDF 页面生成多向量表示;这些表示存储并索引在向量数据库中。在检索过程中,模型会为(文本)用户查询动态创建多向量表示,并通过后期交互机制实现 PDF 页面与查询之间的精确匹配。

扩展 VLLM 的挑战

VLLM 生成的庞大多向量表示使得大规模 PDF 检索计算密集。如果未经优化使用,这些模型对于大规模 PDF 检索任务效率低下。

扩展背后的数学原理

ColPali 每个 PDF 页面生成超过 1,000 个向量,而其后继者 ColQwen 生成略少一些——根据图像大小动态调整,最多 768 个向量。通常,ColQwen 每个页面生成约 700 个向量

为了理解其影响,考虑构建 HNSW 索引,这是一种常见的向量数据库索引算法。让我们粗略估计将一个新的 PDF 页面插入索引所需的比较次数。

  • 每页向量数:约 700 (ColQwen) 或约 1,000 (ColPali)
  • ef_construct100 (默认值)

向量比较次数的下限估计为

$$ 700 \times 700 \times 100 = 49 \ \text{百万} $$

现在想象一下在 20,000 页上构建索引需要多长时间!

对于 ColPali,这个数字翻倍。结果是索引构建时间极慢

我们的解决方案

我们建议在第一阶段检索中减少 PDF 页面表示中的向量数量。在第一阶段使用减少的向量数量检索后,我们建议使用原始未压缩的表示对检索到的子集进行重新排序

向量数量的减少可以通过对 VLLM 生成的多向量输出应用均值池化操作来实现。均值池化将选定子组中所有向量的值进行平均,将多个向量压缩成一个代表性向量。如果做得好,它可以在显著减少向量数量的同时保留原始页面的重要信息。

VLLM 生成对应于 PDF 页面不同部分的补丁(patch)的向量。这些补丁可以按 PDF 页面的列和行进行分组。

例如

  • ColPali 将 PDF 页面划分为 1,024 个补丁
  • 对这个补丁矩阵的行(或列)应用均值池化,将页面表示减少到只有 32 个向量

ColPali patching of a PDF page

我们使用 ColPali 模型测试了这种方法,通过 PDF 页面行对其多向量进行均值池化。结果显示

  • 索引时间快一个数量级
  • 检索质量与原始模型相当

有关此实验的详细信息,请参阅我们的 GitHub 存储库ColPali 优化博客文章网络研讨会“大规模 PDF 检索”

本教程的目标

在本教程中,我们将演示一种使用 QdrantColPaliColQwen2 VLLM 进行可扩展 PDF 检索的方法。强烈推荐使用这种方法,以避免索引时间长和检索速度慢等常见问题。

在以下部分中,我们将演示一种源于我们成功实验的优化检索算法。

使用均值池化向量进行第一阶段检索

  • 使用均值池化向量构建 HNSW 索引。
  • 将它们用于第一阶段检索。

使用原始模型多向量进行重新排序

  • 使用 ColPali 或 ColQwen2 的原始多向量在第一阶段检索到的结果进行重新排序

设置

安装和导入所需库

# pip install colpali_engine>=0.3.1
from colpali_engine.models import ColPali, ColPaliProcessor
# pip install qdrant-client>=1.12.0
from qdrant_client import QdrantClient, models

为了运行这些实验,我们使用 Qdrant 集群。如果您刚开始使用,可以设置一个免费层级集群进行测试和探索。请按照文档 “如何创建免费层级 Qdrant 集群” 中的说明操作。

client = QdrantClient(
    url=<YOUR CLUSTER URL>,
    api_key=<YOUR API KEY>
)

下载 ColPali 模型及其输入处理器。请确保选择适合您设置的后端。

colpali_model = ColPali.from_pretrained(
        "vidore/colpali-v1.3",
        torch_dtype=torch.bfloat16,
        device_map="mps",  # Use "cuda:0" for GPU, "cpu" for CPU, or "mps" for Apple Silicon
    ).eval()

colpali_processor = ColPaliProcessor.from_pretrained("vidore/colpali-v1.3")
对于 ColQwen 模型
from colpali_engine.models import ColQwen2, ColQwen2Processor

colqwen_model = ColQwen2.from_pretrained(
        "vidore/colqwen2-v0.1",
        torch_dtype=torch.bfloat16,
        device_map="mps", # Use "cuda:0" for GPU, "cpu" for CPU, or "mps" for Apple Silicon
    ).eval()

colqwen_processor = ColQwen2Processor.from_pretrained("vidore/colqwen2-v0.1")

创建 Qdrant 集合

现在我们可以在 Qdrant 中创建一个集合,用于存储由 ColPaliColQwen 生成的 PDF 页面的多向量表示。

集合将包含按行和列均值池化的 PDF 页面表示,以及原始多向量表示。

client.create_collection(
    collection_name=collection_name,
    vectors_config={
        "original": 
            models.VectorParams( #switch off HNSW
                    size=128,
                    distance=models.Distance.COSINE,
                    multivector_config=models.MultiVectorConfig(
                        comparator=models.MultiVectorComparator.MAX_SIM
                    ),
                    hnsw_config=models.HnswConfigDiff(
                        m=0 #switching off HNSW
                    )
            ),
        "mean_pooling_columns": models.VectorParams(
                size=128,
                distance=models.Distance.COSINE,
                multivector_config=models.MultiVectorConfig(
                    comparator=models.MultiVectorComparator.MAX_SIM
                )
            ),
        "mean_pooling_rows": models.VectorParams(
                size=128,
                distance=models.Distance.COSINE,
                multivector_config=models.MultiVectorConfig(
                    comparator=models.MultiVectorComparator.MAX_SIM
                )
            )
    }
)

选择一个数据集

在本教程中,我们将使用 Daniel van Strien 的 UFO 数据集。该数据集可在 Hugging Face 上获取;您可以直接从那里下载。

from datasets import load_dataset
ufo_dataset = "davanstrien/ufo-ColPali"
dataset = load_dataset(ufo_dataset, split="train")

嵌入和均值池化

我们将使用一个函数,该函数以批处理方式生成每个 PDF 页面(即图像)的多向量表示及其均值池化版本。为了完全理解,重要的是要考虑 ColPaliColQwen 的以下具体细节:

ColPali:理论上,ColPali 设计用于每个 PDF 页面生成 1,024 个向量,但实际上它会生成 1,030 个向量。这种差异是由于 ColPali 的预处理器会在每个输入前附加文本 <bos>Describe the image.。这段额外文本会生成额外的 6 个多向量。

ColQwen:ColQwen 根据 PDF 页面的大小动态确定“行和列”中补丁的数量。因此,多向量的数量在不同输入之间可能有所不同。ColQwen 预处理器会在前面加上 <|im_start|>user<|vision_start|>,并在后面加上 <|vision_end|>Describe the image.<|im_end|><|endoftext|>

例如,ColQwen 多向量输出就是这样形成的。

that’s how ColQwen multivector output is formed

get_patches 函数用于获取 ColPali/ColQwen2 模型将 PDF 页面划分成的 x_patches(行)和 y_patches(列)的数量。对于 ColPali,这些数字总是 32x32;ColQwen 将根据 PDF 页面大小动态定义它们。

x_patches, y_patches = model_processor.get_n_patches(
    image_size, 
    patch_size=model.patch_size
)
对于 ColQwen 模型
model_processor.get_n_patches(
    image_size, 
    patch_size=model.patch_size,
    spatial_merge_size=model.spatial_merge_size
)

我们选择保留前缀和后缀多向量。我们的池化操作根据模型确定的行和列数量(ColPali 固定为 32x32,ColQwen 动态为 XxY)压缩代表图像标记的多向量。该函数保留并重新整合模型产生的额外多向量到池化表示中。

适用于 ColPali 模型的简化版池化

(完整版——也适用于 ColQwen——请参阅教程笔记本


processed_images = model_processor.process_images(image_batch) 
# Image embeddings of shape (batch_size, 1030, 128)
image_embeddings = model(**processed_images)

# (1030, 128)
image_embedding = image_embeddings[0] # take the first element of the batch

# Now we need to identify vectors that correspond to the image tokens
# It can be done by selecting tokens corresponding to special `image_token_id`

# (1030, ) - boolean mask (for the first element in the batch), True for image tokens 
mask = processed_images.input_ids[0] == model_processor.image_token_id

# For convenience, we now select only image tokens 
#   and reshape them to (x_patches, y_patches, dim)

# (x_patches, y_patches, 128)
image_patch_embeddings = image_embedding[mask].view(x_patches, y_patches, model.dim)

# Now we can apply mean pooling by rows and columns

# (x_patches, 128)
pooled_by_rows = image_patch_embeddings.mean(dim=0)

# (y_patches, 128)
pooled_by_columns = image_patch_embeddings.mean(dim=1)

# [Optionally] we can also concatenate special tokens to the pooled representations, 
# For ColPali, it's only postfix

# (x_patches + 6, 128)
pooled_by_rows = torch.cat([pooled_by_rows, image_embedding[~mask]])

# (y_patches + 6, 128)
pooled_by_columns = torch.cat([pooled_by_columns, image_embedding[~mask]])

上传到 Qdrant

上传过程很简单;唯一需要注意的是 ColPali 和 ColQwen2 模型的计算成本。在资源有限的环境中,建议使用较小的批处理大小进行嵌入和均值池化。

完整的上传代码可在教程笔记本中找到。

查询 PDF

索引 PDF 文档后,我们可以使用我们的两阶段检索方法来查询它们。

query = "Lee Harvey Oswald's involvement in the JFK assassination"
processed_queries = model_processor.process_queries([query]).to(model.device)

# Resulting query embedding is a tensor of shape (22, 128)
query_embedding = model(**processed_queries)[0]

现在让我们设计一个函数,使用 VLLM 生成的多向量进行两阶段检索。

  • 步骤 1:使用压缩的多向量表示和 HNSW 索引进行预取结果。
  • 步骤 2:使用原始多向量表示对预取的结果进行重新排序。

让我们使用组合的均值池化表示来查询我们的集合,进行第一阶段检索。

# Final amount of results to return
search_limit = 10
# Amount of results to prefetch for reranking
prefetch_limit = 100

response = client.query_points(
    collection_name=collection_name,
    query=query_embedding,
    prefetch=[
        models.Prefetch(
            query=query_embedding,
            limit=prefetch_limit,
            using="mean_pooling_columns"
        ),
        models.Prefetch(
            query=query_embedding,
            limit=prefetch_limit,
            using="mean_pooling_rows"
        ),
    ],
    limit=search_limit,
    with_payload=True,
    with_vector=False,
    using="original"
)

并检查我们查询“Lee Harvey Oswald 卷入 JFK 刺杀事件”的最佳检索结果。

dataset[response.points[0].payload['index']]['image']

Results, ColPali

结论

在本教程中,我们演示了一种使用 Qdrant 进行大规模 PDF 检索的优化方法,该方法使用生成庞大多向量表示的 VLLM,如 ColPaliColQwen2

如果没有这种优化,检索系统的性能可能会严重下降,无论是索引时间还是查询延迟,特别是随着数据集规模的增长。

我们强烈建议在您的工作流程中实施此方法,以确保高效且可扩展的 PDF 检索。忽略优化检索过程可能导致性能慢得令人无法接受,从而影响系统的可用性。

立即开始扩展您的 PDF 检索!

本页面有用吗?

感谢您的反馈!🙏

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