← 返回信息流

精选Hugging Face Blog新闻

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

huggingface.co教程模型AI评分:70/100

本文介绍了多向量(晚期交互)嵌入模型,如 ColBERT 风格模型,它们为每个 token 保留向量,通过 MaxSim 操作进行评分,在保持可索引性的同时提升检索质量。文章详细说明了如何加载模型、编码查询和文档、进行语义搜索、索引、视觉文档检索等,并讨论了索引大小和速度的权衡。

-68

普通嵌入模型将整段文本压缩为一个向量,而多向量模型则为每个词元保留一个向量,并使用MaxSim算子计算查询与文档之间的得分。这保留了单向量模型不得不平均掉的词元级匹配信息,通常意味着更强的检索能力,但代价是索引体积更大。它也是视觉文档检索的最先进方法——文本查询直接与页面图像匹配,中间无需OCR步骤。

在本文中,我们将展示如何使用这些模型:加载各种检查点格式、编码与打分、将其接入搜索栈、在页面图像上运行,以及控制索引成本。以下所有内容只需pip install -U sentence-transformers即可运行。

目录

  • 什么是多向量模型?MaxSim算子 你得到什么,代价是什么
  • 安装
  • 加载模型 检查检查点配置了什么
  • 编码查询与文档
  • 使用MaxSim打分 分数量级与MeanMaxSim
  • 语义搜索
  • 检索与重排
  • 索引
  • 视觉文档检索
  • 音频检索
  • 视频检索
  • 可解释性
  • 词元池化
  • 加速推理
  • 评估模型
  • 从PyLate或colpali-engine迁移而来
  • 支持的模型
  • 致谢
  • 其他资源

什么是多向量模型?

稠密嵌入模型读取文本并返回一个固定大小的向量。模型注意到的所有信息都必须塞进那384、768或1024个数字里,相似度就是两个这样的摘要之间的一个点积。这种方法效果非常好,但压缩在特定意义上有损:稀有实体、精确标识符或长段落中一个关键从句,都必须竞争同一个向量中的空间。一个同时包含多个要求的查询也会撞上同样的墙。对于“绿色沙发,木腿,圆润靠垫”这样的查询,单向量必须将四个要素融合成一个点,因此一个腿不对的绿色沙发最终会与你真正想要的那个靠得很近。

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

查询与文档之间的交互随后被推迟到打分阶段,这就是“晚期交互”名称的由来。交叉编码器是早期交互:两个文本一起通过模型,这很精确,但没有任何可预计算的内容,因为每个文档都必须为每个新查询重新编码。双编码器——即上述稠密嵌入模型——几乎不交互(两个完成摘要之间的一个点积),这正是让你能一次性编码整个集合并快速查询的原因。晚期交互介于两者之间:文档仍然独立编码,可以离线索引,但打分时会将每个查询词元与每个文档词元进行比较,为两者交互留下了远更多的空间。

Dense embedding versus multi-vector late interaction: a dense model encodes each text into one vector and scores with c…
Dense embedding versus multi-vector late interaction: a dense model encodes each text into one vector and scores with c…

MaxSim算子

打分使用MaxSim:对于每个查询词元,取其与任何文档词元的最高相似度,然后将这些最大值在查询维度上求和。

MaxSim(Q,D)=∑Qi∈Qmax⁡Dj∈DQi⋅Dj

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

你可以将这个算子理解为一种软对齐:每个查询词元指向最能解释它的那个文档词元,得分就是文档整体上对查询的支持程度。

这种对齐不一定是词面上的,因为词元嵌入是上下文相关的。用lightonai/mLateOn将“Where do penguins live?”与“Penguins inhabit Antarctica.”进行编码,查询词元live在inhabit上找到最佳匹配,相似度为0.94——这两个词没有任何共同字符!这是词法检索做不到的事情,BM25及其同类方法需要词项本身出现,因此同义词和改写会从它们眼皮底下溜走。稠密嵌入模型当然也能弥合这一差距。晚期交互的额外优势在于,它做到这一点的同时不放弃另一个方向:当精确匹配至关重要时(产品代码、姓氏、函数名),MaxSim仍然让那个词元独立存在,而单向量模型则不得不将其与其他所有内容平均在一起。它也不是一对一的,因为多个查询词元通常会落在同一个文档词元上。

