在 Elasticsearch 中扩展后期交互模型 - 第 2 部分 - 8.18

news2025/3/22 18:31:33

作者:来自 Elastic Peter Straßer 及 Benjamin Trent

本文探讨了如何优化后期交互向量,以适应大规模生产工作负载,例如减少磁盘空间占用和提高计算效率。

在之前关于 ColPali 的博客中,我们探讨了如何使用 Elasticsearch 创建视觉搜索应用。我们主要关注 ColPali 等模型为应用带来的价值,但与 E5 等双编码器向量搜索相比,它们在性能上存在一定劣势。

基于第 1 部分的示例,本文将探讨如何利用不同技术和 Elasticsearch 强大的向量搜索工具,使后期交互向量适应大规模生产工作负载。

完整代码示例可在 GitHub 上查看。

问题

ColPali 在索引中的每个文档页面会生成 1000 多个向量,这在使用后期交互向量时带来了两个主要挑战:

  1. 磁盘空间:存储所有这些向量会占用大量磁盘空间,在大规模应用时成本高昂。
  2. 计算量:在使用 maxSimDotProduct() 进行文档排序时,需要将每个文档的所有向量与查询的 N 个向量进行比较,计算成本极高。

接下来,我们将探讨一些优化技术,以解决这些问题。

位向量(Bit Vectors)

为了减少磁盘空间占用,我们可以将图像压缩为位向量(bit vectors)。可以使用一个简单的 Python 函数,将多向量转换为位向量:

def to_bit_vectors(embeddings: list) -> list:
    return [
        np.packbits(np.where(np.array(embedding) > 0, 1, 0))
        .astype(np.int8)
        .tobytes()
        .hex()
        for embedding in embeddings
    ]

函数核心概念

该函数的核心逻辑非常简单:

  • 值大于 0 的元素转换为 1
  • 值小于 0 的元素转换为 0

这样,我们就得到了一个 仅包含 0 和 1 的数组,并将其转换为 十六进制字符串 来表示位向量(bit vector)。

在索引映射(index mapping)中,我们需要将 element_type 参数设置为 bit

