跳到正文
Shopify Engineering·· 23 天前精选AI 评分76

Shopify 详解跨数百万店铺的十亿级商品聚类架构

Cross-store product clustering across millions of stores

AI 导读

Shopify 工程团队披露了跨店铺商品聚类系统架构,通过构建全局统一商品标识符(UPI)为 AI 智能体与开发者提供标准化商品数据层。系统利用向量检索生成候选对,设计了由 CPU 级 GBDT、多语言 E5 交叉编码器与图文重排模型组成的三层打分机制,并通过有界图切分与层次凝聚聚类完成高精度合并。

推荐理由

文章详细拆解了十亿级商品跨店聚类的三层打分与图聚类架构,为电商平台构建面向智能体的全局商品层提供了工程参考。

正文 · AI 翻译

阅读本系列的第一篇,了解我们如何在单个店铺内对相关商品条目进行归类。

Shopify 上数百万家店铺汇聚了数十亿个商品条目。商家可以自由按自己的习惯来组织商品,但要统一处理整个平台的商品库,这种灵活性就带来了挑战。不同商家发布的条目可能对应同一款实体商品,我们需要识别出这些关联,从而为买家、开发者和 AI 智能体提供关于商品及其在售店铺的统一视图。

Shopify Catalog 是一个统一的智能层,它将全平台的商品数据进行标准化,并通过 Catalog API 提供给开发者和 AI 智能体。Catalog 的核心技术在于商品聚类。针对现实中的同一款商品,不同商家的组织方式可能截然不同:有的商家会创建一个包含所有尺码和颜色变体的单一商品条目,有的则可能按颜色分别建档。

借助聚类技术,我们可以把所有这些相关的变体和商品归纳到同一个通用商品标识符(在 Catalog API 中表示为 UPID)之下。无论商家以何种形式发布,同一款现实商品的所有版本都能统一归纳在一起。

在第一篇中,我们介绍了如何在单个店铺内将相关条目归纳到一个 UPI 下。但 Shopify Catalog 还必须解决一个难度更高的问题:当不同商家销售同一款商品或其变体时,我们怎么确定它们是同一件东西?这就是跨店铺聚类:将不同商家发布的同款商品条目整合到同一个全局 UPI 之下。

此前介绍的店内处理方案无法直接照搬。在单个店铺内,我们可以把商家的商品目录作为上下文参考:观察他们如何命名商品、描述品牌,以及如何区分型号与变体。但在跨店铺场景下,这些惯用规则就失效了。不同商家拟定的标题和描述五花八门,拍摄同款商品的视角和风格各异,关键细节填写的字段也不尽相同。某个商家写在标题里的型号代码,到了另一个商家那里可能被埋在描述正文中。条形码虽有参考价值,但普及率不足且准确度参差不齐。

此外,搜索空间也截然不同。我们无法再简单地按商家把问题分块处理:某款商品的潜在匹配条目可能散落在全平台的任意店铺中。我们必须在数十亿级条目中找出这些匹配项,既不能对每两个条目都做两两比对,又要避免将本该区分的商品误归为一类。

从商品比对到全局聚类

与其让大语言模型(LLM)去给出全局统一的商品标签,我们不如抛出一个范围更明确的问题:这两个商品条目是否应该归入同一个 UPI?

模型可以直接比对两者的标题、图片、选项以及其他元数据,而无需依赖多次独立的模型调用都能对同一商品生成完全一致的标签。从两两比对入手,也避免了预先建立一套高可靠全局聚类库的门槛:我们可以先收集匹配证据,再利用这些证据来构建聚类。不过,成对判断可能存在非传递性:模型可能会判定 A 与 B 匹配、B 与 C 匹配,却认为 A 与 C 不匹配。这也是为什么我们不能使用连通分量这类简单方法,而必须采用更高级聚类算法的原因。

我们的系统围绕三个步骤构建:筛选疑似配对、为每组配对打分,以及将打分结果聚类。

筛选疑似配对

第一步是生成候选配对。我们用嵌入向量表示商品,并通过近似最近邻搜索为每件商品检索出最接近的 Listing。

即便只是找出这份候选短名单,挑战也相当巨大:我们要检索的是数十亿级别的向量库,而非单个商家的目录。系统必须在检索质量、索引大小与搜索吞吐量之间取得平衡。近似搜索让计算在工程上变得可行,但近似算法本身以及短名单的数量限制,都可能导致原本匹配的商品未能进入候选池。这些配对也就无缘进入后续的匹配模型。不过,哪怕一次未能匹配,商品也不会从商品目录中消失(它依然可以通过自身 ID 被搜索到),只是暂时无法与其他店铺的同款商品归为一类,直到后续的构建流程重新发现这一关联。