你得到什么,代价是什么

你获得的检索质量提升,尤其体现在以下场景:文档中某个特定片段才是其相关性的关键所在的查询;像上文沙发示例那样的多条件查询,每个条件都能找到自己的证据;以及超出分布范围的数据,此时稠密模型的压缩是针对不同分布调优的。这种压缩是从训练查询中学习而来的,因此模型学会了保留训练查询所需的信息,丢弃其余一切,而丢弃的内容可能恰恰是你的生产环境查询所问及的。这种影响会随着文档长度的增加而加剧,因为更多的文本必须塞进同样大小的固定向量中。

代价是索引体积。每个 token 一个向量,而非每个文档一个向量,这意味着向量数量大幅增加,而维度变小只能部分抵消这一影响。使用 lightonai/LateOn 编码 4,874 个 Natural Questions 段落,产生了 608,414 个 token 向量,平均每个段落 124.8 个:

表示方式向量数量维度float32 大小
稠密,all-MiniLM-L6-v24,8743847.5 MB
稠密,gte-modernbert-base4,87476815.0 MB
多向量,LateOn608,414128311.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,即 Indexing 部分中出现的晚期交互索引。到了 v6.0 版本,这些能力已经内置在 Sentence Transformers 本身之中。

了解了这一权衡之后,让我们来运行一个模型。

安装

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

pip install -U sentence-transformers

对于 ColPali 风格的视觉文档检索,你还需要图像依赖项(完整附加项请参阅 Installation,多模态支持总体情况请参阅 Multimodal Embedding & Reranker Models):

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

Sentence Transformers v6.0 要求 transformers v5.x、torch 2.2+ 和 huggingface-hub v1.x。如果你将其中任何一个版本固定得更低,请先规划升级。完整的破坏性变更列表请参阅 Migration Guide。

加载模型

加载多向量模型看起来与加载任何其他 Sentence Transformers 模型完全一样:

from sentence_transformers import MultiVectorEncoder

model = MultiVectorEncoder("lightonai/LateOn")

要查找可用的模型,请在 Hub 上查找 multi-vector 和 sentence-transformers 标签。任何带有这些标签的模型都可以用上面的代码行加载,无论它最初是 PyLate 检查点、Stanford-NLP ColBERT 检查点,还是用于视觉文档检索的 ColPali 系列模型。我们正在整个生态系统中努力,将这一标签添加到所有可用的模型上,因此列表会不断增长。

在底层,MultiVectorEncoder 会读取这些检查点多年来发布的各种格式,因此即使某些检查点尚未添加标签,PyLate 和 Stanford-NLP 检查点也可以直接加载:

from sentence_transformers import MultiVectorEncoder



model = MultiVectorEncoder("lightonai/LateOn")
model = MultiVectorEncoder("mixedbread-ai/mxbai-edge-colbert-v0-17m")
model = MultiVectorEncoder("LiquidAI/LFM2.5-ColBERT-350M", trust_remote_code=True)



model = MultiVectorEncoder("colbert-ir/colbertv2.0")
model = MultiVectorEncoder("answerdotai/answerai-colbert-small-v1")


model = MultiVectorEncoder("answerdotai/ModernBERT-base")

视觉文档检索模型是个例外。ColPali 系列检查点以 colpali-engine 自己的格式发布,这种格式不携带 Sentence Transformers 可用的任何信息,因此每个模型都需要在其仓库中添加一个小型配置才能加载。这项工作大部分已经完成,正在等待合并。当前状态以及如何加载这些模型,请参阅 Supported Models。

检查检查点配置了什么

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

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

你很少需要改动这些,因为每个发布的检查点都已配置好自身参数。只有在从裸骨干网络构建模型时才需要关注,这在“创建自定义模型”一节中有介绍。

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

编码查询与文档

多向量模型是非对称的:查询和文档经过不同的前缀、不同的长度上限和不同的评分掩码。与许多稠密模型(其中两者可互换)不同,encode_query() 和 encode_document() 是获得正确嵌入所必需的:

注意你得到的内容:一个 2D 张量列表,每个输入对应一个,每个张量的形状为 (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,
)

使用 MaxSim 评分

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

火星胜出,理应如此。注意亚军的接近程度:土星也包含字面短语“the Red Planet”,而木星是一颗带有红斑的行星,因此标记级算子在这三者中都有大量可捕捉的信息。排序才是关键。

