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

作者:来自 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 / normelse:normalized_avg_vector = avg_vectorreturn 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 HierarchicalTokenPoolerpooler = HierarchicalTokenPooler(pool_factor=3) # test on your data for a good pool_factordef 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.mzph.cn/bicheng/74143.shtml

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

相关文章

JAVA泛型的作用

‌1. 类型安全(Type Safety)‌ 在泛型出现之前,集合类(如 ArrayList、HashMap)只能存储 Object 类型元素,导致以下问题: ‌问题‌:从集合中取出元素时,需手动强制类型转…

深入理解 JavaScript/TypeScript 中的假值(Falsy Values)与逻辑判断 ✨

🕹️ 深入理解 JavaScript/TypeScript 中的假值(Falsy Values)与逻辑判断 在 JavaScript/TypeScript 开发中,if (!value) 是最常见的条件判断之一。它看似简单,却隐藏着语言的核心设计逻辑,也是许多开发者…

【AI速读】30分钟搭建持续集成:用Jenkins拯救你的项目

每个开发者都踩过的坑 你有没有这样的经历?花了一周时间改代码,自信满满准备提交,结果合并同事的更新后,项目突然编译失败,测试跑不通。你焦头烂额地排查问题,老板还在催进度……但明明不是你的错! 这种“集成地狱”几乎每个团队都遇到过。传统的手动集成方式(比如每周…

doris:负载均衡

用户通过 FE 的查询端口(query_port,默认 9030)使用 MySQL 协议连接 Doris。当部署多个 FE 节点时,用户可以在多个 FE 之上部署负载均衡层来实现 Doris 查询的高可用。 本文档介绍多种适用于 Doris 的负载均衡方案,并…

【大语言模型_6】mindie启动模型错误整理

一、启动报 [hccl_runner.cpp:141] AllGatherHcclRunner:0 HcclCommInitRootInfo fa il, error:2, rank:0, rankSize:2 背景:运行DeepSeek-R1-Distill-Qwen-14B模型,在2张300 P卡可以运行,单独一张启动报以上错误。 问题分析&…

dcat-admin已完成项目部署注意事项

必须 composer update 更新项目php artisan admin:publish 发布dcatadmin的静态资源手动创建目录(如果没有) storage/appstorage/framework/cachestorage/framework/sessionsstorage/framework/views 需检查 php不要禁用以下函数 putenvsymlinkproc_…

【计算机网络】网络简介

文章目录 1. 局域网与广域网1.1 局域网1.2 广域网 2. 路由器和交换机3. 五元组3.1 IP和端口3.2 协议3.3 协议分层 4. OSI七层网络协议5. TCP/IP五层模型5.1 TCP/IP模型介绍5.2 网络设备所在分层 6. 封装与分用6.1 数据包的称谓6.2 封装6.3 分用 1. 局域网与广域网 1.1 局域网 …

在QT中进行控件提升操作

目录 一、概述 二、功能需求 三、提升操作 1)拖入标准控件 2)自定义类 3)提升控件 一、概述 QT中提供的标准控件能够满足我们大多数情况下的功能需求,但是在一些特殊应用场合,我们可能需要对控件的功能进行扩展&am…

如何自定义知行之桥Webhook端口返回的Response消息

一、Webhook端口功能概述 知行之桥的Webhook端口提供灵活的消息响应机制,支持用户通过修改配置文件自定义返回的消息体内容,能够查看是否调用接口成功、数据是否推送成功以及自定义返回给用户端的响应内容。 本指南将详解如何通过脚本配置实现以下需求…

pnpm config set ignore-workspace-root-check true

异常 ERR_PNPM_ADDING_TO_ROOT  Running this command will add the dependency to the workspace root, which might not be what you want - if you really meant it, make it explicit by running this command again with the -w flag (or --workspace-root). If you don…

【iOS】SwiftUI 路由管理(NavigationStack)

QDRouter.swift import SwiftUIMainActor class QDRouter: ObservableObject {Published var path NavigationPath()static let main QDRouter() // 单例private init() {}func open(_ url: String) {guard let url URL(string: url) else {return}UIApplication.shared.op…

蓝桥杯学习-13回溯

13回溯 一、回溯1 例题1–递归实现排列型枚举-蓝桥19684 1.递归可以解决不定次数的循环问题 2.使用数组来标记数字是否被选过import java.util.Scanner;public class Main {static int n;static boolean[] st new boolean[10]; //判断数字是否被选过static int[] path ne…

【IDEA中配置Maven国内镜像源】

1. 为什么需要配置国内镜像源? 首先,Maven本身的工作原理是通过从仓库中下载依赖包。而这些依赖通常来自于 Maven中央仓库(位于国外),由于网络原因,我们在国内访问这些远程仓库的速度比较慢,甚至…

【QA】观察者模式在QT有哪些应用?

1. 信号与槽机制 Qt的**信号与槽(Signals & Slots)**是观察者模式的典型实现,通过元对象系统(Meta-Object System)实现松耦合通信。 核心特点: 类型安全:编译时检查参数匹配跨线程支持&…

uniapp中的路由、本地存储与网络请求

navigator 在UniApp中,navigator 组件用于页面跳转和应用内导航。 基本使用 属性: url: 需要跳转的目标页面路径,路径可以是相对路径或绝对路径。open-type: 跳转的方式,默认为 navigateTo。其他可选值包括:redirec…

python3使用lxml解析xml时踩坑记录

文章目录 你的 XML 数据解析 XML----------------------------1. 获取 mlt 根元素的属性--------------------------------------------------------2. 获取 chain 元素的属性--------------------------------------------------------3. 获取所有 property 的值-------------…

【DeepSeek 学c++】dynamic_cast 原理

用于向下转化。 父类引用指向指类对象 假设父亲是a, 子类是b. B* pb new B; 子类对象 A* pa 父类引用指向子类对象, 那么向上转化 Apa pb 这个是自动完成的,隐式转化,不需要dynamic_cast 向下转化指的是 A pa new B。 这个是指向子类对象…

c++ 数组索引越界检查

用 c 编写了一些程序&#xff0c;发现 c 不会自动检查数组的索引越界问题。有时候程序运行错误&#xff0c;提示的错误信息莫名其妙&#xff0c;但很可能是某个数组越界的问题。 例如&#xff1a; #include <iostream>int main() {double arr[5] {1.1, 2.2, 3.3, 4.4,…

Touch Diver:Weart为XR和机器人遥操作专属设计的触觉反馈动捕手套

在虚拟现实&#xff08;VR&#xff09;和扩展现实&#xff08;XR&#xff09;领域&#xff0c;触觉反馈技术正逐渐成为提升沉浸感和交互体验的重要因素。Weart作为这一领域的创新者&#xff0c;凭借其TouchDIVER Pro和TouchDIVER G1触觉手套&#xff0c;为用户带来了高度逼真的…

基于deepseek的智能语音客服【第二讲】后端异步接口调用封装

本篇内容主要讲前端请求&#xff08;不包含&#xff09;访问后端服务接口&#xff0c;接口通过检索知识库&#xff0c;封装提示词&#xff0c;调用deepseek的&#xff0c;并返回给前端的全过程&#xff0c;非完整代码&#xff0c;不可直接运行。 1.基于servlet封装异步请求 为…