BasicEngine — 基于DII平台的推荐召回引擎

本文涉及的产品
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
智能开放搜索 OpenSearch向量检索版,4核32GB 1个月
OpenSearch LLM智能问答版免费试用套餐,存储1GB首月+计算资源100CU
简介: BasicEngine是阿里巴巴搜索事业部自研的推荐在线召回引擎,依托强大的搜索底层技术支持,可以在线实现复杂的关联排序运算,支持灵活的推荐策略组合,为推荐系统的升级发展拓展了无限想象空间。

一、诞生

时光回到2016年,那时候阿里推荐系统的核心召回逻辑还称为"u2i",即离线计算好user到推荐item的关系,在线仅负责简单查询。在"u2i"模式中,受限于计算能力和产出时间的要求,无法将全网user与i2i全量表完整关联,通常都需要将i2i全量表前置过滤、截断、然后再与user行为表join产出u2i表,最后回流线上iGraph提供查询。这是一种重离线轻在线的架构,主要存在几大问题:一是浪费离线计算资源,每一个业务接入、每一次效果迭代,都需要针对所有user做一次全量计算;二是浪费iGraph在线存储资源,每一个业务的每一种策略,都会存在一张完整的u2i表,数据规模巨大;三是迭代周期慢,每一次截断和排序策略的调整,都要经过全量离线计算和数据回流、等待几个甚至十几个小时以上。2016双十一过后,推荐算法团队和搜索工程团队合作,启动了推荐offline2online项目,希望通过将笨重的u2i离线计算搬到在线,达到节约整体资源、加快迭代效率、提升召回计算能力的目的。经过三个月的协作努力,项目成功发布,推荐召回系统终于从老牛车改造成了小汽车,也是在这个项目中,借助DII的管控平台和服务框架,针对召回逻辑的抽象和封装,让我们产出了BasicEngine这个推荐在线召回引擎(后面简称BE)。简单来说,BE是一个典型的多列searcher+proxy的架构,各种i2i/s2i/b2i的召回(search)、合并(union)、关联(join)、过滤(filter)和排序(sorter)均在searcher本地完成,最后经过proxy的合并排序(merge sorter)返回出去。BE的整体架构图如下:
be_arch.png | center

经过半年多的努力,手淘首焦、首图、卡片、清单、生活研究所、购后链路猜你喜欢、猫客猜你喜欢等大场景都成功接入了BE,既提升了迭代效率又拓展了优化空间,均取得了不错的业务效果提升。在过去的2017双十一中,BE也成功经受了大促需求频繁变更和峰值流量压力的双重考验,收获了第一个完美的双十一。下面的篇幅,就简单给大家汇报一下BE这半年多来的演进,同时这也是对DII/BE平台"从一到一百"能力的一次验证。

二、演进

1. 配置化

BE的searcher上的核心召回流程是多表的union和join操作,一旦union/join操作的树结构可以灵活配置,就可以适配绝大部分场景的业务需求,因此我们做的第一步改造就是让BE支持可配置的执行树结构。一起来看一段配置文件样例:

       <query type="join" join_field="item_id">
          <sorter name="aio_biz_top_sorter">
            <scorer name="aio_rule">
              <init_params>
            </init_params>
            </scorer>
          </sorter>
          <left_query>
            <query type="union">
              <merge_strategy name="type_based" uniq_field="item_id">
              </merge_strategy>
              <filter name="notin_filter">
              </filter>
              <children_query>
                <query type="table" table_name="hpaio_i2i_be" table_type="kkv" query_field="item_list" degrade_seek_count="200" match_type="1">
                </query>
                <query type="table" table_name="hpaio_i2i_be" table_type="kkv" query_field="off_item_list" degrade_seek_count="800" match_type="2">
                </query>
              </children_query>
            </query>
          </left_query>
          <right_query>
            <query type="table" table_name="hpaio_1111_content_info" table_type="kkv">
              <filter name="aio_content_filter">
              </filter>
            </query>
          </right_query>
        </query>

对应的执行树结构如下,与配置文件的映射关系应该是一目了然的了。

                 aio_biz_top_sorter
                         |
           |----------  join -------- |
           |                          |
      not-in-filter                   |
           |                          |
         union                         |
           |                  aio_content_filter
     |-----|------|                   |
     |            |                   |
hpaio_i2i_be  hpaio_i2i_be  hpaio_1111_content_info 
  • 注:hpaio_i2i_be出现两次是通过不同的trigger来查询的,分别对应了实时trigger和全量trigger.