分数往往如此接近,GLInt 通过测量整个候选池的分数分布也证明了这一点。MaxSim 对每个查询标记取最大值,因此一个文档通常会给每个查询标记提供某种不错的匹配,分数从一个底值开始。上下文相关的标记嵌入也是各向异性的,聚集在一个狭窄的锥体内而非分散开来,因此即使是任意的标记对也倾向于获得高分。

还有 model.similarity_pairwise(),适用于当你已经拥有匹配对、只想获得配对分数而非完整相似度矩阵时:

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

分数量级与 MeanMaxSim

MaxSim 对查询标记求和,因此其量级随查询标记数量缩放,这意味着你不能跨不同查询配方的模型比较分数。LateOn 将上述“the Red Planet”查询编码为 12 个标记。将同样的查询和同样的文档通过 ColBERTv2 运行——它会将每个查询填充并截断到恰好 32 个标记——分数会落在完全不同的范围内:

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

print(scores)

在单个模型内,排序就是你所需要的,但如果你想要有界范围内的分数,可以将模型的相似度函数切换为 MeanMaxSim,它会除以查询标记数量。回到 LateOn:

现在每个分数都是 [-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"]))  

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]  
top_scores, top_indices = scores.topk(3)
print(f"Search took {(time.perf_counter() - start) * 1000:.1f}ms")

