01.前言
目前包括B站在内的主流搜索和推荐系统均采用多级漏斗的架构,主要涵盖召回、粗排、精排、重排等关键阶段。其中召回作为整个流程的首要环节,作用在于从海量的稿件集合中,快速高效地筛选出一小部分与用户需求和兴趣高度契合的稿件,作为后续排序阶段的输入数据。为了全面覆盖各类用户复杂多样的需求,通常采用多通道召回的策略。召回结果的优劣,也直接决定了搜推系统效果的上限。
召回系统的核心挑战在于如何在有限的时间与算力资源下处理大规模的数据,并且保持较高的召回率与准确率。随着业务的发展,B站搜推召回系统面临着数据规模的增长、时效性要求的提升、算法策略复杂度不断加大等一系列挑战。
面对这些挑战,经过多轮迭代与架构升级,形成了目前基于B站业务特点的大规模召回系统。本文将从工程实践的视角出发,深入介绍B站搜推召回系统的架构设计与实现。
02 整体架构
在搜推系统迭代历程之初,召回策略相对较为简单,召回仅仅被定位为搜推业务引擎服务内的一个子模块。伴随业务的持续拓展,单体巨型的引擎服务暴露出诸多问题,如代码复杂度过高、可维护性大幅下降、内存资源逼近瓶颈等均制约了系统的进一步迭代。将召回模块从引擎服务中拆分出来,构建为独立的服务,成为了当时解决燃眉之急的关键举措。
但很快由于召回策略迭代频率越来越高、召回候选集规模越来越大、召回通道数量快速增长,独立召回服务的架构也逐渐显得力不从心,难以持续支撑业务的高速发展,阻碍了系统效果的提升。具体而言,主要存在以下亟待解决的问题:
召回索引内存占用庞大,扩展性差;服务启动/扩容速度慢、索引构建速度慢,时效性差;
迭代效率低,新增召回通道开发量大;多通道结果合并策略混乱,通道间耦合严重;
搜索、推荐不同业务场景召回系统实现不统一,维护成本大;
实验、监控、降级、发车流水线等稳定性保障能力不完善;
为了从根本上解决以上问题,我们重新设计了一套云原生、可扩展、配置化、搜推统一的召回框架。
在线检索侧基于计算存储分离的理念,采用merge服务+索引服务(searcher)两级架构:
merge服务主要负责:多通道召回发起,多通道结果去重、过滤、打分&排序,最终合并多通道结果返回给引擎;
索引服务则作为召回通道的载体,加载各召回通道依赖的索引,执行通道特有的检索逻辑,返回单通道的召回结果;
离线侧由index-builder读取策略产出在hive/hdfs上的数据构建base索引;同时支持通过实时数据流更新索引,秒级生效。
03.merge服务
召回merge服务定位为召回的业务引擎,采用配置化、算子化的设计模式,能够灵活应对不同业务场景下的召回需求。merge服务在设计上重计算轻存储,依赖多个外部服务提供相应能力:索引服务提供召回候选、kv服务获取用户数据、推理服务计算user-embedding、score计算相关性打分、统一正排提供正排信息用于过滤。一个典型的召回流程主要由trigger生成、请求各召回通道获得候选、通道间候选去重、正排过滤与打分、通道内候选排序、通道结果合并等流程组成。
在不同业务场景中,召回流程大体相似,仅在一些召回参数和局部流程上有所差异。merge服务通过将各模块封装为可复用的算子/组件,通过编排和配置,即可以灵活快速地支持不同业务场景下的召回策略迭代需求。常用的算子包括:不同类型的trigger获取、请求索引服务、请求多分片索引服务及结果合并、正排及过滤、Z字形merge等。为了尽可能地降低耗时,merge服务引入轻量级的DAG框架进行调度。
DAG框架的优势在于能够清晰地描绘出各个任务之间的依赖关系和执行顺序,通过合理规划任务路径,可以最大程度地增加逻辑并发。例如在召回流程中,不同类型的trigger生成往往不存在依赖,可以同时进行;多召回通道间的并发和相互依赖关系也能被方便地构建出来;正排过滤与打分也能够并行执行。
04.索引服务
索引服务的主要职能在于针对单召回通道精准且高效地生成召回结果,通过支持分片的方式横向扩展以应对上亿量级的数据规模,同时具备实时数据更新的能力,提供充足的时效性保障。
架构层面,索引服务从上到下可以分为四层:
交互层 - 提供单片及分片客户端,支持动态检索参数;
执行层 - 执行具体检索逻辑,支持布尔检索、过滤、排序等;
索引层 - 将数据组织为各类索引,提升检索效率;
构建层 - 天级/小时级数据构建为基准索引,实时数据构建为增量索引,并定期dump整理内存;
常见的召回通道有基于纯文本的语义召回、基于类目/tag的召回、协同过滤、 双塔dssm召回等。
根据不同召回通道的特点,我们相应地设计了不同类型的索引服务以满足召回策略和性能需求,主要分为以下三类:
文本召回;
x2i召回;
向量召回;
下面分别介绍各索引服务的设计实现。
4.1 文本召回
文本召回将是稿件的标题、描述、评论等做切词处理后建立倒排索引,再与用户的query进行匹配,常见于搜索场景。
检索时首先根据query的分词建立查询表达式,例如“周杰伦演唱会”,分词后再通过“周杰伦”和“演唱会”分别查询索引做表达式运算获得召回结果。
文本召回的索引由倒排和正排两部分组成:
倒排索引为term到DocIdList(PostingList)的映射;
正排索引为DocId到DocInfo的映射;
查询流程为term->DocIdList->DocInfo,其中DocId为稿件插入索引时分配的内部递增Id,DocId小的排在倒排拉链头部。通常离线构建索引前,先根据offline-weight按降序排序,这样可以使优质的稿件优先被召回。
每条倒排链按DocId排序,也帮助在做表达式运算时,可以快速地跳到大于等于当前DocId的下一个DocId。
时效性方面,实时增量数据需要写入索引才能生效,而召回索引服务作为读qps远大于写qps的场景,设计上需要权衡检索性能与时效性。
消费kafka实时增量后,会先写入wal-buffer中,积攒部分数据后,在线builder会构建一份delta索引;query查询时会合并base索引和多个delta索引的查询结果;delta索引的数量变多会导致读放大,定期通过index-merger将多个小的delta索引合并,保证索引查询的性能。
除了基础的文本召回功能,我们也做了大量的优化工作,包括:
分拆高频rt正排数据降低倒排索引消耗;
查询提前终止优化长尾;
支持去词双链召回解决欠召回问题;
支持语义模型分提升泛化能力;
距离相近的term构建termpair索引优化检索性能;
文本召回在B站搜索场景中占较大的比重,为综搜和各类垂搜提供标题检索、全文检索、评论检索等召回能力。
4.2 x2i召回
x2i召回多用于服务协同过滤通道,基于itemcf或者swing等算法离线挖掘出每个稿件的近似稿件列表,以用户的足迹作为trigger召回候选;也可以用于基于标签、类目的召回通道如tag2i、cat2i等。
数据类似kkv形式,pkey为trigger稿件id,skey为候选稿件id、value则可以填充候选稿件与trigger稿件的相似分静态数据。trigger从用户足迹中获取,一次召回多至上千trigger,对索引的查询性能要求极高。
我们采用B站自研的NeighborHash【1】【2】作为索引结构,缓存友好、查询性能高、吞吐大:
利用一个扁平数组来存储所有桶,每个桶包含键、值以及链中下一个位置的索引;
动态调整hash冲突元素以确保探测序列长度(PSL)最小化;
致力于将同一链上的桶放置在同一个缓存行内,以尽量减少每次查询的内存带宽使用量;
双向探测可用桶使得查询期间跨缓存行的探测次数减到最少。
为了提升排序能力,我们在i2i检索的基础上支持双塔模型打分,用于替换原有的静态分排序;根据模型目标的不同,可以进一步提升通道的个性化、相关性、一致性。
i2i索引检索之后,将候选按照 av_id % shard_num 分配到多个shard中,然后去重、查emb索引、打分多shard并发,以最大程度降低耗时。打分方面,支持fp32、fp16精度及int32、int16、int8的量化打分,也基于simd优化打分规模与性能。此外,我们也支持按trigger分配召回quota,保证召回结果的多样性。
4.3 向量召回
向量召回将user和稿件映射到同一个向量空间,然后将稿件的embeding在离线构建好索引;用户请求时,在线生成user的embedding,然后在向量空间中检索最近的k个稿件作为召回候选。
我们基于facebook开源的向量检索库faiss【3】搭建了向量召回服务,faiss能快速在大规模向量数据中找到相似向量,支持多种索引格式,包括最常用的ivf与hnsw格式。
ivf基于倒排索引的思想:
首先使用K-Means聚类算法将向量划分到不同的聚类中,每个聚类有一个聚类中心向量;
检索时首先计算查询向量与各个聚类中心的距离,确定距离最近的若干聚类;
之后遍历这些聚类内部,找到与查询向量相近的向量;从而大大缩小了搜索范围,提高了搜索效率;
hnsw则是一种基于图的索引结构:
类似于跳表的思想,它构建了一个多层的图结构,其中顶层的图比较稀疏,底层的图比较密集:
向量被插入到图中时,通过一种启发式的方法连接到其他向量,以确保在搜索时能够快速地在图中导航找到相似的向量;
当进行搜索时,从图的顶层开始,快速定位到可能包含相似向量的区域,然后逐渐深入到底层更精细的区域进行搜索,通过不断地在图中 “跳跃” 和探索邻居节点,找到与查询向量最相似的向量;
由于faiss不支持实时增量,在时效性要求较高的场景,如新稿件召回,就法充分发挥向量通道的作用。为此我们设计了一套增量更新方案:
新稿件在离线任务中请求推理服务获取embedding,通过kafka实时流接入线上服务,插入rt索引;
随着增量数据的积累,再加上读写冲突的影响,rt索引的插入和查询效率均会变慢;
让rt索引定期固化为delta索引,后续的新稿件则插入新的rt索引;
在线服务获取user-embedding后,并发查询base索引、多个delta索引、及一个rt索引,合并结果取最终的topk作为召回结果;
增量数据也会写入wal日志用于服务启动加速;
除了提升时效性,我们还在分类目索引、在线召回率监控、量化打分等方面扩展了向量召回的服务能力。
4.4 索引构建
索引构建通常放在离线进行,离线任务将原始的文本数据构建成上述索引格式,并转化为二进制形式,大大提升在线服务的启动速度。单机索引构建受限于内存资源无法支持不断膨胀的数据规模,有限的cpu资源也限制了索引产出的速度。为了提升扩展性和时效性,我们依托公司调度平台搭建了一套分布式的索引构建流程。
数据源产出后,会根据hash(key)%shard_num的方式均匀分到多个分片任务中进行处理。分片任务之间并行执行,首先进行预处理,然后由各种索引的builder将数据构建为便于加载的格式,并导出到文件系统;最后由数据平台配送到线上对应的索引服务上。
对于接入增量的索引服务,启动时除了加载天级基准索引,还需要回追kafka增量数据;一旦基准产出延迟或是增量qps大,则会导致索引服务启动时间大大拉长,给扩容带来潜在风险。为了加速服务启动,除了天级基准,离线还会定期基于增量数据构建major-dump索引。
在线服务启动时依次加载基准索引、major-dump索引,以及服务原地重启前可能产出的delta索引,可以大大降低需要回追的kafka增量,加速启动。
05.稳定性建设
召回作为搜推系统重要的一环,除了功能上持续迭代与性能深度优化,我们在多个维度做了大量工作为召回稳定性保驾护航。
首先,持续完善开发-测试-发布流程,提升上线前的异常发现能力,力求将潜在风险扼杀在摇篮之中;基于功能完善的debug平台,及时验证召回各阶段结果是否符合预期;接入班车流水线,在上线推全前对召回服务进行coredump检测,性能劣化检测等。
第二,大力强化监控告警体系的建设,在召回服务工程指标、在线召回漏斗数据、以及索引数据dqc等多层次进行监控,例如所有召回通道共有的漏斗监控,会实时监控各通道的召回数、粗排数、精排数以及通道独占召回数、粗排数、精排数;另外通道/索引特有的信息,如向量索引的召回率,x2i的拉链长度等,也会进行监控;从而实现线上问题的即时感知与精准预警,为快速响应与处理赢得宝贵时间。
第三,制定完备的降级容灾策略预案,支持通道维度、索引维度以及召回整体按比例的灵活降级方式,结合上游客户端召回兜底机制,在面对线上突发事故时,能够以最小的业务损失实现系统的平稳过渡。
最后,通过优化架构做到云原生全面达标,大幅加快召回服务的启动速度与扩容效率,有效提升系统的弹性与应变能力,从容应对各种复杂多变的业务场景与流量高峰挑战。
06.展望
当前这套召回系统已经在B站推荐、搜索等多种业务场景下广泛应用。未来还将在以下方面持续迭代优化,助力搜推系统效果提升:
跟进新一代模型召回技术发展,提供端到端召回能力;
持续提升组件化、可编排能力,支持更多场景低成本接入;
借助平台化建设,提升自动化水平,降低运维成本。
07.引用
【1】https://github.com/slow-steppers/NeighborHash
【2】https://arxiv.org/pdf/2409.0400
【3】https://github.com/facebookresearch/faiss
-End-
作者丨胡迪、明楼、HevLfreis、浪小柒
开发者问答
关于搜推系统大规模召回,大家在实践中还有什么优秀的经验和方案?
欢迎在留言区分享你的见解~
转发本文至朋友圈并留言,即可参与下方抽奖⬇️
小编将抽取1位幸运的小伙伴获取扭扭龙+b站pu定制包
抽奖截止时间:01月10日12:00
如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路
丨丨
丨丨
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...