检索大幅缩小了搜索范围,但剩下的候选配对仍有数万亿之多。更何况,嵌入空间中的距离接近,并不等于两条 Listing 就一定代表同一款商品。

为每组配对打分

能力出众的大语言模型确实可以结合商品信息和我们对商品同一性的定义,对这些比对进行推理。但如果让大模型逐一评估数万亿对候选商品,成本将高到无法承受。

因此,我们改用 LLM 裁判对抽样的比对结果进行标注,拿人工标注的结果来校准模型判断,再用大模型生成的标签来训练专用的匹配模型。此外,能力更强的模型打出的分数,也能为更低成本的模型提供训练信号。这样一来,高昂的推理成本就转化成了可复用的训练数据,而不必在每次比对时重复买单。

我们采用了三层配对打分器(L1、L2 和 L3),它们在能力和推理成本上各有侧重。聚类构建器会汇总它们的打分,而计算成本更高的比对任务则会放入后台队列处理。

将打分结果聚类

我们将商品抽象为图节点,将打分得出的关联关系抽象为边。但这绝非简单地把所有连通的节点归为一类:只要出现一条错误连接,就可能把原本毫无关联的两组商品强行拼到一起。

我们首先把图切分成若干个大小受限的连通分支,以便独立处理。这些连通分支只是计算单元,并不是最终确定的统一商品。随后,UPI 构建器会调用训练好的模型,对拟议的聚类合并进行评估;它不仅会考量触发该提议的这一对商品的得分,还会综合权衡支持和反对合并的所有证据。

Cross-store clustering pipeline

不同打分器各司其职

用最高规格的模型为每一对候选商品打分,成本过于高昂;但如果只依赖成本最低的模型,又会漏掉那些需要更细致辨别的情况。因此,我们采取了分工策略:

  • L1 依托轻量特征,快速对海量候选配对完成打分
  • L2 将两条 Listing 的文本放在一起进行综合评估
  • L3 针对筛选出的比对,进一步引入图片和更丰富的元数据

以下是各模型的工作原理以及选用理由。

L1:用极低成本完成首轮初筛

L1 是一个基于 GBDT(梯度提升决策树)的分类器,依托低成本的配对特征运行,包括嵌入距离、标题与品牌相似度以及价格关联等。它无需对每一对商品都调用神经交叉编码器,就能完成对数万亿对候选商品的评估。

L1 有两项职责:提供快速匹配依据,以及协助判断哪些候选商品值得调用更昂贵的模型来处理。这两个目标互相关联,但并不完全一致。即便 L1 得出的分数还不足以支撑商品合并,某个商品对也完全可能值得交给更强的模型进一步判断。分清这一差异,我们才能在额外调用模型的成本与发现更多真实匹配的收益之间取得平衡。

L2:双商品联合比对

L2 是经过微调的多语言 E5 交叉编码器(cross-encoder)。它会结合标题、商家名称、预测类目、URL 以及店铺名称,将两款商品的信息联合输入并读取。联合编码让模型能够关注两个输入之间的细微差异,而不是仅仅依赖分别生成的嵌入向量。

当大部分文本都吻合、唯独一处细节决定商品身份时,这一点尤为关键。两款手机壳的商品页面可能品牌和产品线完全一致,但依然属于不同商品,比如“适用于 iPhone 15”与“适用于 iPhone 15 Pro”。联合编码让 L2 能够在这段简短的兼容后缀与大段重合的品牌、产品线文本之间权衡权重,而不会让整体相似度将这一关键区别彻底掩盖。

它的局限在于输入内容。L2 涵盖的信息很多,但并不包含商品描述详情。一旦区分两个商品的关键细节仅出现在描述中,L2 就无从察觉,商品对看起来就会像是匹配的。这正是我们将筛选出的商品对送入 L3 的部分原因——L3 可以从图片中寻找差异。

针对这类需要高精度判定的场景,我们采用联合编码;而在召回检索阶段,独立的嵌入向量依然是核心。

L3:引入图像参与决策

L3 是一个经过微调的开源重排模型,它能同时结合文本和元数据来查看商品图像。图像能够提供单纯文本容易遗漏的证据,例如不同的接口、按键布局或手机壳开孔。但图像也会带来歧义——两个商家可能会从不同角度拍摄同一款商品——因此模型必须结合上下文来理解图像,而不能只是单纯比对像素。

我们要求 L3 输出一个匹配分数,而不是生成一段解释。这是线上推理服务中的一个重要权衡:在推理阶段,聚类构建器需要的是匹配得分,而不是一段文字解释。成本更高昂的推理解释工作,应该放在训练数据构建流程中。