for score, index in zip(top_scores.tolist(), top_indices.tolist()):
    print(f"{score:.4f}  {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
"""

这 4,874 个段落在一张 RTX 3090 上仅用 20 秒完成编码,每次搜索端到端耗时约 120 毫秒,其中大部分时间花在对全部 608,414 个 token 向量进行 MaxSim 评分上。这种方法是精确的,但其计算量随语料库总 token 数线性增长,且需要将所有 token 向量保存在内存中,因此适用于几千篇文档而非几百万篇的场景。此脚本的可运行版本见 semantic_search.py。

超出这个规模后,你需要一个真正的后期交互索引,而 Sentence Transformers 并不内置该功能。这其实并不必要:这些索引存储的正是 encode_document 的输出,所以你在这里完成编码,然后将 token 向量交给专门为此设计的工具即可。索引部分提供了四种可选方案的可用代码片段,下面一节则介绍如何完全跳过索引。

检索与重排序

你也可以在不维护后期交互索引的情况下获得后期交互的质量,方法是将多向量模型用作重排序器。一个快速的 bi-encoder 先将大规模语料库缩小到少量候选,然后多向量模型仅对这些候选重新评分:

from datasets import load_dataset

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

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)


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]


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():.4f}  {candidates[index][:100]}")

只有这 50 个候选会被编码为多向量,因此你的索引仍然是普通的稠密索引,token 向量只是临时存在。这与 cross-encoder 在检索-重排序架构中的角色相同,但多向量模型对每个候选的计算成本要低得多。你可以一次性批量编码文档,然后通过矩阵乘法进行评分,而不是对每个查询-文档对分别执行一次前向传播。可运行脚本见 retrieve_rerank.py,它会打印两个阶段的耗时。

索引

多个向量数据库原生支持多向量的索引和评分:Qdrant 自 v1.10 起、Weaviate 自 v1.29 起、Vespa 已支持多年、LanceDB 自 v0.15.0 起,以及 VectorChord——它为 Postgres 增加了 MaxSim 算子,这是普通 pgvector 所不具备的。Milvus 在 v2.6.4 中加入了对该功能的支持,采用数组结构而非其所谓的“多向量搜索”这一不相关特性。如果你完全不想运行服务器,LightOn 的 fast-plaid 只需 pip install 即可安装,它直接实现了 PLAID 算法,而 PyLate 则将其封装在更完整的检索框架中。

还有几个方案只能算部分支持。OpenSearch 和 Elasticsearch 可以用 MaxSim 对候选结果重新打分,但不能直接用它检索,而且 Elasticsearch 的那项功能还处于技术预览阶段,且属于企业版。turbopuffer 的 late-interaction 索引则还在私有测试阶段。

下面的代码片段索引的是文本,但其中没有任何内容是文本特有的。无论文档是一段文字、一页图片、一段音频还是一段视频,encode_document 返回的都是同样的 token 向量矩阵列表,因此 Visual Document Retrieval 中的 ColPali 风格模型可以原封不动地接入这些方案。只是每个文档的向量数量更多,这也正是 Token Pooling 在这些场景下更值得尽早采用的原因。

fast-plaid、Qdrant、Weaviate 和 Vespa 都能直接接受 encode_document 的返回值,所以代码除了客户端库不同之外完全一样。下面是针对每个方案的可用代码片段,运行在 Semantic Search 示例中的 4,874 个段落和 608,414 个 token 向量上。每个片段都记录了在一台机器(RTX 3090,i7-13700K)上的索引和查询耗时,除了代码中展示的内容外没有做任何调优,以便让你了解工作量的分布情况。这四个方案回答查询的速度都快于该节中 model.similarity 的 98 毫秒,而且其中三个是在 CPU 上完成的,因为这里只有 fast-plaid 使用了 GPU。

这四个方案返回的三个段落及其顺序,与本文前面用穷举 PyTorch MaxSim 得到的结果完全一致,而且其中三个数据库的分数精确到了小数点后四位!这是因为它们的代码片段对每个文档都进行了评分,在这个规模下这样做是可行的,同时也排除了近似计算这个变量。fast-plaid 天生就是近似检索,所以它的分数略有不同。每个方案下方的注释说明了切换到近似索引后会发生什么变化,那时排名就会开始出现偏差。

fast-plaid 是 LightOn 对 PLAID 的 Rust 实现,而 PLAID 正是 ColBERT 最初所基于的索引。它不需要启动服务器,并且可以直接读取 encode_document 返回的张量,无需任何转换。

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")


fast_plaid.create(documents_embeddings=document_embeddings)

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

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
"""

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

这是四个方案中唯一一个近似检索的,也是本节中分数与穷举 MaxSim 不一致的唯一一处。PLAID 使用质心进行剪枝,并存储量化后的残差,因此三个分数与之前计算出的 11.9192 / 11.7591 / 11.6710 相比,在两个方向上都有百分之几的偏差。这里的排名没有受到影响,而这正是 PLAID 所做的权衡:它是为远比这个语料库更大的语料库设计的,在那些场景下扫描所有内容是不可行的。

Qdrant 需要一个服务器:docker run -p 6333:6333 qdrant/qdrant。客户端也有一个本地模式(QdrantClient(":memory:")),不需要服务器,但它是纯 Python 的重新实现,所以适合用来试用,而不是用来计时。

MAX_SIM 是 Qdrant 提供的唯一比较器,而 hnsw_config=HnswConfigDiff(m=0) 是他们对晚期交互字段的建议,因为这些向量用于重新评分而非图遍历。请注意,Qdrant 自己也建议将晚期交互保留用于对几百个候选结果进行重排,而不是扫描整个集合,这正是“检索后重排”(Retrieve and Rerank)模式。在 4,874 篇文档上,全量扫描耗时 18 毫秒且结果精确,但这个数字不能外推到更大规模。

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

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",
    
    vector_config=[Configure.MultiVectors.self_provided(name="colbert")],
    properties=[Property(name="text", data_type=DataType.TEXT)],
)


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),
)  