BE的proxy上除了对searcher返回的结果进行merge sort之外,另一个常见的操作就是访问iGraph上的曝光或者购买类目等user维度的表,然后将返回结果传递到searcher上做曝光过滤或购买类目过滤。不同的业务在访问的iGraph表名、过滤条件、返回字段上都有差异,因此我们也将iGraph的访问和结果解析做成了配置化的方式。还是来看一段示例感受一下:

      <!-- igraph请求列表  -->
      <igraph_query_list>
        <igraph_query name="xxx_user_bought_leafcast_igraph">
          <table_name>xxx_user_bought_leafcast_igraph</table_name>
          <pkey>${user_id}</pkey>
          <range>5000</range>
          <return_fields>leafcat</return_fields>
        </igraph_query>
        <igraph_query name="TPP_user_exposure_log_for_all">
          <table_name>TPP_user_exposure_log_for_all</table_name>
          <pkey>${user_id}</pkey>
          <filter><![CDATA[scene="3553"]]></filter>
          <range>5000</range>
          <return_fields>item_id</return_fields>
        </igraph_query>
      </igraph_query_list>
      <!-- igraph返回结构解析列表  -->
      <igraph_result_parser_list>
        <igraph_result_parser type="send_searcher_request">
          <query_name>xxx_user_bought_leafcast_igraph</query_name>
          <need_hash>false</need_hash>
          <id_seperator>|</id_seperator>
          <result_field>leafcat</result_field>
          <search_query_field>filter_leafcat_list</search_query_field>
        </igraph_result_parser>
        <igraph_result_parser type="send_searcher_request">
          <query_name>TPP_user_exposure_log_for_all</query_name>
          <need_hash>true</need_hash>
          <id_seperator>,</id_seperator>
          <result_field>item_id</result_field>
          <search_query_field>exposed_list</search_query_field>
        </igraph_result_parser>
      </igraph_result_parser_list>

这段配置的含义是以request中的user_id参数值为key请求iGraph的xxx_user_bought_leafcast_igraph和TPP_user_exposure_log_for_all,分别返回若干leafcat和item_id,然后将这些leafcat和item_id按指定方式加入到传给searcher的request中去,便于searcher上获取参数值并使用。

BE的searcher/proxy上分别支持了这两种配置化方式之后,接入一个新场景的主要工作就聚焦在filter/sorter插件逻辑的编写上了,为了更大程度减少接入成本,我们的目光又投向了filter/sorter插件的通用化。

2. 通用插件

  • 基于集合的过滤
    BE内置支持in/not-in filter,无需编码,类似曝光或购买过滤这种需求直接配置使用即可。
  • LR/GBDT算分
    BE内置支持LR/GBDT模型算分功能,用户只需要根据场景导入模型并配置好fg文件即可使用。BE上主要进行的是召回之后的粗排序,一般到LR/GBDT这种复杂度已经足够用了,如果有更复杂的深度模型,可以在精排阶段的RTP上实现。
  • trigger信息透传
    在很多场景中,都希望将trigger的信息透传到粗排算分过程中,BE内置支持这个功能,只需要配置上透传字段的名称和类型,就可以获取到request中带过来的透传字段信息。假设request中的trigger格式是trigger_id:trigger_score:timestamp:count,则可以按如下格式配置透传信息:
<extend_key value="key"/>
<extend_fields>
    <field name="trigger_score" type="float"/>
    <field name="trigger_timestamp" type="int64"/>
    <field name="trigger_count" type="int64"/>
</extend_fields>
  • 打散排序
    从目前接入的场景来看,大部分场景的粗排序都可以归纳为打散排序,我们也正在开发一个通用的打散排序插件,这样类似"每个类目下最多出3个"这种需求也无需额外写代码来实现了。

3. 实时更新

在离线u2i的方式下,实时更新几乎是不可能完成,但是在BE上进行实时更新却变得非常简单,依赖DII平台底层的搜索技术体系,无论是i2i关系对还是精品库/素材库的新增、删除、更新,都可以轻松做到秒级生效。双十一大促中,我们就有多个场景将这种能力封装暴露给运营同学,让他们可以实时干预精品库/素材库,轻松应对大促紧急需求。

4. 性能优化

作为底层的在线召回引擎,高性能毫无疑问是我们的基石。还记得BE刚完成在猜你喜欢场景上线的时候,按照当时的性能评估,如果要支撑首页、购后等场景的双十一流量,预估需要8w+ core的计算资源,但是经过我们持续不断的性能优化,双十一当天实际使用的计算资源是4w core。

4.1 存储引擎升级

在BE上线之初,i2i关系是以倒排表的方式存储的,虽然功能上是支持了根据triggr-id查询得到triggered item-id-list,但是性能很差。由于i2i关系只需要支持一种查询方式,用不到复杂的and-or组合查询,k-k-v才是更合适的存储格式。k-k-v存储上线之后,BE在i2i查询性能上提升了20%+,在某些右表(素材和宝贝之间也是一对多的关系)的查询上更是提升了100%以上。