我们用规模更大的 LLM 裁判为抽样样本提供标注,性能更强的模型给出的评分也能作为低成本模型的训练信号。我们会用人工标注来校验这位 LLM 裁判的准确性,因为它的错误可能会传递到以其标注为训练数据的下游模型中。

这几个层级提供了互为补充的判断依据,但它们输出的分数并不能直接划等号。我们针对每个评分模块单独设定阈值,并评估它们对聚类产生的综合效果。毕竟,商品对匹配得分再高,只有最终能聚出更合理的商品组才有意义。

承接店内聚类成果

我们在 第一部分 构建的管线,绝不是为了在第二部分就直接弃用。店内聚类形成的商品组,为跨店铺系统提供了候选关联关系。

区别在于,我们现在的视野不再局限于单一店铺。其他商家的商品页面能提供更多维度的佐证,帮助判断哪些商品应该归为一类。统一商品标识(UPI)构建器会结合这些更广泛的证据来综合考量店内关联,而不是机械地照搬原有的商品组。

这样一来,我们既保留了前期工作的价值,又能让最终的聚类结果充分利用仅看单个店铺时无法获取的信息。

边界清晰的组件

我们将图拆分成规模较小的连通分量,单台机器即可运行 UPI 构建器完成处理。这样一来,各个分量便能彼此独立、并行计算。

难点在于如何切分。若将商品随机分配到多台机器上,大量本应匹配的商品就会被拆散;普通的连通分量固然能把关联商品聚拢,但某些分量规模会膨胀得过大,无法高效处理。我们的目标是在严格限制分量规模的同时,尽可能把真正匹配的商品留在同一个分量里。

先从单机算法入手

我们采用了一种带有规模上限的有界并查集(bounded union-find)数据结构,用它来追踪每个商品所属的分量:

  • 初始化时,每个商品自成一个独立分量。
  • 按照边的得分从高到低依次处理。
  • 若某条边两端的节点已在同一个分量中,则直接跳过;若不在,且两分量合并后的总规模仍在预算限制内,则予以合并。

我们使用商品节点的度数之和(即商品关联的边数)来衡量这笔“规模预算”,而不是只看商品个数。毕竟与规模相当但连接稀疏的群组相比,高度密集的商品组处理起来要复杂得多。

整个算法逻辑非常清晰:优先考虑置信度更高的连接,且任何合并都不得让分量超出既定的规模预算。

并行处理边数据

在我们的数据体量下,单机处理整张图的所有边很容易演变成性能瓶颈。因此,必须减少进入最后一步有界合并时的边数量。

我们将边数据分发给多个 worker 并行处理。每个 worker 利用普通的并查集消除各自数据分区内的冗余连接。比如三个商品构成三角形(两两相连),worker 就可以在不破坏连通性的前提下剔除一条边。Worker 仅保留这些连通边,生成对应的生成森林(spanning forest),而无需上传完整的边列表。

随后,我们汇总各 worker 生成的森林,在这份精简后的边集合上再次按得分顺序执行有界合并算法。森林结构虽然保留了原图的连通性,但并不保证生成的分量与直接处理全量边时完全一致。需要注意的是,规模预算依然依据原图中的连接数来统计,而非精简后的森林。

这样一来,大量边处理工作由 worker 并行消化,最后的整合阶段则负责统一执行分量规模上限。最终划分出的分量即可交给独立的 UPI 构建 worker 继续处理。

Bounded components

UPI 构建器

将图切分成若干有界分量后,原先全目录级别的复杂难题被拆解成了多个互不干扰的小任务。每个分量规模足够小,单台机器即可轻松处理,分量之间还能完全并行。这使得 UPI 构建器可以直接在群簇级别评估关系,无需跨机器协调单次的合并决策。

在每个分量内部,我们采用引入学习合并规则的层次凝聚聚类(HAC)算法。系统逐步将小簇合并为大簇,并利用 GBDT 模型为每次合并提议打分。这一步的目标,是在确保不混淆不同商品的前提下,尽可能把所有同款商品的 listing 聚合到同一个 UPI 之下。

我们为这些合并操作设定了极高的精确率目标。因为一次误合并就会让两整组不同的商品混在一起,而漏掉一次有效合并,仅仅是让它们暂时保持分散。因此在调优模型的采纳阈值时,我们以精确率优先,并在满足该指标的前提下,最大化提升召回率。

充分利用边上的已有信息

此时,图中的边已经同时包含类型和分值。这些分值综合了大量的商品信息:根据打分模型的不同,它们反映了标题、品牌、类目、图片以及其他元数据的比对结果。边的类型则用来区分证据的来源及其置信度。

UPI 构建器的职责就是高效利用这些信息。它无须在每次提出合并建议时重新读取商品数据,而是直接整合现有的预测结果,在聚类层面做出决策:现有证据是否支持将这两个群组合并?