for result in results.objects:
    
    print(f"{-result.metadata.distance:.4f}  {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()

这里使用默认设置就足够了:Weaviate 的动态 ef 在 top-3 查询时解析为 100,而该排序从大约 32 起就已经是精确的。这个余量是嵌入本身的属性,而非 Weaviate 的特性,因此建议在你自己的模型上验证,而不是假设默认设置一定成立。

Weaviate 还支持 MUVERA 编码,在我们的测试中,这使得数据摄入速度快了 3 倍,查询速度快了 1.8 倍。但在这种规模下,它牺牲的精度远超速度提升的价值:正确的第三篇段落甚至没有出现在其前 50 名中。

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

Vespa 是四种方案中结构最前置的,因为你声明的是排序管道而不仅仅是索引。作为交换,你可以将 MaxSim 写成张量表达式,并精确看到它计算的内容。这个版本将 MaxSim 放在对 where true 的第一阶段排序中,对所有 4,874 篇文档进行评分,这就是输出与穷举 MaxSim 完全一致的原因。这刻意不是 Vespa 在大规模场景下的推荐做法:他们的 ColBERT 示例应用存储 int8 二值化向量,并将 MaxSim 移到第二阶段,用于对更廉价的第一阶段结果进行重排。

转向这种分阶段设置需要谨慎:默认情况下,第二阶段只对最佳 100 个候选进行重新评分,而在此处,这个窗口导致三个正确段落中有两个完全未被评分。提高重排数量以覆盖你的候选集可以解决这个问题,不过在这个规模下,分阶段版本仍然比直接扫描全部内容更慢。

视觉文档检索

晚期交互是视觉文档检索的最先进技术:将文本查询与页面图像进行匹配,保留图表、表格和布局,且无需 OCR 步骤。这就是 ColPali 系列模型所做的事情,这些检查点通过相同的 API 加载和运行,修订版本固定了添加该模型 Sentence Transformers 配置的开放拉取请求(Supported Models 中有完整列表)。图像文档以 URL、本地路径或 PIL 图像的形式传入:

每个查询检索到各自的页面(对角线),第二个查询的区分比第一个清晰得多,因为四个页面中只有一个是关于随时间变化的支出。

代码没有变化。在底层,处理器处理视觉提示和图像块,MaxSim 对查询文本标记与文档图像块进行评分。一个页面包含许多独立的区域,这正是晚期交互在此处如此自然契合的原因,因为单个向量必须将图表、表格和三段文字平均成一个摘要。但这种保真度需要付出索引空间的代价。上面的形状是一个页面的 755 个标记向量对比查询的 25 个标记向量,而之前 Natural Questions 段落平均约 125 个,因此在此处进行标记池化比文本场景更值得提前使用。

这些是 VLM,所以要为它们所需的内存做好规划。Supported Models 中的表格涵盖从 252M 到 8.8B 参数的范围,其中小规模端在 CPU 上仍然实用,而数十亿参数的模型则不行。

页面图像是常见情况,但并非唯一的非文本模态。Sentence Transformers 接受文本、图像、音频和视频,而某个检查点支持其处理器所支持的任何模态,这一点由 model.modalities 报告。单个文档也可以组合多种模态,方法是传入一个字典,例如 {"text": ..., "image": ...},而不是单独的值。Multimodal Embedding & Reranker Models 更广泛地介绍了 Sentence Transformers 中的多模态模型,而 Usage 文档则列出了每种模态具体接受哪些输入格式。

音频检索

vidore/colqwen-omni-v0.1 基于 Qwen2.5-Omni 构建,支持全部四种模态。用它检索一段录制的对话与检索页面一样,同样是两次调用:

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)



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"]]  

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:.4f}  {' / '.join(dataset[index]['texts'][:2])}")
"""
50.8902  Excuse me? Do you have anything for a carsickness? / Yes, but you look fine.
46.1028  Excuse me, could you tell me where you have got that music book? / Certainly. Let me see. Oh, it's on that shelf.
46.0514  Jeff, I'm going to the supermarket. Do you want to come with me? / I think the supermarket is closed now.
"""

ColQwen-Omni 仅在图像-文本对上训练,因此它的音频检索是零样本的:它从未见过训练样本,而且整个流程中没有任何转写步骤。查询说的是 nausea(恶心),而录音里说的是 carsickness(晕车),它仍然以很大优势从二十段对话中选出了药房那段。

视频检索

视频的工作方式相同,但需要对帧进行采样,否则会耗尽你的显存。其发布博客对此直言不讳,称视频“非常消耗内存,因此最适合短视频片段”:

import torch

from sentence_transformers import MultiVectorEncoder

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


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))

在 1 fps 和全分辨率下,同样的两个视频分别产生 8,426 和 5,137 个 token 向量,峰值显存占用达 20.8 GB;而这里只有 4,240 和 2,446 个向量,占用 12.5 GB,且模型本身就要占 9.0 GB。两种方式的排序结果完全相同。长音频也需要同样的处理,发布博客建议使用 30 秒的片段,每个片段大约对应 800 个 token。

可解释性

由于 MaxSim 是每个查询 token 最大值的总和,因此排序可以被精确分解:文档得分的每一点都归属于一个查询 token 和一个文档 token。这让你能够精确地回答“为什么这个排在这里?”,而不是凭肉眼判断。

