Hugging Face Blog

Multi-Vector (Late Interaction) Embedding Models with Sentence Transformers

8.5内容质量

TL;DR · AI 摘要

Sentence Transformers v6.0新增MultiVectorEncoder模型,支持ColBERT风格的晚期交互检索,提升视觉文档检索效果。

核心要点

  • MultiVector模型保留每个token的向量,避免信息压缩损失。
  • MaxSim操作符在视觉文档检索中直接匹配图像,无需OCR步骤。
  • 支持PyLate和colpali-engine模型,兼容现有API。

结构提纲

按章节快速跳转。

  1. 介绍Sentence Transformers v6.0新增MultiVectorEncoder模型及其应用场景。

  2. 解释多向量模型与传统嵌入模型在信息保留和检索精度上的差异。

  3. 描述MaxSim如何通过token级匹配提升视觉文档检索效果。

  4. 涵盖模型加载、编码、评分及在搜索栈中的集成步骤。

  5. 演示如何无需OCR直接匹配图像与文本查询。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Multi-Vector模型与Sentence Transformers
    • 核心机制
      • MaxSim操作符
      • token级向量保留
    • 应用场景
      • 视觉文档检索
      • 语义搜索
    • 技术实现
      • 兼容PyLate/colpali-engine
      • 支持多种模型格式

金句 / Highlights

值得收藏与分享的关键句。

#Sentence Transformers#多向量模型#ColBERT#语义搜索
打开原文

使用 Sentence Transformers 的多向量(晚期交互)嵌入模型

返回文章列表

[-1

]