mappings = {
    "mappings": {
        "properties": {
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)

在将所有新的位向量(bit vectors)写入索引后,我们可以使用以下代码对它们进行排序:

query = "What do companies use for recruiting?"
query_vector = to_bit_vectors(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimInvHamming(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}

通过牺牲少量精度,我们可以使用 Hamming 距离maxSimInvHamming(...))进行排序,该方法能够利用 位掩码(bit-masks)、SIMD 等优化技术。更多关于 位向量Hamming 距离 的信息,请参考我们的博客。

或者,我们也可以 将查询向量转换为位向量,而是直接使用完整精度的后期交互向量进行搜索:

query = "What do companies use for recruiting?"
query_vector = create_col_pali_query_vectors(query)
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}

这将使用非对称相似性函数来比较向量。

让我们考虑两个位向量(bit vectors)之间的常规 Hamming 距离。假设我们有一个文档向量 D

以及一个查询向量 Q

简单的二进制量化将 D 转换为 10101101,将 Q 转换为 11111011。对于 Hamming 距离,我们需要直接的位运算 —— 这是非常快速的。在这种情况下,Hamming 距离01010110,其值为 86。因此,得分就变成了该 Hamming 距离的反值。请记住,更相似的向量 具有 更小的 Hamming 距离,因此反转该值可以使更相似的向量得分更高。在此案例中,得分将为 0.012

然而,需要注意的是,我们失去了每个维度的大小差异。1 就是 1。因此,对于 Q0.010.79 之间的差异消失了。由于我们只是按照 >0 进行量化,我们可以做一个小技巧,即 Q 向量不进行量化。这虽然无法利用极其快速的位运算,但它仍然保持了较低的存储成本,因为 D 向量仍然是量化的。

简而言之,这保留了 Q 中的信息,从而提高了距离估计的质量,并保持了低存储成本。

使用 位向量 可以显著节省磁盘空间和查询时的计算负载。但我们还可以做更多的优化。

平均向量(Average Vectors)

为了在数十万文档的搜索中进行扩展,即使是 位向量 带来的性能提升也不足以满足需求。为了应对这些类型的工作负载,我们需要利用 ElasticsearchHNSW 索引结构进行向量搜索。

由于 ColPali 每个文档会生成大约 一千个向量,这对于添加到我们的 HNSW 图 中来说太多了。因此,我们需要减少向量的数量。为此,我们可以通过对 ColPali 生成的所有文档向量取平均值,来创建文档含义的单一表示。

目前,这在 Elasticsearch 中无法直接实现,因此我们需要在将向量导入 Elasticsearch 之前对其进行预处理。

我们可以使用 LogstashIngest Pipelines 来完成此操作,但在这里我们将使用一个简单的 Python 函数

def to_avg_vector(vectors):
    vectors_array = np.array(vectors)
    
    avg_vector = np.mean(vectors_array, axis=0)
    
    norm = np.linalg.norm(avg_vector)
    if norm > 0:
        normalized_avg_vector = avg_vector / norm
    else:
        normalized_avg_vector = avg_vector

    return normalized_avg_vector.tolist()

我们还对向量进行了归一化,以便使用 点积相似度

在将所有 ColPali 向量转换为平均向量后,我们可以将它们索引到我们的 dense_vector 字段中:

mappings = {
    "mappings": {
        "properties": {
            "avg_vector": {
                "type": "dense_vector",
                "dims": 128,
                "index": True,
                "similarity": "dot_product"
            },
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)

我们必须考虑到,这会增加总的磁盘使用量,因为我们不仅保存了后期交互向量,还保存了更多的信息。此外,我们还需要额外的 RAM 来存储 HNSW 图,从而使我们能够在数十亿个向量中进行扩展搜索。为了减少 RAM 的使用,我们可以利用我们流行的 BBQ 功能。这样,我们就能在庞大的数据集上获得快速的搜索结果,否则是无法实现的。

现在,我们只需使用 KNN 查询 来查找最相关的文档。

query = "What do companies use for recruiting?"
query_vector = to_avg_vector(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "knn": {
        "field": "avg_vector",
        "query_vector": query_vector,
        "k": 10,
        "num_candidates": 100
    },
    "size": 5
}

之前最佳匹配不幸降到了排名第三。

为了解决这个问题,我们可以进行多阶段检索。在第一阶段,我们使用 KNN 查询 在数百万个文档中搜索查询的最佳候选文档。在第二阶段,我们仅对前 k(此处为 10)个文档进行重新排序,使用 ColPali 的高保真度后期交互向量来提高准确度。

query = "What do companies use for recruiting?"
col_pali_vector = create_col_pali_query_vectors(query)
avg_vector = to_avg_vector(col_pali_vector)
es_query = {
  "_source": False,
  "retriever": {
    "rescorer": {
      "retriever": {
        "knn": {
          "field": "avg_vector",
          "query_vector": avg_vector,
          "k": 10,
          "num_candidates": 100
        }
      },
      "rescore": {
        "window_size": 10,
        "query": {
          "rescore_query": {
            "script_score": {
              "query": {
                "match_all": {}
              },
              "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                  "query_vector": col_pali_vector
                }
              }
            }
          }
        }
      }
    }
  },
  "size": 5
}

这里,我们使用在 8.18 版本中引入的 rescore retriever 来对结果进行重新排序。重新评分后,我们看到最佳匹配再次排在第一位。

注意:在生产应用中,我们可以使用比 10 更高的 k 值,因为 max sim 函数仍然相对高效。

Token pooling

Token pooling 通过汇聚冗余信息(如白色背景区域)来减少多向量嵌入的序列长度。这种技术减少了嵌入的数量,同时保留了页面的大部分信息。

我们通过聚类语义相似的向量来减少总的向量数量。

Token pooling 通过使用聚类算法将文档中相似的 token 嵌入分组为簇。然后,计算每个簇中向量的均值,以创建一个单一的聚合表示。这个聚合向量替代该组中的原始 tokens,从而减少总的向量数量,同时几乎不损失文档信号。

ColPali 论文为大多数数据集提出了初始的 pool factor 值为 3,这在减少总向量数量 66.7% 的同时,保留了原始性能的 97.8%。

Source:  https://arxiv.org/pdf/2407.01449

但我们需要小心:Shift 数据集包含非常密集、文本密集且几乎没有空白区域的文档,在 pool factor 增加时性能会迅速下降。

为了创建池化向量(pooled vectors),我们可以使用 colpali_engine 库:

from colpali_engine.compression.token_pooling import HierarchicalTokenPooler

pooler = HierarchicalTokenPooler(pool_factor=3) # test on your data for a good pool_factor

def pool_vectors(embedding: list) -> list:
    tensor = torch.tensor(embedding).unsqueeze(0)
    pooled = pooler.pool_embeddings(tensor)
    return pooled.squeeze(0).tolist()

我们现在有一个其维度减少了大约 66.7% 的向量。我们像往常一样将其索引,并能够使用 maxSimDotProduct() 函数进行搜索。

我们能够获得良好的搜索结果,代价是结果的准确性略有下降。

提示:使用更高的 pool_factor(100-200),你也可以在平均向量方案和我们在这里讨论的方案之间找到一个折衷方案。当每个文档大约有 5-10 个向量时,将它们索引到嵌套字段中并利用 HNSW 索引变得可行。

Coss-encoder 与 late-interaction 和 bi-encoder 的对比

通过我们目前所学的内容,late-interaction 模型(如 ColPali 或 ColBERT)与其他 AI 检索技术相比,处于什么位置呢?

虽然 max sim 函数比 cross-encoders 更便宜,但它仍然需要比使用 bi-encoders 的向量搜索进行更多的比较和计算。在 bi-encoder 中,我们仅需对每个查询-文档对比两个向量。

因此,我们的建议是将 late-interaction 模型一般只用于对前 k 个搜索结果进行重新排序。我们在字段类型的命名中也反映了这一点:rank_vectors

但那 cross-encoder 呢?是否 late interaction 模型因为执行时更便宜而更好?像往常一样,答案是:这取决于情况。 cross-encoders 通常产生更高质量的结果,但它们需要大量计算,因为查询-文档对必须经过完整的 Transformer 模型处理。它们的一个优势是,它们不需要对向量进行索引,并且可以以无状态的方式运行。这带来了以下优势:

  • 使用更少的磁盘空间
  • 系统更简单
  • 更高质量的搜索结果
  • 较高的延迟,因此不能进行深度重新排序

另一方面,late-interaction 模型可以将部分计算卸载到索引时进行,从而使查询变得更便宜。我们为此付出的代价是需要索引向量,这使得我们的索引管道更加复杂,并且需要更多的磁盘空间来保存这些向量。

特别是在 ColPali 的情况下,来自图像的信息分析非常昂贵,因为它们包含大量数据。在这种情况下,折衷更倾向于使用 late-interaction 模型,如 ColPali,因为在查询时评估这些信息会非常耗费资源/缓慢。

对于像 ColBERT 这样的 late-interaction 模型,它处理的主要是文本数据(像大多数 cross-encoders,例如 elastic-rerank-v1),则决策可能会更多地倾向于使用 cross-encoder 来利用磁盘节省和系统简化的优势。

我们鼓励你根据自己的用例权衡这些优缺点,并尝试 Elasticsearch 提供的不同工具,以构建最佳的搜索应用程序。

结论

在这篇博客中,我们探讨了各种优化 late interaction 模型(如 ColPali)的方法,以便在 Elasticsearch 中进行大规模的向量搜索。虽然 late interaction 模型在检索效率和排名质量之间提供了良好的平衡,但它们也带来了与存储和计算相关的挑战。

为了解决这些问题,我们探讨了以下几种方法:

  • 比特向量:通过使用高效的相似性计算(如汉明距离或非对称最大相似度)显著减少磁盘空间。
  • 平均向量:将多个嵌入压缩成一个单一的密集表示,从而通过 HNSW 索引实现高效的检索。
  • 标记池化:智能地合并冗余的嵌入,同时保持语义完整性,减少查询时的计算开销。

Elasticsearch 提供了一个强大的工具包,可以根据你的需求自定义和优化搜索应用程序。无论你是优先考虑检索速度、排名质量还是存储效率,这些工具和技术都使你能够根据实际应用的需求平衡性能和质量。

Elasticsearch 拥有许多新功能,帮助你构建适合你用例的最佳搜索解决方案。深入了解我们的示例笔记本,开始免费的云试用,或者现在就在本地机器上试试 Elastic。

原文:Scaling late interaction models in Elasticsearch - part 2 - Elasticsearch Labs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2319723.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥每日打卡--区间移位

#蓝桥#JAVA#区间移位 题目描述 数轴上有n个闭区间:D1,⋯Dn。 其中区间Di用一对整数[ai,bi]来描述,满足 ai≤bi。 已知这些区间的长度之和至少有。 所以,通过适当的移动这些区间,你总可以使得他们的"并"覆盖 [0,],也…

CUDAOpenCV 基于Hessian矩阵计算特征值

文章目录 一、简介二、实现代码三、实现效果一、简介 基于之前的博客:CUDA&OpenCV Hessain矩阵计算,我们可以计算出每个像素的特征值: 二、实现代码 ComputeHessainMatrix.cuh #ifndef HESSAIN_GPU_CUH #

基于CAMEL 的Workforce 实现多智能体协同工作系统

文章目录 一、workforce 简介1.架构设计2.通信机制 二、workforce 工作流程图示例1.用户角色2.工作流程 三、workforce 中重要函数说明1.__init__函数2.add_single_agent_worker 函数3.add_role_playing_worker 函数4.add_workforce 函数 四、基于workforce实现多智能体协调&am…

PostgreSQL_数据表结构设计并创建

目录 前置: 1 数据表设计思路 2 数据表格SQL 3 创建 3.1 创建数据库 db_stock 3.2 在 pgAdmin4 中创建表 前置: 本博文是一个系列。在本人“数据库专栏”-》“PostgreSQL_”开头的博文 1 数据表设计思路 1 日数据来自优矿,优矿的数据…

如何在MCU工程中启用HardFault硬错误中断

文章目录 一、HardFault出现场景二、启动HardFault三、C代码示例 一、HardFault出现场景 HardFault(硬故障) 错误中断是 ARM Cortex-M 系列微控制器中一个较为严重的错误中断,一旦触发,表明系统遇到了无法由其他异常处理机制解决…

MySQL -- 复合查询

数据库的查询是数据库使用中比较重要的环节,前面的基础查询比较简单,不做介绍,可自行查阅。本文主要介绍复合查询,并结合用例进行讲解。 本文的用例依据Soctt模式的经典测试表,可以自行下载,也可以自己创建…

卷积神经网络 - 卷积层(具体例子)

为了更一步学习卷积神经网络之卷积层,本文我们来通过几个个例子来加深理解。 一、灰度图像和彩色图像的关于特征映射的例子 下面我们通过2个例子来形象说明卷积层中“特征映射”的概念,一个针对灰度图像,一个针对彩色图像。 例子 1&#x…

测试Claude3.7 sonnet画蛋白质

测试Claude3.7 sonnet画蛋白虽然画的很粗糙,但是大致画了出来

java项目之基于ssm的游戏攻略网站(源码+文档)

项目简介 游戏攻略网站实现了以下功能: 管理员主要负责填充图书和其类别信息,并对已填充的数据进行维护,包括修改与删除,管理员也需要审核老师注册信息,发布公告信息,管理自助租房信息等。 💕…

本地基于Ollama部署的DeepSeek详细接口文档说明

前文,我们已经在本地基于Ollama部署好了DeepSeek大模型,并且已经告知过如何查看本地的API。为了避免网络安全问题,我们希望已经在本地调优的模型,能够嵌入到在本地的其他应用程序中,发挥本地DeepSeek的作用。因此需要知…

python NameError报错之导库报错

在日常代码编写中,经常出现如 图1 一样的报错,在代码多时很难找到问题,但翻看代码后就会发现是因为未导库, 图1 报错 代码: time.sleep(0.1) print("time库") 解决方法: 第一步:在代码中添加导库代码 import time #…

Web3网络生态中数据保护合规性分析

Web3网络生态中数据保护合规性分析 在这个信息爆炸的时代,Web3网络生态以其独特的去中心化特性,逐渐成为数据交互和价值转移的新平台。Web3,也被称为去中心化互联网,其核心理念是将数据的控制权归还给用户,实现数据的…

C++ 语法之数组指针

一维数组: 如果我们定义了一个一维数组,那么这个数组名,就是指向第一个数组元素的地址,也即,是整个数组分配的内存空间的首地址。 比如 int a[3]; 定义了一个包含三个元素的数组。因为一个int占4个字节,那…

PLY格式文件如何转换成3DTiles格式——使用GISBox软件实现高效转换

一、概述 在三维GIS和数字孪生领域,3DTiles格式已成为主流的数据格式之一。它由Cesium团队提出,专为大规模3D数据可视化设计,能够高效地加载和展示海量模型数据。而PLY格式则是一种常见的三维模型文件格式,主要用于存储点云数据或…

Java定时任务的三重境界:从单机心跳到分布式协调

《Java定时任务的三重境界:从单机心跳到分布式协调》 本文将以生产级代码标准,揭秘Java定时任务从基础API到分布式调度的6种实现范式,深入剖析ScheduledThreadPoolExecutor与Quartz Scheduler的线程模型差异,并给出各方案的性能压…

【Linux网络】手动部署并测试内网穿透

📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…

java项目之在线购物系统(源码+文档)

项目简介 在线购物系统实现了以下功能: 使用在线购物系统的用户分管理员和用户两个角色的权限子模块。 管理员所能使用的功能主要有:主页、个人中心、用户管理、商品分类管理、商品信息管理、系统管理、订单管理等。 用户可以实现主页、个人中心、我的…

OO_Unit1

第一次作业 UML类图 代码复杂度分析 其中Expr中的toString方法认知复杂度比较高,主要源于多层条件嵌套和分散的字符串处理逻辑,重构时可重点关注这两部分的解耦。 代码量分析 1.”通用形式“ 我觉得我的设计的最大特点就是“通用形式”,具…

重要重要!!fisher矩阵元素有什么含义和原理; Fisher 信息矩阵的形式; 得到fisher矩阵之后怎么使用

fisher矩阵元素有什么含义和原理 目录 fisher矩阵元素有什么含义和原理一、对角线元素( F i , i F_{i,i} Fi,i​)的含义与原理二、非对角线元素( F i , j F_{i,j} Fi,j​)的含义与原理Fisher 信息矩阵的形式矩阵的宽度有位置权重数量决定1. **模型参数结构决定矩阵维度**2.…

[已解决]jupyter notebook报错 500 : Internal Server Error及notebook闪退

jupyter notebook出现如上图的报错,可以在黑色窗口中检查是为什么报错。 我检查发现是nbconvert导致的问题,卸载重装nbconvert。 但是这时候出现,jupyter notebook闪退问题。jupyter的黑色窗口出现一秒钟就没了。 在Anaconda Prompt中检查ju…