对于图像文档,sentence_transformers.multi_vector_encoder.interpretability 将该分解以标准 ColPali 热力图的形式叠加到页面上,既可以按整个查询聚合,也可以为每个查询 token 生成一张热力图。针对上面的支出页面提问“水资源和电力花了多少钱?”,这就是 water 这个 token 的注意力分布:

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

文本文档没有可叠加的补丁网格,但同样的分解同样适用。text_similarity_map.py 对语料库进行排序,然后逐 token 地归因排名第一的文档的得分来源,这里使用的是之前 Natural Questions 语料库和 32M 参数的 mxbai-edge-colbert-v0-32m 模型:

查询:里士满上一次参加预选决赛是什么时候 通过穷举MaxSim在4874篇文档中排名前3(191.0毫秒): 12.3489 里士满足球俱乐部 里士满以2017年开局5连胜开启赛季,这是自19年以来未曾实现的壮举 12.1771 2017年AFL总决赛 2017年AFL总决赛是一场澳大利亚规则足球比赛,对阵双方为 12.0591 2018年欧洲冠军联赛决赛 2018年欧洲冠军联赛决赛是20年的决赛比赛

MaxSim heatmap of the query token "water" overlaid on a 1971 US budget outlays page, with the brightest patch on the "W…
MaxSim heatmap of the query token "water" overlaid on a 1971 US budget outlays page, with the brightest patch on the "W…

查询词元 最佳文档词元 相似度 占比 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%

rich、mond、preliminary和final匹配到了自身,而when匹配到了since,play匹配到了game。特殊词元也值得注意:其中三个贡献了22.7%的得分,却不携带查询的任何内容。在这张表下方,脚本会打印出段落本身,并在原文位置高亮显示获胜词元。

词元池化

如果你担心索引占用空间,最有效的调节手段是存储更少的词元向量。HierarchicalTokenPooling实现了Clavié、Chaffin和Adams提出的词元池化技术:它使用余弦距离上的Ward连接对每篇文档的词元向量进行聚类,并用每个聚类的均值替换该聚类,大约保留1/pool_factor的词元。在一篇文档内部,许多词元向量最终会彼此接近,因此你丢弃的大部分是冗余而非信号:

根据你希望何时承担计算成本,有三个应用位置:

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


pooled = pooling.pool(document_embeddings)


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

默认情况下,池化仅应用于文档,因为查询较短,且是你不容失真的那一侧。在之前提到的Natural Questions语料上,缩减幅度与pool_factor密切相关,池化全部608k个词元向量大约耗时6秒:

pool_factor词元向量数缩减幅度float32索引大小
1(关闭)608,4141.00倍311.5 MB
2305,4381.99倍156.4 MB
3204,4072.98倍104.7 MB
4153,9363.95倍78.8 MB

聚类均值作为查询词元的匹配对象,不如其成员中的最佳匹配,而且聚类越粗糙,这种差距越明显。原始实验在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对多向量模型的帮助大于大多数模型,因为文档只做截断而从不填充到统一长度,因此你的批次具有差异很大的序列长度,去填充可以利用这一点:

使用非注意力查询扩展(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% 的准确率损失。完整的基准测试细节、导出和量化辅助工具,以及选择后端的流程图,请参阅“加速推理”一节。

GPU
GPU
CPU
CPU

评估模型

MultiVectorNanoBEIREvaluator 使用 MaxSim 评分运行 NanoBEIR 套件(包含 13 个小型 BEIR 子集),并且无需您准备任何数据:

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]:.4f}")

这也让验证本文开头的论断变得容易。lightonai/LateOn 和 lightonai/DenseOn 由 LightOn 在相同数据上训练,使用相同的 ModernBERT 主干和相同的 149M 参数,唯一区别在于前者保留每个 token 一个向量,后者则池化为每个文档一个向量。在全部 13 个 NanoBEIR 数据集上运行这两个模型,可以单独衡量这一选择带来的差异:

NanoBEIR 数据集LateOn(多向量,128d)DenseOn(稠密,768d)
MSMARCO0.71940.6517
NQ0.78100.7511
HotpotQA0.92950.8802
FEVER0.97020.9612
ClimateFEVER0.48870.4846
DBPedia0.68360.6748
QuoraRetrieval0.97950.9687
Touche20200.59380.5673
ArguAna0.55620.5660
NFCorpus0.39490.3851
SciFact0.79780.8057
SCIDOCS0.44690.4484
FiQA20180.58710.6491
平均值0.68680.6764