我们使用 GBDT 模型,提取描述两个群组及其潜在合并状态的特征:

  • 支持度(Support):连接两个群组的边有多少?相对于各自规模而言,这些连接的紧密程度如何?
  • 覆盖度(Coverage):这些连接是涉及双方的大量商品,还是仅仅集中在一两款商品上?
  • 凝聚度(Cohesion):与群组外部的连接相比,每个群组内部商品之间的连接强度如何?
  • 矛盾度(Contradiction):有多少低分的 L2 或 L3 证据反对将这两个群组合并?

我们针对不同类型的边证据分别计算这些特征,从而明确区分高匹配度的结果与否定性判断。这样一来,合并模型不仅能看到触发该提议的单对商品得分,还能从全局视角全面了解两个群组之间的关联。

A strong pair is evidence

从模型的决策中收集数据

训练集的构建经历了几轮迭代。新模型上线后,被采纳的合并项会发生变化,从而改变生成的中间聚类,进而影响后续需要面对的决策场景。仅靠旧策略收集的训练数据,无法完全覆盖新策略所引发的新情况。

我们建立了一套循环机制:运行构建器、对其提出的合并提议进行抽样、用大模型裁判打标、再重新训练。我们不仅需要可接受合并的样本,还需要不可接受合并的样本,而不是单纯让模型去复现旧构建器的决策。每一次迭代都扩充了训练集,覆盖了新策略遇到的更多实际场景。

图本身的变化也会影响模型的输入。随着新打分结果的输入或商品从目录中下架,聚类的密度和连接关系都会发生变化,因此我们不仅需要收集新标签,还要同步刷新从图中提取的训练特征。

现在,我们会定期收集最新的合并提议并进行重训,确保训练数据始终与构建器行为的变化以及商品目录的演进保持同步。

紧跟动态变化的商品目录

商品目录始终处于动态变化之中。新商品不断入库,商家频繁更新标题和图片,原本毫无关联的商品也可能成为新的候选匹配对。因此,我们必须持续评估新的候选对,并在底层商品信息发生变化时及时刷新现有分值。

保持推理持续运行

L1 运行在 CPU 上,速度足够支撑对全量候选集打分。每当新增或变更的商品生成候选对时,L1 都会为每一对分配一个初始匹配得分。

L2 和 L3 需要 GPU 进行推理,无法以同样的规模应用于所有候选对。因此,我们维护了待比对任务队列。新任务会与尚未接受高开销模型打分的既有候选对一起进入这些队列,排队等待处理。

这样一来,我们的 GPU 集群就能源源不断地获得任务供给。与其在每天的聚类运行中强行算完所有耗费高昂的比对,不如根据现有算力按部就班地持续推进。

把有价值的比对放在首位

挑选合适的任务来充分利用 GPU 算力,这才是关键所在。

我们会综合前序模型的评分、购买与搜索信号,以及当前的图结构来对候选商品进行优先级排序。模型评分有助于筛选出极具潜力的匹配项或值得进一步深究的比对;需求信号有助于优先处理买家频繁互动的商品;图上下文则有助于优先处理那些比对结果可能影响 UPI 构建器合并决策的项。

因此,任务队列承担着双重作用:既要跟上商品目录的变动,又要提高已有商品的匹配覆盖率。新的比对任务会按优先级挑选,从而优先于较旧的任务得到处理。

跨聚类构建复用计算结果

我们会缓存已计算出的分值,并记录生成这些分值所用的模型以及相关商品特征。只要模型和相关商品特征没有改变,就可以直接复用缓存的分数。

每日构建 UPI 时,系统会将当前的 L1 结果与本次运行可用的 L2、L3 分值相结合,而无需等待推理队列彻底清空。与此同时,后台推理会持续处理新增、变更以及此前未打分的商品对。

这样一来,在后台 GPU 推理不断补充和刷新 UPI 构建器所需评分的同时,我们依然能够每天更新聚类结果。

成果

我们构建的这套系统,不仅让商家能够保留各自独立的商品页面,还能协助 AI 智能体高精度地识别出哪些商品属于同款并将其归为一组。随着商品的新增或变更,系统会持续更新这些分组,且专为在 Shopify 这样的大体量下快速、可靠地运行而设计。

核心思路在于让高成本的判断结果能够被复用:既可以作为训练数据来训练更快的模型,也可以作为缓存分值供后续构建使用。这让我们能够在不断强化商品身份判定依据的同时,将昂贵的推理算力集中在新增、变更或此前未打分的比对上。

感谢 Mariya Mansurova、 Boris Nazarov 以及 Oleg Akimov 的贡献。

来源:Shopify Engineering · shopify.engineering