[0

2026年8月18日发布

GitHub 上的更新

点赞

76

[

  • +70

Tom Aarsen

tomaarsen

关注

Antoine Chaffin

NohTow

lightonai

Raphael Sourty

raphaelsty

Sentence Transformers 是一个用于使用和训练嵌入模型及重排序模型的 Python 库,适用于检索增强生成、语义搜索等应用场景。随着 v6.0 版本更新,该库新增了第四种模型类型:MultiVectorEncoder,用于实现 ColBERT 风格的晚期交互检索。任何 PyLate 检查点和任何 Stanford-NLP ColBERT 检查点都可以直接加载到该模型中,同时通过相同的 API 也可以使用 colpali-engine 模型进行视觉文档检索,该 API 与您目前用于密集型、稀疏型和重排序模型的 API 保持一致。

常规的嵌入模型会将整个文本压缩成一个向量,而多向量模型则为每个标记保留一个向量,并使用 MaxSim 操作符对查询和文档进行评分。这种方式保留了单个向量需要平均化的标记级匹配信息,通常意味着在索引体积增大的代价下实现更强的检索效果。它也是视觉文档检索的最先进方法,其中文本查询可以直接与页面图像进行匹配,无需中间的 OCR 步骤。

在本文中,我们将向您展示如何使用这些模型:加载各种检查点格式、编码和评分、将其集成到搜索系统中、在页面图像上运行模型,并保持索引的经济性。以下所有内容都可以通过简单的 pip install -U sentence-transformers 命令实现。

目录

  • 什么是多向量模型?MaxSim 操作符的优势与代价
  • 安装
  • 加载模型并检查检查点配置
  • 编码查询和文档
  • 使用 MaxSim 进行评分:分数幅度与 MeanMaxSim
  • 语义搜索
  • 检索与重排序
  • 索引构建
  • 视觉文档检索
  • 音频检索
  • 视频检索
  • 可解释性
  • 标记池化
  • 加速推理
  • 模型评估
  • 来自 PyLate 或 colpali-engine 的迁移
  • 支持的模型
  • 致谢
  • 其他资源

什么是多向量模型?

密集型嵌入模型会读取文本并返回一个固定大小的单一向量。模型所捕捉到的所有信息都必须压缩到 384、768 或 1024 这些数字中,相似性则通过两个摘要之间的点积计算得出。这种方法效果显著,但压缩过程存在特定的损耗:罕见实体、精确标识符或长段落中的关键条款都必须在同一个向量中争夺空间。同时包含多个要求的查询也会遇到同样的问题。对于“带木质腿和圆润坐垫的绿色沙发”这样的查询,单个向量必须将这四个要素融合成一个点,因此带有错误腿型的绿色沙发最终会与您实际查询的沙发接近。

多向量模型(也称为晚期交互模型或 ColBERT 风格模型,得名于 ColBERT 论文)跳过了这种压缩过程。它运行相同的变压器模型,但不是将标记嵌入池化为一个向量,而是将每个标记嵌入投影到一个小维度(经典值为 128)并保留所有结果。一个包含 9 个标记的文档会变成一个 9x128 的矩阵,而不是 1x128 的向量。

查询与文档之间的交互被推迟到评分阶段,这也就是“晚期交互”名称的由来。交叉编码器(cross-encoder)则在早期进行交互:两个文本会同时通过模型处理,这种方式虽然准确,但无法进行预计算,因为每个文档都需要针对每个新查询重新编码。双编码器(bi-encoder)则几乎不进行交互(仅在两个完成的摘要之间进行一次点积运算),这正是它能够一次性对文档集合进行编码并快速查询的原因。晚期交互处于两者之间:文档仍然可以独立编码并离线索引,但评分时会将每个查询词与每个文档词进行比较,这为两者提供了更广泛的交互空间。

MaxSim 操作符

评分使用 MaxSim:对每个查询词,取其与任意文档词的最高相似度,然后将这些最大值在查询中求和。

MaxSim ( Q , D ) = ∑ Q i ∈ Q max ⁡ D j ∈ D Q i ⋅ D j MaxSim ( Q , D ) = ∑ Q i ∈ Q max ⁡ D j ∈ D Q i ⋅ D j

由于词嵌入是 L2 归一化的,每个点积结果都是 [-1, 1] 范围内的余弦相似度,因此整个求和结果落在 [-num_query_tokens, num_query_tokens] 范围内。

可以将该操作符理解为一种软对齐:每个查询词都会指向最能解释它的文档词,而评分则表示文档对查询的整体支持程度。

这种对齐不一定是词汇层面的,因为词嵌入是上下文相关的。使用 lightonai/mLateOn 对 "Where do penguins live?" 与 "Penguins inhabit Antarctica." 进行编码时,查询词 "live" 会与 "inhabit"(相似度 0.94)匹配,尽管两者没有共同字符!这是词汇检索无法实现的,BM25 及其变体需要完全匹配术语,因此同义词和改写表达会漏掉。密集嵌入模型当然也能弥补这一差距。晚期交互的额外优势在于,它不会放弃另一个方向:当精确匹配至关重要时(如产品代码、姓氏、函数名),MaxSim 仍然保留该词独立存在,而单向量模型则不得不将其与其他内容平均混合。此外,这种对应关系也不是一对一的,因为多个查询词通常会匹配到同一个文档词。

你获得的收益与付出的成本

你获得的收益是检索质量的提升,特别是在以下场景:当文档中的某个特定部分决定其相关性时,如上述沙发案例中需要满足多个条件的查询,以及在领域外数据上,密集模型的压缩可能针对不同分布进行优化。这种压缩是通过训练查询学习的,模型会保留训练查询需要的信息并丢弃其他内容,这可能恰好包含你生产查询所关注的内容。随着文档长度增加,这种效果会更加明显,因为更多文本需要压缩到相同长度的固定向量中。

付出的成本是索引大小。每个词对应一个向量而非每个文档对应一个向量,这会显著增加向量数量,虽然维度减小部分抵消了这一增长。使用 lightonai/LateOn 对 4,874 个自然问题(Natural Questions)段落进行编码,产生了 608,414 个词向量,平均每段落 124.8 个:

表示方式 | 向量数 | 维度 | float32 大小 ---|---|---|--- 密集嵌入,all-MiniLM-L6-v2 | 4,874 | 384 | 7.5 MB 密集嵌入,gte-modernbert-base | 768 | 15.0 MB 多向量,LateOn | 608,414 | 128 | 311.5 MB

这大约是MiniLM索引存储空间的42倍,即每段约62 KiB。然而,索引通常会被压缩,例如,同样的608,414个向量以fast-plaid索引形式存储时仅需92 MB,因为PLAID存储的是每个向量的质心ID和量化残差,而不是向量本身。以规模参考,像Qwen3-Embedding-8B这样的4096维密集模型,处理相同的4,874段内容需要约80 MB,因此压缩后的多向量索引与人们目前使用的密集索引处于同一量级。Token Pooling在任何操作之前就减少了向量数量,而Retrieve and Rerank则完全避免了构建索引的过程。

PyLate在本文中多次出现,简要说明:Sentence Transformers处理了密集和稀疏模型,但未支持晚期交互,因此LightOn在其基础上构建了PyLate以弥补这一差距,增加了这些模型所需的训练、推理和检索组件。下面加载的许多模型都是使用PyLate训练的,LightOn也围绕它构建了生态系统,包括fast-plaid——在索引部分出现的晚期交互索引。从v6.0版本开始,这些功能已直接集成到Sentence Transformers本身中。

考虑到这些权衡,现在让我们启动一个模型。

安装

多向量模型可以通过普通安装方式使用:

code
pip install -U sentence-transformers

对于ColPali风格的视觉文档检索,还需要安装图像依赖项(详见安装部分的所有附加功能,以及多模态嵌入与重排序模型部分的多模态支持):

code
pip install -U
"sentence-transformers[image]"

Sentence Transformers v6.0需要transformers v5.x、torch 2.2+和huggingface-hub v1.x。如果将这些版本锁定在更低版本,请先规划升级。完整的重大变更列表请参阅迁移指南。

加载模型

加载多向量模型的方式与加载其他Sentence Transformers模型完全相同:

code
from
sentence_transformers
import
MultiVectorEncoder

model = MultiVectorEncoder(
"lightonai/LateOn"
)

要查找可用模型,请在Hub上搜索带有multi-vector和sentence-transformers标签的模型。任何带有这些标签的模型都可以通过上述代码加载,无论其最初是作为PyLate检查点、斯坦福NLP ColBERT检查点,还是用于视觉文档检索的ColPali家族模型。我们正在推动生态系统完善,为所有兼容模型添加该标签,因此可用模型列表将持续增长。

在底层,MultiVectorEncoder会读取这些年这些检查点发布的各种格式,因此即使尚未添加标签,PyLate和斯坦福NLP检查点也能直接加载:

code
from
sentence_transformers
import
MultiVectorEncoder
# 原生Sentence Transformers检查点。PyLate基于相同架构,
# 因此任何PyLate检查点都能以相同方式加载
model = MultiVectorEncoder(
"lightonai/LateOn"
)
model = MultiVectorEncoder(
"mixedbread-ai/mxbai-edge-colbert-v0-17m"
)
model = MultiVectorEncoder(
"LiquidAI/LFM2.5-ColBERT-350M"
, trust_remote_code=
True
)
# 任何斯坦福NLP ColBERT检查点,通过`HF_ColBERT`架构
# 标记进行检测。内联投影权重和配方来自`artifact.metadata`
model = MultiVectorEncoder(
"colbert-ir/colbertv2.0"
)
model = MultiVectorEncoder(
"answerdotai/answerai-colbert-small-v1"
)
# 一个纯Transformer模型:会追加一个全新的随机投影,因此需要训练
model = MultiVectorEncoder(
"answerdotai/ModernBERT-base"
)

视觉文档检索模型是例外。ColPali家族的检查点以colpali-engine自己的格式发布,这种格式不包含Sentence Transformers可用的任何信息,因此每个检查点在加载前都需要在其仓库中添加一个小配置。大部分工作已经完成并等待合并。请查看支持的模型以了解当前状态和如何今天加载它们。

检查检查点的配置

多向量模型携带一些根据检查点而变化的配方旋钮:查询和文档的标记前缀、长度限制、查询是否用[MASK]标记填充,以及在评分文档时跳过哪些标记。所有这些都位于模块配置中,因此print(model)会准确显示你加载的内容。以下是原始的ColBERTv2检查点,它将每个查询精确填充到32个标记,并将文档截断到180个:

code
from
sentence_transformers
import
MultiVectorEncoder

model = MultiVectorEncoder(
"colbert-ir/colbertv2.0"
)
print
(model)
"""
MultiVectorEncoder(
(0): Transformer({..., 'document_length': 180,
'query_expansion': {'strategy': 'fixed', 'attend': False, 'token': None, 'length': 32}})
(1): Dense({'in_features': 768, 'out_features': 128, 'bias': False, ...})
(2): MultiVectorMask({'skiplist_words': ['!', '"', '#', ...], 'skiplist_tasks': ['document'], ...})
(3): Normalize({...})
)
"""
print
(model.prompts)
# {'query': '[unused0] ', 'document': '[unused1] '}

这就是经典的ColBERT流程:一个生成上下文化标记嵌入的Transformer,一个将每个标记投影到128维的标记级Dense层,一个决定评分时哪些标记有效的MultiVectorMask,以及一个标记级Normalize。其他检查点会填充不同的值。lightonai/GTE-ModernColBERT-v1使用相同的四个模块,但使用[Q]和[D]提示,没有查询扩展,长度限制为48和300。

你很少需要接触这些配置,因为每个发布的检查点都会自行配置。当你从一个裸骨干构建模型时(这在创建自定义模型中会涉及),这一点才变得重要。

有一个值值得你根据自己的数据进行检查。document_length会截断内容,因此超出部分永远不会进入索引。例如,LateOn的300标记限制下,一个662标记的段落会返回273个向量,其余部分直接丢失。这些检查点大多是在短段落上训练的,因此如果你的块长度超过限制,可以通过encode_document(..., processing_kwargs={"text": {"max_length": 512}})单次调用提升限制,但要注意你将运行超出模型训练时的长度,且索引大小大致成比例增长。多向量模型通常能很好地容忍这一点。在MLDR(一个长文档检索基准测试)上,上述模型的多语言变体清楚地展示了差距:mLateOn得分77.92,而mDenseOn仅得51.59。

编码查询和文档

多向量模型是不对称的:查询和文档会经过不同的前缀、不同的长度限制和不同的评分掩码。与许多密集模型不同(其中两者可以互换),必须使用encode_query()和encode_document()才能获得正确的嵌入:

code
from
sentence_transformers
import
MultiVectorEncoder

model = MultiVectorEncoder(
"lightonai/mLateOn"
)

queries = [
"What is the capital of France?"
]
documents = [
"Paris is the capital of France."
,
"Berlin is the capital and largest city of Germany, by both area and population."
,
]
python
query_embeddings = model.encode_query(queries)
document_embeddings = model.encode_document(documents)
print(query_embeddings[0].shape)
# (10, 128)
print(document_embeddings[0].shape, document_embeddings[1].shape)
# (10, 128) (19, 128)
code

注意返回的内容:你会得到一个二维张量的列表,每个输入对应一个张量,形状为(num_tokens, embedding_dim)。与密集嵌入不同,你无法将这些张量堆叠成一个矩形张量,因为每个输入都有自己的标记数量。第二个文档比第一个长,因此返回的是一个更高维的矩阵。

每次调用都会应用模型自身的处理逻辑:encode_query会在查询前添加查询标记,如果检查点要求的话会将查询扩展到固定长度,并限制在查询长度内。encode_document会在文档前添加文档标记,限制在文档长度内,并从评分掩码中移除被跳过的标记(对于大多数检查点来说是标点符号)。

常规的encode()参数仍然有效,因此batch_size、show_progress_bar、convert_to_numpy、device和多进程池都会按预期工作:

document_embeddings = model.encode_document( documents, batch_size=64, show_progress_bar=True, )

code

## 使用MaxSim进行评分

model.similarity()计算完整的MaxSim矩阵:

from sentence_transformers import MultiVectorEncoder

model = MultiVectorEncoder("lightonai/LateOn")

query_embeddings = model.encode_query([ "Which planet is known as the Red Planet?" ]) document_embeddings = model.encode_document([ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", "Saturn, famous for its rings, is sometimes mistaken for the Red Planet." ])

scores = model.similarity(query_embeddings, document_embeddings) print(scores)

tensor([[10.7942, 11.1104, 10.9743, 11.0811]])

code

火星胜出,这是正确的结果。注意亚军之间的接近程度:土星也包含字面短语"the Red Planet",木星是一个有红色斑点的行星,因此三个文档在标记级别都有足够的匹配点。排序结果才是关键。

评分通常会如此接近,GLInt通过测量整个候选池的分布来证明这一点。MaxSim对每个查询标记取最大值,因此文档通常会为每个查询标记提供一个不错的最佳匹配,评分从一个基准值开始。上下文化标记嵌入也是各向异性的,集中在狭窄的锥形区域而不是分散开,因此即使任意标记对也往往得分较高。

还有model.similarity_pairwise(),当你已经有匹配对且只需要成对评分而不是完整的相似度矩阵时使用:

scores = model.similarity_pairwise(query_embeddings, document_embeddings[:1]) print(scores)

tensor([10.7942])

code

### 评分幅度与MeanMaxSim

MaxSim对查询标记求和,因此其幅度会随着查询标记数量的增加而变化,这意味着你不能在使用不同查询配方的模型之间比较评分。LateOn将上述"Red Planet"查询编码为12个标记。将相同查询和文档通过ColBERTv2处理,ColBERTv2会将每个查询填充或截断到恰好32个标记,评分会落在完全不同的范围内:

model = MultiVectorEncoder( "colbert-ir/colbertv2.0" )

... 与 encode_query / encode_document / similarity 调用相同 ...

print (scores)

tensor([[12.7970, 27.1945, 23.8495, 24.5656]])

code

在单个模型中,排序结果就是你所需要的,但如果你想在有限范围内获取分数,可以将模型的相似度函数切换为 MeanMaxSim,该函数会除以查询词元数量。回到 LateOn 模型:

model = MultiVectorEncoder( "lightonai/LateOn" , similarity_fn_name= "meanmaxsim" )

或者在已加载的模型上:model.similarity_fn_name = "meanmaxsim"

print (model.similarity(query_embeddings, document_embeddings))

tensor([[0.8995, 0.9259, 0.9145, 0.9234]])

code

现在每个分数都是 [-1, 1] 范围内的平均余弦相似度,但实际上你只会看到 [0, 1] 的范围。

## 语义搜索

如果语料库较小,对全部内容进行完整的 MaxSim 计算是最简单有效的方法。一次性对语料库进行编码,然后将每个查询与所有内容进行评分:

import time from datasets import load_dataset from sentence_transformers import MultiVectorEncoder

dataset = load_dataset( "sentence-transformers/natural-questions" , split= "train[:5000]" )

多个问题共享一个答案段落,因此去除重复项但保留顺序

corpus = list ( dict .fromkeys(dataset[ "answer" ]))

5,000 行 -> 4,874 个段落

model = MultiVectorEncoder( "lightonai/LateOn" ) corpus_embeddings = model.encode_document(corpus, show_progress_bar= True )

query = "when did richmond last play in a preliminary final" start = time.perf_counter() query_embeddings = model.encode_query([query]) scores = model.similarity(query_embeddings, corpus_embeddings)[ 0 ]

98ms

top_scores, top_indices = scores.topk( 3 ) print ( f"Search took {(time.perf_counter() - start) * 1000 : .1 f} ms" ) for score, index in zip (top_scores.tolist(), top_indices.tolist()): print ( f" {score: .4 f} {corpus[index][: 100 ]} " ) """ Search took 122.7ms 11.9192 Richmond Football Club Richmond began 2017 with 5 straight wins, a feat it had not achieved 11.7591 2017 AFL Grand Final The 2017 AFL Grand Final was an Australian rules football game contest 11.6710 Battle of Appomattox Court House The Battle of Appomattox Court House (Virginia, U.S.), fou """

code

在 RTX 3090 上,这 4,874 个段落用了 20 秒进行编码,每次搜索端到端大约需要 120 毫秒,其中大部分时间用于对全部 608,414 个词元向量进行 MaxSim 评分。这是精确的,但随着语料库总词元数量线性扩展,且需要将所有词元向量保留在内存中,因此更适合用于几千个文档而非几百万个文档。该脚本的可运行版本是 semantic_search.py 。

超过这个规模时,你需要一个真正的晚期交互索引,但 Sentence Transformers 并未自带。它不需要自带:这些索引存储的是 encode_document 生成的任何内容,因此你在这里进行编码,然后将词元嵌入传递给为它们构建的系统。索引部分提供了四个选项的可用代码片段,而下方直接介绍如何完全跳过索引。

## 检索与重排序

你也可以通过使用多向量模型作为重排序器,无需维护晚期交互索引即可获得晚期交互质量。一个快速的双编码器可以将一个大型语料库缩小到几个候选,然后多向量模型仅对这些候选进行重新评分:

from datasets import load_dataset from sentence_transformers import MultiVectorEncoder, SentenceTransformer from sentence_transformers.util import semantic_search

/think

dataset = load_dataset( "sentence-transformers/natural-questions" , split= "train[:50000]" ) corpus = list ( dict .fromkeys(dataset[ "answer" ]))

retriever = SentenceTransformer( "jinaai/jina-embeddings-v5-text-nano-retrieval" ) reranker = MultiVectorEncoder( "perplexity-ai/pplx-embed-v1-late-0.6b" , trust_remote_code= True )

第一阶段:使用快速双编码器对语料库进行一次索引

corpus_embeddings = retriever.encode_document(corpus, convert_to_tensor= True , show_progress_bar= True )

检索前50个结果

query = "when did richmond last play in a preliminary final" hits = semantic_search(retriever.encode_query([query], convert_to_tensor= True ), corpus_embeddings, top_k= 50 )[ 0 ] candidates = [corpus[hit[ "corpus_id" ]] for hit in hits]

第二阶段:仅对这些候选结果使用MaxSim重新排序

query_embeddings = reranker.encode_query([query]) document_embeddings = reranker.encode_document(candidates) scores = reranker.similarity(query_embeddings, document_embeddings)[ 0 ] for index in scores.argsort(descending= True )[: 3 ].tolist(): print ( f" {scores[index].item(): .4 f} {candidates[index][: 100 ]} " )

code

只有这50个候选结果会被编码为多向量,因此你的索引仍然保持为普通的密集索引,而标记向量是临时的。这与在检索和重排序堆栈中交叉编码器的作用相同,但多向量模型每个候选的计算成本显著更低。你以一个批次对文档进行编码,并通过矩阵乘法进行评分,而不是对每个查询-文档对进行一次前向传递。可运行的脚本是retrieve_rerank.py,它会打印两个阶段的时间消耗。

## 索引

多个向量数据库原生支持多向量的索引和评分:Qdrant自v1.10版本起、Weaviate自v1.29版本起、Vespa多年来一直支持、LanceDB自v0.15.0版本起,以及VectorChord,它为Postgres添加了普通pgvector没有的MaxSim操作符。Milvus在v2.6.4版本中也加入了支持,但使用的是数组结构而非其称为多向量搜索的不相关特性。如果你不想运行任何服务器,LightOn的fast-plaid只需pip安装即可直接实现PLAID,而PyLate则将其封装在更完整的检索堆栈中。

其他一些工具部分支持该功能。OpenSearch和Elasticsearch可以使用MaxSim对候选结果重新排序,但无法基于此进行检索,且Elasticsearch的该功能还在技术预览阶段且属于企业级功能。turbopuffer的晚期交互索引目前处于私有测试阶段。

下面的代码片段用于索引文本,但其中没有任何部分是特定于文本的。encode_document无论文档是段落、页面图像、音频片段还是视频,都会返回相同格式的标记向量矩阵,因此来自视觉文档检索的ColPali风格模型可以直接用于这些场景中的任意一种。文档中只需简单地增加每个文档的向量数量,这就是为什么在这些场景中更早采用Token Pooling是有价值的。

fast-plaid、Qdrant、Weaviate和Vespa都直接使用encode_document返回的内容,因此直到客户端库的代码都是相同的。以下是针对每个工具的可运行代码片段,用于处理语义搜索示例中的4,874个段落和608,414个标记向量。每个代码片段都展示了在一台机器(RTX 3090,i7-13700K)上运行时产生的摄入和查询时间,除了代码中显示的内容外没有进行任何调整,以展示工作量的大致情况。这四个工具处理查询的速度都比该部分中model.similarity的98ms更快,其中三个工具甚至在CPU上即可完成,因为这里只有fast-plaid使用了GPU。

所有四个系统返回了与之前详尽的 PyTorch MaxSim 相同的三个段落,顺序完全一致,且三个数据库的得分精确到小数点后四位!这是因为它们的片段对每个文档进行评分,在当前规模下这是可行的,消除了近似值作为变量。fast-plaid 本身设计为近似算法,因此其得分略有差异。每个系统下方的注释说明了切换到近似索引时会发生的变化,这正是排名开始出现偏差的地方。

fast-plaid

fast-plaid 是 LightOn 对 PLAID 的 Rust 实现,PLAID 是 ColBERT 最初构建的索引。无需启动服务器,它直接读取 encode_document 返回的张量,无需任何转换。

pip install sentence-transformers datasets fast-plaid

from datasets import load_dataset from fast_plaid import search from sentence_transformers import MultiVectorEncoder

dataset = load_dataset( "sentence-transformers/natural-questions" , split="train[:5000]" ) corpus = list( dict.fromkeys(dataset["answer"]) ) model = MultiVectorEncoder( "lightonai/LateOn" ) query = "when did richmond last play in a preliminary final" document_embeddings = model.encode_document(corpus, batch_size=32) query_embedding = model.encode_query(query)

fast_plaid = search.FastPlaid(index="natural-questions", device="cuda")

4,874 documents (608,414 token vectors) indexed in 5s

fast_plaid.create(documents_embeddings=document_embeddings)

results = fast_plaid.search(queries_embeddings=query_embedding.unsqueeze(0), top_k=3)

11ms

for index, score in results[0]: print( f"{score:.4f} {corpus[index][:90]}" ) """ 11.8828 Richmond Football Club Richmond began 2017 with 5 straight wins, a feat it had not achieve 11.7676 2017 AFL Grand Final The 2017 AFL Grand Final was an Australian rules football game contes 11.6758 Battle of Appomattox Court House The Battle of Appomattox Court House (Virginia, U.S.), fo """

code

索引参数是一个目录,而不仅仅是标签,因此索引在构建时会写入磁盘。将新的 FastPlaid 指向相同路径时,会重新打开该索引用于搜索或添加更多文档,而无需每次都从嵌入向量重新构建。在这个语料库上,它占用 92 MB 空间,而原始 float32 向量则需要 311.5 MB。

这是四个系统中唯一一个采用近似算法的实现,也是本节中唯一一个得分与详尽 MaxSim 不匹配的地方。PLAID 通过质心进行剪枝并存储量化残差,因此三个得分与之前计算的 11.9192 / 11.7591 / 11.6710 相比,在两个方向上都有数百分之一的偏差。此处排名不受影响,这就是 PLAID 的权衡:它被设计用于远大于此规模的语料库,在这种规模下无法进行全量扫描。

Qdrant

Qdrant 需要启动服务器:docker run -p 6333:6333 qdrant/qdrant。客户端也有本地模式(QdrantClient(":memory:")),无需服务器,但这是纯 Python 重实现,建议仅用于测试而非性能评估。

pip install sentence-transformers datasets qdrant-client

from datasets import load_dataset from qdrant_client import QdrantClient, models from sentence_transformers import MultiVectorEncoder

code

dataset = load_dataset(
"sentence-transformers/natural-questions"
, split=
"train[:5000]"
)
corpus =
list
(
dict
.fromkeys(dataset[
"answer"
]))
model = MultiVectorEncoder(
"lightonai/LateOn"
)
query =
"when did richmond last play in a preliminary final"
document_embeddings = model.encode_document(corpus, batch_size=
32
)
query_embedding = model.encode_query(query)

client = QdrantClient(
"http://localhost:6333"
)
client.create_collection(
    collection_name=
"natural-questions"
,
    vectors_config=models.VectorParams(
        size=model.get_embedding_dimension(),
        distance=models.Distance.COSINE,
        multivector_config=models.MultiVectorConfig(
            comparator=models.MultiVectorComparator.MAX_SIM
        ),
# MaxSim never walks the HNSW graph, so skip building one
hnsw_config=models.HnswConfigDiff(m=
0
),
    ),
)
# 4,874 documents (608,414 token vectors) ingested in 26.3s
client.upload_points(
    collection_name=
"natural-questions"
,
    points=[
        models.PointStruct(
id
=idx, vector=embedding, payload={
"text"
: text})
for
idx, (embedding, text)
in
enumerate
(
zip
(document_embeddings, corpus))
    ],
    batch_size=
64
,
)

results = client.query_points(
    collection_name=
"natural-questions"
,
    query=query_embedding,
    limit=
3
,
    with_payload=
True
,
).points
# 18ms
for
result
in
results:
print
(
f"
{result.score:
.4
f}
{result.payload[
'text'
][:
90
]}
"
)
"""
11.9192  Richmond Football Club Richmond began 2017 with 5 straight wins, a feat it had not achieve
11.7591  2017 AFL Grand Final The 2017 AFL Grand Final was an Australian rules football game contes
11.6710  Battle of Appomattox Court House The Battle of Appomattox Court House (Virginia, U.S.), fo
"""

MAX_SIM 是 Qdrant 提供的唯一比较器,对于 late-interaction 字段,他们推荐使用 hnsw_config=HnswConfigDiff(m=0),因为这些向量用于重新排序而非图遍历。需要注意的是,Qdrant 官方建议将 late interaction 用于对几百个候选结果进行重排序,而非扫描整个集合,这属于 Retrieve and Rerank 模式。在 4,874 个文档的完整扫描中耗时 18ms 且精确,但这种性能无法线性扩展。

Weaviate

Weaviate 同样需要服务器:docker run -p 8080:8080 -p 50051:50051 cr.weaviate.io/semitechnologies/weaviate:1.34.0。多向量支持需要 1.29 或更高版本,且 Windows 系统不支持嵌入模式。

code
# pip install sentence-transformers datasets weaviate-client
import
weaviate
from
datasets
import
load_dataset
from
sentence_transformers
import
MultiVectorEncoder
from
weaviate.classes.config
import
Configure, DataType, Property
from
weaviate.classes.query
import
MetadataQuery

dataset = load_dataset(
"sentence-transformers/natural-questions"
, split=
"train[:5000]"
)
corpus =
list
(
dict
.fromkeys(dataset[
"answer"
]))
model = MultiVectorEncoder(
"lightonai/LateOn"
)
query =
"when did richmond last play in a preliminary final"
document_embeddings = model.encode_document(corpus, batch_size=
32
)
query_embedding = model.encode_query(query)

client = weaviate.connect_to_local() collection = client.collections.create( "Documents" ,

self_provided turns on MaxSim late interaction

vector_config=[Configure.MultiVectors.self_provided(name= "colbert" )], properties=[Property(name= "text" , data_type=DataType.TEXT)], )

4,874 documents (608,414 token vectors) ingested in 41s

with collection.batch.fixed_size(batch_size= 64 ) as batch: for text, embedding in zip (corpus, document_embeddings): batch.add_object(properties={ "text" : text}, vector={ "colbert" : embedding.tolist()})

results = collection.query.near_vector( near_vector=query_embedding.tolist(), target_vector= "colbert" , limit= 3 , return_metadata=MetadataQuery(distance= True ), )

17ms

for result in results.objects:

Weaviate reports the MaxSim score as a negated distance

print ( f" {-result.metadata.distance: .4 f} {result.properties[ 'text' ][: 90 ]} " ) """ 11.9192 Richmond Football Club Richmond began 2017 with 5 straight wins, a feat it had not achieve 11.7591 2017 AFL Grand Final The 2017 AFL Grand Final was an Australian rules football game contes 11.6710 Battle of Appomattox Court House The Battle of Appomattox Court House (Virginia, U.S.), fo """ client.close()

code

默认配置已足够:Weaviate 的动态 ef 在 top-3 查询中会解析为 100,且从约 32 的结果开始排名已精确。这个差距是嵌入向量本身的属性,而非 Weaviate 的特性,因此建议在使用自己的模型时验证该特性,而非直接依赖默认值。

Weaviate 还支持 MUVERA 编码,这在我们的测试中使数据导入速度提升 3 倍,查询速度提升 1.8 倍。但在这个规模下,这种速度提升带来的准确性损失远超过其价值:正确的第三段内容甚至未进入前 50 名。

Vespa

Vespa 也可以在容器中运行,但 pyvespa 会为你启动它,因此无需单独执行 docker run 命令。

pip install sentence-transformers datasets pyvespa

from datasets import load_dataset from sentence_transformers import MultiVectorEncoder from vespa.deployment import VespaDocker from vespa.package import ( ApplicationPackage, Document, Field, FirstPhaseRanking, Function, RankProfile, Schema, )

code

dataset = load_dataset(
"sentence-transformers/natural-questions"
, split=
"train[:5000]"
)
corpus =
list
(
dict
.fromkeys(dataset[
"answer"
]))
model = MultiVectorEncoder(
"lightonai/LateOn"
)
query =
"when did richmond last play in a preliminary final"
document_embeddings = model.encode_document(corpus, batch_size=
32
)
query_embedding = model.encode_query(query)
# "dt" 是对可变标记数量的映射维度,"x" 是密集的 128 维向量
package = ApplicationPackage(
    name=
"colbert"
,
    schema=[
        Schema(
            name=
"doc"
,
            document=Document(fields=[
                Field(name=
"text"
,
type
=
"string"
, indexing=[
"summary"
]),
                Field(name=
"colbert"
,
type
=
"tensor<float>(dt{}, x[128])"
, indexing=[
"attribute"
]),
            ]),
            rank_profiles=[
                RankProfile(
                    name=
"colbert"
,
                    inputs=[(
"query(qt)"
,
"tensor<float>(qt{}, x[128])"
)],
                    functions=[Function(
                        name=
"max_sim"
,
# 每个查询标记取最佳文档标记,然后求和
expression=
"sum(reduce(sum(query(qt) * attribute(colbert), x), max, dt), qt)"
,
                    )],
                    first_phase=FirstPhaseRanking(expression=
"max_sim"
),
                )
            ],
        )
    ],
)
app = VespaDocker(port=
8080
).deploy(application_package=package)
# 启动约需 40 秒
# Vespa 将混合张量视为 {标记索引: 向量},对文档和查询均适用
def
to_tensor
(
embedding
):
return
{
str
(token): vector
for
token, vector
in
enumerate
(embedding.tolist())}
# 约 80 秒内导入 4,874 个文档(608,414 个标记向量)
app.feed_iterable(
    ({
"id"
:
str
(idx),
"fields"
: {
"text"
: text,
"colbert"
: to_tensor(embedding)}}
for
idx, (text, embedding)
in
enumerate
(
zip
(corpus, document_embeddings))),
    schema=
"doc"
,
)

response = app.query(body={
"yql"
:
"select text from doc where true"
,
"ranking.profile"
:
"colbert"
,
"hits"
:
3
,
"input.query(qt)"
: to_tensor(query_embedding),
})
# 首次调用约需 115 毫秒
for
hit
in
response.hits:
print
(
f"
{hit[
'relevance'
]:
.4
f}
{hit[
'fields'
][
'text'
][:
90
]}
"
)
"""
11.9192  Richmond Football Club Richmond began 2017 with 5 straight wins, a feat it had not achieve
11.7591  2017 AFL Grand Final The 2017 AFL Grand Final was an Australian rules football game contes
11.6710  Battle of Appomattox Court House The Battle of Appomattox Court House (Virginia, U.S.), fo
"""

Vespa 要求最直接的结构,因为您是在声明一个排序流水线而非仅仅一个索引。作为交换,您可以将 MaxSim 表达为张量表达式并精确看到其计算过程。此版本将 MaxSim 放在 first-phase 的 where true 之上,会对所有 4,874 个文档进行评分,这也是输出与完整 MaxSim 完全匹配的原因。这并非 Vespa 在大规模场景下的推荐方式:他们的 ColBERT 示例应用存储的是 int8 二值化向量,并将 MaxSim 移动到 second-phase 以对更便宜的第一阶段结果进行重排序。

转向这种分阶段设置需要谨慎:默认情况下 second-phase 仅对最佳的 100 个候选进行重评分,而在此场景中这个窗口完全遗漏了三个正确段落中的两个。提高 rerank-count 以覆盖候选集可以解决这个问题,但在这个规模下分阶段版本仍然比直接扫描所有内容更慢。

可视化文档检索

晚期交互是视觉文档检索的最新技术:在保持图表、表格和版式完整性的前提下,将文本查询与页面图像进行匹配,且无需OCR步骤。ColPali模型系列正是通过这种方式实现的,这些检查点通过相同的API加载和运行,修订版本会固定添加该模型Sentence Transformers配置的开放拉取请求(Supported Models列出了完整列表)。图像文档可以以URL、本地路径或PIL图像形式传递:

code
from
sentence_transformers
import
MultiVectorEncoder

model = MultiVectorEncoder(
"vidore/colqwen2.5-v0.2"
)

queries = [
"What is the variable represented on the y-axis of the graph?"
,
"Total outlay is maximum in which year?"
,
]
images = [
"https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/doc1.jpg"
,
"https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/doc2.jpg"
,
"https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/doc3.jpg"
,
"https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/doc4.jpg"
,
]

query_embeddings = model.encode_query(queries)
document_embeddings = model.encode_document(images)
print
(query_embeddings[
0
].shape, document_embeddings[
0
].shape)
# (25, 128) (755, 128)
scores = model.similarity(query_embeddings, document_embeddings)
print
(scores)
# tensor([[13.8672, 12.3115, 12.1670, 11.0293],
#         [ 7.2012, 14.7207,  6.9414,  6.9746]])

每个查询都会检索到对应的页面(对角线位置),由于四页中只有一页涉及时间支出,第二个查询的分离效果比第一个更明显。

代码无需修改。底层处理器会处理视觉提示和图像块,MaxSim评分系统将查询文本标记与文档图像块进行对比。一页包含许多独立区域,这正是晚期交互在这里成为天然适配方案的原因,因为单个向量需要将图表、表格和三段文字合并为一个摘要。这种保真度会占用索引空间。上述形状显示,一页对应755个标记向量,而查询对应25个,早先的自然问题段落平均约为125个,因此在此场景中比文本更早采用标记池化是值得的。

这些是VLM模型,需规划其所需的内存。Supported Models表格中参数规模从252M到8.8B不等,较小的模型在CPU上仍可保持实用性,而多十亿参数的模型则无法实现。

页面图像虽然是常见情况,但并非唯一非文本模态。Sentence Transformers支持文本、图像、音频和视频,检查点会根据处理器支持的模态进行适配,具体支持情况可通过model.modalities查询。单个文档也可以通过传递类似{"text": ..., "image": ...}的字典来组合多种模态。Multimodal Embedding & Reranker Models更全面地介绍了Sentence Transformers中的多模态模型,使用文档也列出了每种模态支持的具体输入格式。

音频检索

vidore/colqwen-omni-v0.1基于Qwen2.5-Omni构建,支持全部四种模态。使用它检索录音对话与检索页面的方式相同,只需两次调用:

code
# pip install -U "sentence-transformers[audio,video]"
import
torch
from
datasets
import
Audio, load_dataset
from
sentence_transformers
import
MultiVectorEncoder

model = MultiVectorEncoder( "vidore/colqwen-omni-v0.1" , model_kwargs={ "dtype" : torch.bfloat16}, ) print (model.modalities)

['text', 'image', 'audio', 'video', 'message']

20段录音对话,平均每段28秒

dataset = load_dataset( "eustlb/dailytalk-conversations-grouped" , split= "train[:20]" ) dataset = dataset.cast_column( "audio" , Audio(sampling_rate= 16_000 )) audio = [row[ "array" ] for row in dataset[ "audio" ]]

原始单声道波形,16kHz采样率,float32格式

query_embeddings = model.encode_query([ "medicine for car nausea" ]) document_embeddings = model.encode_document(audio, batch_size= 2 ) scores = model.similarity(query_embeddings, document_embeddings)[ 0 ]

top_scores, top_indices = scores.topk( 3 ) for score, index in zip (top_scores.tolist(), top_indices.tolist()): print ( f" {score: .4 f} { ' / ' .join(dataset[index][ 'texts' ][: 2 ])} " ) """ 50.8902 对不起?你们有治疗晕车的药吗? / 有的,但你看起来状态不错。 46.1028 对不起,能告诉我你是从哪里得到这本音乐书的吗? / 当然可以。让我看看。哦,它在那个书架上。 46.0514 杰夫,我要去超市。你要和我一起去吗? / 我想超市现在关门了。 """

code

ColQwen-Omni完全基于图文对训练,因此其音频检索属于零样本场景:它从未接触过任何训练样本,整个流程中也没有转录步骤。查询中使用"nausea"(恶心)而录音中是"carsickness"(晕车),它仍能从20段对话中准确识别出药店对话。

## 视频检索

视频处理方式相同,但需要对帧进行采样以避免消耗过多显存。其发布博客明确指出视频"内存消耗极大,最适合处理短片段":

import torch from sentence_transformers import MultiVectorEncoder

model = MultiVectorEncoder( "vidore/colqwen-omni-v0.1" , model_kwargs={ "dtype" : torch.bfloat16}, )

稀疏低分辨率帧:0.5fps而非完整帧率

model[ 0 ].processing_kwargs.update( { "video" : { "max_pixels" : 32 * 28 * 28 , "do_sample_frames" : True , "fps" : 0.5 }} )

query_embeddings = model.encode_query([ "How to cook Mapo Tofu?" ]) document_embeddings = model.encode_document([ "https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/mapo_tofu.mp4" , "https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/zhajiang_noodle.mp4" , ], batch_size= 1 ) print (model.similarity(query_embeddings, document_embeddings))

tensor([[53.3100, 51.0561]])

code

以1fps全分辨率处理相同视频对时,会产生8426和5137个token向量,显存峰值达到20.8GB,而当前设置下仅产生4240和2446个向量,显存占用12.5GB(模型本身占用9.0GB)。排序结果保持一致。长音频同样需要相同处理,发布博客建议使用30秒片段,每个片段约对应800个token。

## 可解释性

由于MaxSim是每个查询token最大值的总和,排序结果可精确分解:文档得分的每个点都对应一个查询token和一个文档token。这使得可以精确回答"为什么这个排在这里?",而非依赖人工判断。

对于图像文档,sentence_transformers.multi_vector_encoder.interpretability 会将该分解以标准 ColPali 热力图形式叠加到页面上,可以是针对查询的聚合结果,也可以是每个查询标记单独一张热力图。以之前支出页面为例,针对问题 "How much was spent on water resources and power?",这就是 "water" 标记的分布情况:

heatmap.py 是可运行版本,包含将文档嵌入与补丁网格对齐的掩码步骤。

文本文档没有可叠加的补丁网格,但相同的分解方法同样适用。text_similarity_map.py 会对语料库进行排序,然后逐标记地将最高命中项的得分归因于查询,这里使用的是之前提到的 Natural Questions 语料库和 32M 参数的 mxbai-edge-colbert-v0-32m 模型:

Query: when did richmond last play in a preliminary final 通过全面 MaxSim 排序的 4874 个文档中前 3 名(耗时 191.0ms): 12.3489 Richmond Football Club Richmond began 2017 with 5 straight wins, a feat it had not achieved since 19 12.1771 2017 AFL Grand Final The 2017 AFL Grand Final was an Australian rules football game contested betwee 12.0591 2018 UEFA Champions League Final The 2018 UEFA Champions League Final was the final match of the 201

查询标记 最佳文档标记 相似度 占比 when since 0.9154 7.4% did had 0.9675 7.8% rich rich 0.9764 7.9% mond mond 0.9856 8.0% last to 0.9249 7.5% play game 0.9384 7.6% in the 0.9732 7.9% a a 0.9587 7.8% preliminary preliminary 0.9394 7.6% final final 0.9654 7.8%


3 个特殊标记 2.8038 22.7% MaxSim 分数 12.3489 100.0%

code

rich、mond、preliminary 和 final 这些标记实现了自我匹配,而 when 则匹配到 since,play 匹配到 game。这些特殊标记也值得注意:它们虽然不包含查询内容,却贡献了 22.7% 的得分。表格下方,脚本会打印出原始段落,并将获胜标记高亮显示。

## 标记池化

如果索引占用空间让你担忧,最有效的调整方式是存储更少的标记向量。HierarchicalTokenPooling 实现了 Clavié、Chaffin 和 Adams 提出的标记池化技术:它使用 Ward 链接在余弦距离上对每个文档的标记向量进行聚类,并用每个聚类的均值代替,保留约 1/pool_factor 的标记数量。在单个文档中,大量标记向量最终会彼此接近,因此你丢弃的大部分内容是冗余而非有效信息:

from datasets import load_dataset from sentence_transformers import MultiVectorEncoder from sentence_transformers.multi_vector_encoder.modules import HierarchicalTokenPooling

dataset = load_dataset( "sentence-transformers/natural-questions" , split= "train[:5000]" ) documents = list ( dict .fromkeys(dataset[ "answer" ]))

model = MultiVectorEncoder( "lightonai/LateOn" )

pooling = HierarchicalTokenPooling(pool_factor= 2 ) document_embeddings = model.encode_document(documents, token_pooling=pooling)

code

根据你希望何时支付计算成本,有三个位置可以应用该技术:

1. 每次编码调用时,如上所示

document_embeddings = model.encode_document(documents, token_pooling=pooling)

2. 独立使用,对已保存的嵌入向量(例如 [num_tokens, num_dims] 张量列表)

pooled = pooling.pool(document_embeddings)

3. 内置到模型中,因此所有使用该检查点的消费者都能获得池化后的文档

model.append(HierarchicalTokenPooling(pool_factor= 2 )) model.save_pretrained( "my-pooled-colbert" )

code

默认情况下,池化仅适用于文档,因为查询较短,是不能承受失真的关键侧。在之前的Natural Questions语料库中,缩减效果与pool_factor紧密相关,池化所有608k个token向量耗时约6秒:

pool_factor

code

Token向量

缩减

float32索引

1 (关闭)

1.00x

2

305,438

1.99x

156.4 MB

3

204,407

2.98x

104.7 MB

4

153,936

3.95x

78.8 MB

聚类均值与查询token的最佳匹配成员相比匹配度更差,且聚类越粗,这种差异越明显。原始实验在BEIR上测量了这种代价,发现影响非常有限:在pool_factor=2时平均保留了100.6%的未池化检索性能,在pool_factor=3时保留了99.0%。无需成本将索引减半是个好交易,因此从2开始是合理的起点。不过在你的数据上代价有多大取决于语料库,因此在确定因子前应使用评估器进行测量。可运行的比较请参见token_pooling.py。

pool_factor能推进到多远也部分取决于模型本身。LightOn的层次池化正则化训练正是为此设计,通过塑造嵌入空间使池化代价更低,并在5倍压缩时报告了99.4%的保留率。目前Sentence Transformers尚未支持这种正则化训练,但生成的检查点是普通的PyLate模型,因此lightonai/LateOn-hpool-regularized可像其他模型一样加载和池化。

## 加速推理

多向量模型与Sentence Transformers其他模型共享相同的后端机制,因此你可以使用torch(默认)、onnx和openvino,同时支持半精度、Flash Attention和torch.compile。

在GPU上,我们测得fp16配合Flash Attention表现最佳,吞吐量是fp32的2.44倍,且没有可测量的检索质量损失。Flash Attention对多向量模型的提升比大多数模型更大,因为文档仅被截断而不会填充到共享长度,因此你的批次中序列长度差异较大,可以利用去填充操作:

from sentence_transformers import MultiVectorEncoder

model = MultiVectorEncoder( "lightonai/GTE-ModernColBERT-v1" , model_kwargs={ "attn_implementation" : "flash_attention_2" , "dtype" : "float16" }, )

code

GPU

CPU

> 不使用注意力查询扩展的模型(attend=False,包括Stanford-NLP检查点如colbert-ir/colbertv2.0和answerdotai/answerai-colbert-small-v1)在加载时会拒绝Flash Attention。Flash Attention会移除attention_mask=0的位置,因此MaxSim评分的[MASK]扩展标记将永远无法获得注意力更新。这些模型应使用"sdpa"。

在CPU上,如果架构支持,OpenVINO是更好的选择,而int8量化可进一步加速,代价约为0.4%的精度损失。有关完整基准测试细节、导出和量化辅助工具以及后端选择流程图,请参见《加速推理》部分。

## 模型评估

MultiVectorNanoBEIREvaluator 运行包含 13 个小型 BEIR 子集的 NanoBEIR 套件,并采用 MaxSim 评分方式,无需您进行任何数据准备:

code
from
sentence_transformers
import
MultiVectorEncoder
from
sentence_transformers.multi_vector_encoder.evaluation
import
MultiVectorNanoBEIREvaluator

model = MultiVectorEncoder(
"lightonai/GTE-ModernColBERT-v1"
)
evaluator = MultiVectorNanoBEIREvaluator(batch_size=
16
)
results = evaluator(model)
print
(
f"
{evaluator.primary_metric}
:
{results[evaluator.primary_metric]:
.4
f}
"
)

这也使得验证本文开头的声明变得简单。lightonai/LateOn 和 lightonai/DenseOn 均由 LightOn 使用相同数据、相同 ModernBERT 主干网络和相同 149M 参数进行训练,唯一区别在于是否保留每个 token 的单个向量或合并为每个文档的单个向量。在所有 13 个 NanoBEIR 数据集上运行这两个模型,可以明确这种选择带来的差异:

NanoBEIR 数据集

LateOn(多向量,128d)

DenseOn(密集,768d)

MSMARCO

0.7194

0.6517

NQ

0.7810

0.7511

HotpotQA

0.9295

0.8802

FEVER

0.9702

0.9612

ClimateFEVER

0.4887

0.4846

DBPedia

0.6836

0.6748

QuoraRetrieval

0.9795

0.9687

Touche2020

0.5938

0.5673

ArguAna

0.5562

0.5660

NFCorpus

0.3949

0.3851

SciFact

0.7978

0.8057

SCIDOCS

0.4469

0.4484

FiQA2018

0.5871

0.6491

平均值

0.6868

0.6764

Late interaction 在 13 个数据集中的 9 个以及平均值上表现更优,领先约 1 个 NDCG 点。在 4 个表现落后的数据集(ArguAna、FiQA2018、SCIDOCS 和 SciFact)中,体现了预期的权衡关系:在相同模型规模下,检索质量的真实提升需要以索引占用空间为代价,而非在所有数据集上都取得优势。这对模型在完整 15 数据集 BEIR 上的得分分别为 57.22 和 56.20,差距相当,因此这一优势并非小基准的偶然结果。

除 NanoBEIR 外,MultiVectorInformationRetrievalEvaluator、MultiVectorRerankingEvaluator、MultiVectorTripletEvaluator 和 MultiVectorDistillationEvaluator 覆盖了在您自己的数据上进行常规评估的常见设置。它们的文档可在 评估 API 参考 中找到。

从 PyLate 或 colpali-engine 迁移

MultiVectorEncoder 整合了这两个库的建模、推理、训练和评估功能。所有 PyLate 检查点均可直接加载,支持模型列表中同时列出了 colpali-engine 检查点及仍需传递的修订版本。如果正在迁移,以下调用方式会发生变化:

PyLate

Sentence Transformers

code
pylate.models.ColBERT(model_name_or_path=...)
code
MultiVectorEncoder(...)
code
model.encode(..., is_query=True)
code
model.encode_query(...)
code
model.encode(..., is_query=False)
code
model.encode_document(...)
code
pylate.scores.colbert_scores
code
model.similarity
code
pylate.indexes.PLAID

/

code
pylate.retrieve.ColBERT

无等效功能,保留 PyLate 的 PLAID 或参见

索引

colpali-engine

code
ColQwen2.from_pretrained(...)

+

code
ColQwen2Processor
code
processor.process_queries(...)
code
model(**batch)
code
model.encode_query(queries)
code
processor.process_images(...)
code
model.encode_document(images)
code
processor.score_multi_vector(qs, ds)
code
model.similarity(query_embeddings, document_embeddings)
code
mask_non_image_embeddings=True
code
MultiVectorMask(keep_only_token_ids=[. ..])
code
HierarchicalTokenPooler
code
HierarchicalTokenPooling
code
colpali_engine.interpretability
code
sentence_transformers.multi_vector_encoder.interpretability

需要特别指出的一个区别:在未使用 ColBERT 的检查点上,PyLate 的 ColBERT("bert-base-uncased") 默认应用经典方案,而 MultiVectorEncoder("bert-base-uncased") 构建普通堆栈,并将前缀、查询扩展和跳列表作为显式选项。训练损失和评估器等价项以及数据处理差异请参阅《迁移指南》。

请注意,所有情况下保存兼容性都是单向的:PyLate、Stanford-NLP ColBERT 和 colpali-engine 的检查点均可加载到 MultiVectorEncoder 中,但 MultiVectorEncoder.save_pretrained 的输出无法被它们加载。

支持的模型

在 Hub 上带有 multi-vector 和 sentence-transformers 标签的模型列表会持续更新,我们正在努力为所有适用模型添加这些标签。下表是我们直接测试的模型列表,应将其视为起点而非完整集合。特别是对于文本检索,无论是否带有标签,所有 PyLate 或 Stanford-NLP ColBERT 检查点均可加载。

部分模型需要先在其仓库中添加少量 Sentence Transformers 配置,其中一些仍在撰写中。如果下方列出了修订版本,请在相关 Pull Request 合并前传递该版本,之后仅需使用普通模型名称即可:

code
model = MultiVectorEncoder(
"vidore/colqwen-omni-v0.1"
, revision=
"refs/pr/N"
)

文本检索模型

这些模型加载时会恢复训练的前缀标记、查询扩展和标点跳列表配置。

NanoBEIR 列报告了 13 个 NanoBEIR 数据集的平均 NDCG@10(数值越高越好),每个数据集是 BEIR 数据集的 50 个查询子集,作为英文文本检索质量的快速代理指标。我们使用 MultiVectorNanoBEIREvaluator 计算主要面向英文的模型得分。"-" 表示模型未在该数据集上进行评估。请注意 NanoBEIR 是一个小型基准测试,其得分不能替代在您自己的数据上进行评估,后者始终是选择模型的正确方式。

| 模型 | 参数量 | 维度 | NanoBEIR | 备注 | |------|--------|------|----------|------| | lightonai/LateOn-regularized | 149M | 0.6897 | - | | | lightonai/LateOn-hpool-regularized | 0.6876 | | | | lightonai/LateOn | | 0.6851 | | | VAGOsolutions/SauerkrautLM-Multi-ModernColBERT | 0.6741 | | | lightonai/GTE-ModernColBERT-v1 | 0.6720 | | | topk-io/Iso-ModernColBERT | 0.6687 | | | perplexity-ai/pplx-embed-v1-late-0.6b | 596M | 0.6662 | | | VAGOsolutions/SauerkrautLM-Multi-Reason-ModernColBERT | 0.6616 | | | lightonai/ColBERT-Zero | 0.6569 | | | answerdotai/answerai-colbert-small-v1 | 33M | 96 | 0.6550 | | | mixedbread-ai/mxbai-edge-colbert-v0-32m | 32M | 64 | 0.6524 | | | LiquidAI/LFM2-ColBERT-350M | 0.6441 | | | mixedbread-ai/mxbai-edge-colbert-v0-17m | 17M | 48 | 0.6407 | | | lightonai/colbertv2.0 | 110M | 0.6201 | | | lightonai/LateOn-Code | 0.6169 | | | lightonai/Agent-ModernColBERT | 0.6164 | | | lightonai/Reason-ModernColBERT | 0.6078 | | | colbert-ir/colbertv2.0 | 0.6053 | | | VAGOsolutions/SauerkrautLM-Reason-EuroColBERT | 212M | 0.6039 | | | VAGOsolutions/SauerkrautLM-EuroColBERT | 0.5965 | | | antoinelouis/colbert-xm | 853M | 0.5915 | | | mixedbread-ai/mxbai-colbert-large-v1 | 335M | 0.5733 | | | lightonai/LateOn-Code-edge | 0.5274 | | | NeuML/biomedbert-base-colbert | 0.4320 | | | yjoonjang/colbert-ko-v1 | | | ytu-ce-cosmos/turkish-colbert | 111M | 256 | | | samheym/GerColBERT | | | | |

`

### 可视化文档检索模型

ColPali风格的模型将页面图像作为文档进行嵌入,将文本作为查询。

NanoViDoRe列报告了在NanoViDoRe v3基准测试中的平均NDCG@10(数值越高越好),该基准测试是一个涵盖8个子集的紧凑型视觉文档检索基准测试(包括英文和法语的计算机科学、能源、金融、人力资源、工业、制药和物理领域)。与NanoBEIR类似,NanoViDoRe是一个小型基准测试,不应替代在您自己的数据上进行的评估。

NanoViDoRe

webAI-Official/webAI-ColVec1.1-8b

8.4B

640

0.6580

webAI-Official/webAI-ColVec1.1-4b

4.5B

0.6520

tencent/EVIE-Preview-4.5B

4.54B

0.6405

TomoroAI/tomoro-colqwen3-embed-8b

8.8B

320

0.6206

TomoroAI/tomoro-colqwen3-embed-4b

4.4B

0.6019

vidore/colqwen2.5-v0.2

3.8B

0.5402

vidore/colqwen2.5-v0.1

0.5395

vidore/colqwen-omni-v0.1

0.5309

vidore/colpali-v1.3

2.9B

0.4802

vidore/colpali-v1.3-hf

0.4793

vidore/colpali-v1.2

0.4691

vidore/colqwen2-v1.0

2.2B

0.4685

vidore/colqwen2-v0.1

0.4526

vidore/colpali

0.4516

vidore/colpali-v1.1

0.4314

vidore/colsmolvlm-v0.1

2.1B

0.4054

vidore/colpali-hard-v1.1

vidore/colSmol-500M

507M

0.3459

vidore/colSmol-256M

256M

0.2673

ModernVBERT/colmodernvbert

252M

0.2632

vidore/colpali-v1.2-hf

vidore/colqwen2-v1.0-hf

这些模型中大部分是LoRA适配器仓库,适配器在加载时直接应用到其基础模型上。一些模型在Hub上还有-merged版本(例如vidore/colpali-v1.3-merged),其中适配器权重已合并到基础模型中。

三个-hf条目是transformers原生的*ForRetrieval接口实现。它们无需任何配置即可加载,但使用了更多来自transformers的建模组件,而来自sentence_transformers的组件更少。通常建议使用原始模型,因为接口实现的得分大致相同。

## 致谢

Sentence Transformers中后期交互功能建立在大量早期工作基础上。感谢Omar Khattab和Matei Zaharia开发的ColBERT,所有此处的内容都源自ColBERT。同时感谢LightOn团队(Antoine Chaffin、Raphael Sourty、Paulo Moura和Amélie Chatelain)开发的PyLate和fast-plaid,这些工具多年来实现了后期交互功能,并塑造了上述API的很大一部分。

感谢ColPali团队(Manuel Faysse、Hugues Sibille、Tony Wu、Bilel Omrani、Gautier Viaud、Céline Hudelot和Pierre Colombo)开发的ColPali和colpali-engine,这些工作将后期交互功能引入页面图像处理。同时感谢Benjamin Clavié、Antoine Chaffin和Griffin Adams在token pooling方面的贡献。

同时感谢MTEB核心团队(包括Kenneth Enevoldsen和Roman Solomatin等众多成员)开发的MTEB,以及他们为信息检索研究持续进行的幕后工作。

最后感谢所有在"支持的模型"中训练并发布检查点的人员。没有他们的贡献,本文将无法进行任何评估。

## 附加资源

### 文档

- 多向量编码器 > 使用方法

- 多向量编码器 > 预训练模型

- 多向量编码器 > 创建自定义模型

- 多向量编码器 > 加速推理

- 多向量编码器 > API参考

- 迁移指南

### 示例脚本

- ColPali热力图

- 文本相似度地图

- NanoBEIR评估

### 训练

要了解如何在自己的数据上训练或微调这些模型:

- 多向量编码器 > 训练概述

- 多向量编码器 > 损失函数概述

- 多向量编码器 > 训练示例

- LateOn 和 mLateOn 训练脚本:LightOn 提供的 PyLate 方案用于 LateOn、mLateOn、DenseOn 和 mDenseOn,其中微调脚本展示了实用细节,例如将包含 16,384 个样本的批次拆分为 16 个样本的小批次。

### Hugging Face Hub

- Hub 上的多向量模型

- Hub 上的 Sentence Transformers 数据集

### 附录博文

- 使用 Sentence Transformers 训练和微调嵌入模型:面向纯文本密集型嵌入模型的一般训练指南。

- 使用 Sentence Transformers 训练和微调重排序模型:交叉编码器训练,另一种添加精确第二阶段的方法。

- 使用 Sentence Transformers 训练和微调稀疏嵌入模型:SPLADE 和其他稀疏编码器,这些模型与混合搜索中的晚期交互结合效果良好。

- 使用 Sentence Transformers 的多模态嵌入与重排序模型:单向量多模态模型,ColPali 风格检索的密集型对应方案。

- 使用 Sentence Transformers 训练和微调多模态嵌入与重排序模型:包含单向量模型的视觉文档检索完整操作指南。

- 🪆 Matryoshka 嵌入模型简介:通过降维压缩密集嵌入,类似通过计数压缩多向量嵌入的 token pooling 方法。

## 本文提到的模型 10

## 本文提到的数据集 3

## 本文提到的集合 1

更多博客文章

多模态

自然语言处理

社区

## 使用 Sentence Transformers 训练和微调多模态嵌入与重排序模型

81

2026 年 4 月 16 日

## 使用 Sentence Transformers 的多模态嵌入与重排序模型

71

2026 年 4 月 9 日

### 社区

编辑

预览

通过拖拽文本输入框、粘贴或

点击此处

上传图片、音频和视频。

轻点或粘贴此处上传图片

评论

· 注册或登录以发表评论

- +64