晚期交互在 13 个数据集中的 9 个以及平均值上胜出,大约高出 1 个 NDCG 点。它输掉的四个数据集(ArguAna、FiQA2018、SCIDOCS 和 SciFact)恰好体现了您应当预期的权衡形态:在相同模型规模下获得真实的检索质量提升,代价是索引占用空间增大,而非在每个数据集上都全面胜出。同一对模型在完整的 15 数据集 BEIR 上得分为 57.22 对 56.20,差距相当,因此这一优势并非小型基准测试的偶然产物。

除 NanoBEIR 之外,MultiVectorInformationRetrievalEvaluator、MultiVectorRerankingEvaluator、MultiVectorTripletEvaluator 和 MultiVectorDistillationEvaluator 覆盖了在您自己数据上的常规评估设置。它们记录在“评估 API 参考”中。

从 PyLate 或 colpali-engine 迁移

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

PyLateSentence Transformers
pylate.models.ColBERT(model_name_or_path=...)MultiVectorEncoder(...)
model.encode(..., is_query=True)model.encode_query(...)
model.encode(..., is_query=False)model.encode_document(...)
pylate.scores.colbert_scoresmodel.similarity
pylate.indexes.PLAID / pylate.retrieve.ColBERT无对应项,保留 PyLate 的 PLAID 或参见“索引”一节
colpali-engineSentence Transformers
ColQwen2.from_pretrained(...) + ColQwen2ProcessorMultiVectorEncoder(...)
processor.process_queries(...) + model(**batch)model.encode_query(queries)
processor.process_images(...) + model(**batch)model.encode_document(images)
processor.score_multi_vector(qs, ds)model.similarity(query_embeddings, document_embeddings)
mask_non_image_embeddings=TrueMultiVectorMask(keep_only_token_ids=[...])
HierarchicalTokenPoolerHierarchicalTokenPooling
colpali_engine.interpretabilitysentence_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 配置,其中一些在撰写本文时仍是开放的拉取请求。如果下面列出了修订版本,请在该拉取请求合并之前传入该修订版本,之后仅使用模型名称即可:

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

文本检索模型

这些模型加载时会恢复其训练好的前缀标记、查询扩展和标点跳过列表(均从保存的配置中获取)。

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

模型参数维度NanoBEIR备注
lightonai/LateOn-regularized149M1280.6897-
lightonai/LateOn-hpool-regularized149M1280.6876-
lightonai/LateOn149M1280.6868-
LiquidAI/LFM2.5-ColBERT-350M353M1280.6864需要 trust_remote_code=True
lightonai/mLateOn307M1280.6851-
VAGOsolutions/SauerkrautLM-Multi-ModernColBERT149M1280.6741-
lightonai/GTE-ModernColBERT-v1149M1280.6720-
topk-io/Iso-ModernColBERT149M1280.6687-
perplexity-ai/pplx-embed-v1-late-0.6b596M1280.6662需要 trust_remote_code=True
VAGOsolutions/SauerkrautLM-Multi-Reason-ModernColBERT149M1280.6616-
lightonai/ColBERT-Zero149M1280.6569-
answerdotai/answerai-colbert-small-v133M960.6550-
mixedbread-ai/mxbai-edge-colbert-v0-32m32M640.6524-
LiquidAI/LFM2-ColBERT-350M353M1280.6441-
mixedbread-ai/mxbai-edge-colbert-v0-17m17M480.6407-
lightonai/colbertv2.0110M1280.6201-
lightonai/LateOn-Code149M1280.6169-
lightonai/Agent-ModernColBERT149M1280.6164-
lightonai/Reason-ModernColBERT149M1280.6078-
colbert-ir/colbertv2.0110M1280.6053-
VAGOsolutions/SauerkrautLM-Reason-EuroColBERT212M1280.6039-
VAGOsolutions/SauerkrautLM-EuroColBERT212M1280.5965-
antoinelouis/colbert-xm853M1280.5915-
mixedbread-ai/mxbai-colbert-large-v1335M1280.5733-
lightonai/LateOn-Code-edge17M480.5274-
NeuML/biomedbert-base-colbert110M1280.4320-
yjoonjang/colbert-ko-v1149M128--
ytu-ce-cosmos/turkish-colbert111M256--
samheym/GerColBERT110M128--