4.2 通信协议升级

通信协议涉及两方面,一是BE proxy与searcher之间的协议,二是BE与调用方TPP场景之间的协议。
在第一版中,BE的proxy/searcher之间通信使用的是明文query的方式,简单来说就是proxy上把请求按照kvpairs的方式拼接成字符串发给searcher,searcher再去执行url decode和kvpairs split,典型场景的query长度通常都会达到几十k,这种方式显然是非常低效的。所以在后续的版本中,我们把searcher改成了arpc server,proxy/searcher之间的通信也改造成了基于protobuf的rpc协议,这样一来就把协议本身的overhead降低了很多,整体性能也有了很大提升。
BE的proxy与TPP场景之间默认的结果输出格式是json,json格式对调试非常方便,但是当数据量增大到一定规模之后,性能消耗也是非常明显的,因此我们又新增了一种基于flatbuffer协议的格式,flatbuffer是一种非常高效的序列化/反序列化协议,详情可以参考google flatbuffer,在实际场景测试中发现,当返回结果大小超过2000个以上的时候,flatbuffer相比json可以将BE的序列化性能提升50%,TPP场景的总体性能可以提升30%。

4.3 异步化改造

异步化改造主要是针对BE的proxy而言,在早期的版本中,proxy与searcher的交互模式是:一次性批量发送所有请求给searcher,然后再同步等待所有请求返回。这种同步等待的模式带来的后果是,需要配置非常多的工作线程才能提升proxy的单机qps,并且由于大量线程带来的切换代价,proxy的cpu也很难压上去。因此我们基于SAP的异步框架将proxy访问searcher改造成了异步模式:一次性批量发送所有请求给searcher,然后让出线程,等待所有searcher请求返回后再重新调度起来运行后续流程。异步化改造之后,proxy的线程数量大大降低,并且cpu可以压到90%+以上,服务rt也更加平滑。

4.4 执行流程细节的优化

半年来我们对BE的执行流程做了很多细粒度的优化,这里不做一一介绍,简单聊几点优化过程中的心得。1.高频路径上避免使用stl stringstl string是一个非常好用的数据结构,尤其是面向用户的接口上,百分百友好。但是在一些高频路径上,string的频繁构造和析构很伤性能,要尽量避免,多考虑使用(const char* + size_t)或者int64_t替换string。举一个实际的例子,BE的逻辑中经常会涉及到将一个字符串const string切割成多个子串,然后将子串解析成数值类型的逻辑,粗犷的做法都是先split得到vector<string>,然后再挨个转换,其实这个vector<string>的代价是非常高的,相反如果我们仅做一次拷贝得到string_copy,然后在string_copy上原地使用\0切割并做类型转换,可以获得很大的性能收益。2.让CPU少干活,能用static_cast就不用dynamic_cast,能用float就不用double,能用int就不用float。3.点滴汇成海,越是细节的地方越不能忽视,写代码的时候要清楚知道每段逻辑的调用频率,合理在性能和简单之间做取舍。

系统实现层面的优化重要性不言而喻,但是业务方数据层面的优化也是不可或缺的,并且往往能取得立竿见影的效果,举个手淘首页卡片场景的例子,在指标无损的前提下,算法同学对数据的精简和优化一次就可以提升30%以上的性能。所以,任何一个系统的性能优化效果,都应该是系统和业务结合达成的。

5. 向量召回

在过去的半年中,BE在功能上的最大改进就是支持了向量索引的构建和召回。我们深度集成了开源的KNN库–FAISS,改造定制使其支持向量索引的分布式构建和查询,并融合到搜索的技术体系中。向量召回作为BE中一种新的召回策略,可以灵活地与其他召回策略组合使用。向量召回也在手淘猜你喜欢、猫客猜你喜欢等场景成功上线使用,支撑了算法同学在DeepMatch上的探索,并取得了不错的成绩。

c1211a05-425d-4818-89ae-11fbdb55be18.png | center | 582x312

6. 自动降级

自动降级是系统保命的绝招,尤其是应对推荐这种可能出现无法预测的流量凸起的场景,如果没有自动降级,系统可能会在瞬间崩溃掉。借鉴HA3引擎的成功经验,BE的proxy/searcher也都开发支持了自动降级功能。proxy的自动降级主要依赖于gig,一方面可以根据服务能力合理分发流量将多行searcher的cpu打平,另一个方面可以在searcher负载过高时选择随机丢列,减轻searcher整体负载。searcher的自动降级策略主要是在高负载的情况下减少召回的文档数,从而减少计算量、减轻cpu压力。BE上线了自动降级功能之后,可以轻松应对比正常峰值高出一倍的压力,这也让我们在searcher CPU跑到90%依然镇定自若。