视觉文档检索模型

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

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

模型参数维度NanoViDoRe备注
webAI-Official/webAI-ColVec1.1-8b8.4B6400.6580需要 trust_remote_code=True
webAI-Official/webAI-ColVec1.1-4b4.5B6400.6520需要 trust_remote_code=True
tencent/EVIE-Preview-4.5B4.54B1280.6405-
TomoroAI/tomoro-colqwen3-embed-8b8.8B3200.6206需要 trust_remote_code=True
TomoroAI/tomoro-colqwen3-embed-4b4.4B3200.6019需要 trust_remote_code=True
vidore/colqwen2.5-v0.23.8B1280.5402-
vidore/colqwen2.5-v0.13.8B1280.5395-
vidore/colqwen-omni-v0.14.4B1280.5309-
vidore/colpali-v1.32.9B1280.4802-
vidore/colpali-v1.3-hf2.9B1280.4793-
vidore/colpali-v1.22.9B1280.4691-
vidore/colqwen2-v1.02.2B1280.4685-
vidore/colqwen2-v0.12.2B1280.4526-
vidore/colpali2.9B1280.4516-
vidore/colpali-v1.12.9B1280.4314-
vidore/colsmolvlm-v0.12.1B1280.4054-
vidore/colpali-hard-v1.12.9B1280.3949-
vidore/colSmol-500M507M1280.3459-
vidore/colSmol-256M256M1280.2673-
ModernVBERT/colmodernvbert252M1280.2632-
vidore/colpali-v1.2-hf2.9B128--
vidore/colqwen2-v1.0-hf2.2B128--

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

三个-hf条目是transformers原生的*ForRetrieval移植版本。它们无需任何配置即可加载,但更多地使用transformers而非sentence_transformers的建模。一般来说,优先使用原始模型更好,因为移植版本的得分大致相同。

致谢

Sentence Transformers中的后期交互建立在许多早期工作之上。感谢Omar Khattab和Matei Zaharia贡献的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池化方面的工作。

同样感谢MTEB核心团队,其中包括Kenneth Enevoldsen和Roman Solomatin等许多人,感谢他们贡献的MTEB以及那些让信息检索研究得以持续运转的幕后工作。

还要感谢所有训练并发布Supported Models中检查点的人。没有他们,这篇文章将无数据可测。

其他资源

文档

  • Multi-Vector Encoder > 用法
  • Multi-Vector Encoder > 预训练模型
  • Multi-Vector Encoder > 创建自定义模型
  • Multi-Vector Encoder > 加速推理
  • Multi-Vector Encoder > API参考
  • 安装
  • 迁移指南

示例脚本

  • 语义搜索
  • 检索与重排
  • Token池化
  • ColPali热力图
  • 文本相似度图
  • NanoBEIR评估

训练

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

  • Multi-Vector Encoder > 训练概览
  • Multi-Vector Encoder > 损失函数概览
  • Multi-Vector Encoder > 训练示例
  • LateOn和mLateOn训练脚本:LightOn的PyLate配方,用于LateOn、mLateOn、DenseOn和mDenseOn,其中微调脚本展示了实际细节,例如将16,384个样本的批次拆分为16个样本的小批次。

Hugging Face Hub

  • Hub上的多向量模型
  • Hub上的Sentence Transformers数据集

配套博客文章

  • 使用Sentence Transformers训练和微调嵌入模型:面向纯文本稠密嵌入模型的通用训练指南。
  • 使用Sentence Transformers训练和微调重排模型:Cross Encoder训练,这是添加精确第二阶段的另一种方式。
  • 使用Sentence Transformers训练和微调稀疏嵌入模型:SPLADE和其他稀疏编码器,它们在混合搜索中与后期交互配合良好。
  • 使用Sentence Transformers的多模态嵌入与重排模型:单向量多模态模型,是ColPali风格检索的稠密对应物。
  • 使用Sentence Transformers训练和微调多模态嵌入与重排模型:包括使用单向量模型进行视觉文档检索的逐步讲解。
  • 🪆 Matryoshka嵌入模型入门:按维度压缩稠密嵌入,就像token池化按数量压缩多向量嵌入一样。

阅读原文