三、2017双十一

2017双十一,BE支撑了手淘推荐首焦、首图、卡片、生活研究所、猜你喜欢以及Matrix内容/珠峰调控等场景的召回业务,入口叠加峰值接近30w、峰值最大延迟低于35ms、峰值cpu整体利用率达到55%,单场景最大i2i表100亿+、召回宝贝数3w+,单场景最大返回结果数4000+。整个过程如丝般顺滑,不但大促当天的素材和业务预案调整轻松面对,还通过各个场景的扩缩容始终保持着系统整体水位的合理水平。

四、未来

过去的半年,是BE支撑业务架构升级、快速粗放发展的半年,接下来我们会做深做细,紧密结合业务需求,从提升体验、增强功能、优化性能三方面入手继续前进。1.提升体验,功能组件化、配置web化,都可以极大降低用户的接入、开发、迭代成本,是我们需要花大力气去完善的基础设施。2.增强功能,从最开始的i2i召回,到逐渐铺开的向量召回,推荐的召回逻辑也在演进中,BE会随着业务需求提前做好技术储备和系统支撑,帮助业务探索和尝试。3.优化性能,追求极致性能永无止境。

目录
相关文章
|
安全
统一召回引擎
统一召回引擎
67 0
|
搜索推荐
统一召回引擎的优势
统一召回引擎的优势
106 0
|
编解码 达摩院 监控
阿里云 Elasticsearch 向量检索,轻松玩转人脸识别、搜索推荐等29个业务场景
简介:我们知道,市面上有不少开源的向量检索库供大家选择使用,例如 Facebook 推出的 Faiss 以及 Nswlib,虽然选择较多,但业务上需要用到向量检索时,依旧要面对四大共性问题。
10726 1
阿里云 Elasticsearch 向量检索,轻松玩转人脸识别、搜索推荐等29个业务场景
|
新零售 自然语言处理 运维
一文详解 | 开放搜索兼容Elasticsearch做召回引擎
开放搜索发布开源兼容版,支持阿里云Elasticsearch做搜索召回引擎,本文详细介绍阿里云ES用户如何通过接入开放搜索兼容版丰富行业分词库,提升查询语义理解能力,无需开发、算法投入,即可获得淘系同款搜索效果。
1541 0
|
5月前
|
运维 监控 搜索推荐
客户案例 | 识货基于向量检索服务 Milvus 版搭建电商领域的向量数据检索平台
阿里云的Milvus服务以其性能稳定和功能多样化的向量检索能力,为识货团队在电商领域的向量检索场景中搭建业务系统提供了强有力的支持。该服务的分布式扩展能力不仅可靠,而且能够适应日益增长的数据规模。
客户案例 | 识货基于向量检索服务 Milvus 版搭建电商领域的向量数据检索平台
|
6月前
|
Linux 异构计算 Docker
QAnything本地知识库问答系统:基于检索增强生成式应用(RAG)两阶段检索、支持海量数据、跨语种问答
QAnything本地知识库问答系统:基于检索增强生成式应用(RAG)两阶段检索、支持海量数据、跨语种问答
QAnything本地知识库问答系统:基于检索增强生成式应用(RAG)两阶段检索、支持海量数据、跨语种问答
|
机器学习/深度学习 自然语言处理 数据挖掘
向量召回:深入评估离线体系,探索优质召回方法
向量召回:深入评估离线体系,探索优质召回方法
向量召回:深入评估离线体系,探索优质召回方法
|
机器学习/深度学习 搜索推荐 数据管理
语义检索系统:基于Milvus 搭建召回系统抽取向量进行检索,加速索引
语义检索系统:基于Milvus 搭建召回系统抽取向量进行检索,加速索引
语义检索系统:基于Milvus 搭建召回系统抽取向量进行检索,加速索引
|
机器学习/深度学习 运维 搜索推荐
智能引擎搜索-基于问天引擎的智能搜索推荐算法开发|学习笔记
快速学习智能引擎搜索-基于问天引擎的智能搜索推荐算法开发
智能引擎搜索-基于问天引擎的智能搜索推荐算法开发|学习笔记
|
存储 Prometheus 运维
阿里云ES全观测引擎TimeStream时序增强功能重磅发布,助力时序场景实现最佳实践
阿里云ES全观测引擎TimeStream时序增强功能最新发布,在云原生ELK全托管基础上,通过TimeStream时序增强功能插件,可实现高性能、低成本时序数据存储和查询分析。本文介绍TimeStream适用场景、功能优势、性能测试结果和实践案例
2192 0
下一篇
无影云桌面