随着互联网时代的发展,表情包成为现在大家网上交流的必备工具,针对表情搜索的产品需求,爱奇艺逗芽技术团队经历了从 ElasticSearch 到 Lucene 再到结合语义的搜索实践之路。不同阶段的技术选型可能可以为大家提供一些中小体量业务垂直领域搜索的落地思路。
逗芽表情搜索
爱奇艺逗芽表情(https://douya.iqiyi.com)是一款通过视频 AI 算法算法,针对 UGC、PGC 等来源进行表情图片生产,并在爱奇艺内外部多渠道分发的创新产品。用户通过文字输入搜索好玩有趣的表情图片是逗芽的核心功能之一。
通过文字进行表情搜索常见的请求类别包括:
实体名称, 比如热门的明星名、角色名、影视剧名等,以及实体的别名与缩写;
偏口语化的感情、动作描述 ,如“开心”,“抱抱”,“想睡了”等;
实体与动作的组合,如“加油蔡徐坤”,“虞书欣说的好”;
流行的梗、短语,如“奥力给”,“专业团队”,“我是谁我在哪”;
表达完整含义的句子,如“你好,很高兴认识你”。
在不考虑视觉语义嵌入(Visual-Semantic embedding)[1]及其他相关度比较方式的情况下, 表情图片的搜索主要依赖于用户请求与图片标签(文本格式)的相关度做检索。 标签可能包括图片中的实体、文案、动作、情绪、类别等内容,具体标签的生产一般会结合算法与人工标注,不在本次讨论范围之内。对于不同类别的标签,在匹配时可能有不同的排序策略。另外在搜索口语化的短语、短句时,需要有一定的泛化能力来满足召回。在业务层面,不同业务可能还需要过滤不同的图片来源、类型、大小等。
表情搜索的技术选型
目前关于“全文检索”比较流行的技术选型当属 ElasticSearch(ES)[2],它是一款基于 Lucene [3]的开源分布式近实时搜索引擎,对外提供了简单易用的 HTTP API。
图 1 ElasticSearch 索引分层
一个 ES 索引可以切分为多个 shard(分片)并分布式的存储,来实现数据的横向扩展,同时 Shard 也支持复制来提高数据的可用性。Segment(分段)为 Lucene 维护的最小倒排索引单元,一个 Lucene 索引可能包括一到多个 segment。新的文档会写到内存缓存并以较小的时间间隔写入新的 segment,避免耗时的大索引重建,提供了近实时检索支持。
一个图片文档主要包括三类字段:
一类,是图片元信息如各个尺寸的大小、分辨率、CDN 地址等;
二类,是运营相关信息如图片的来源、入库时间、审核、推荐状态等;
三类,是前面提到的标签信息如图片的文案、人物、表情、动作、分类标签等。
依赖上述字段 ES search API 可以通过布尔查询过滤符合业务需求的图片,再通过 function_score 自定义评分函数进行图片排序,比如不同标签字段和运营字段在匹配时可以有不同的权重。因此对不同的业务会去维护不同的相对复杂的查询 JSON 文件。
图 2 ES 图片搜索框架
在服务架构上我们使用 HBase 配合 ES 做图片总库,其中 HBase 保存了全量图片各个阶段处理的所有信息,如算法提取的特征、图片来源详情等,ES 保存了一定时间段内已去重待运营的和已审核通过的图片文档。线上搜索 ES 和运营 ES 隔离,一方面减少了搜索集群的索引文档数量(从数千万到近百万)让索引文件能尽可能在操作系统文件缓存中,另一方面避免了运营操作导致的文件缓存频繁换出。多地 ES 集群通过消息队列实现和主库间的数据同步,提供了跨地域的服务高可用。基于 ES 的搜索架构可以满足我们早期业务的搜索需求。
搜索服务的技术升级
随着对搜索需求的提高,基于 ES 的搜索服务逐渐遇到瓶颈:新接入的业务要求更高的 QPS 和更低的延时,产品也希望通过更定制化的排序逻辑来提高搜索相关度。在一段时间的原型调研后,我们开始使用 Lucene 来替换 ES 做搜索。
图 3 Lucene 架构
切换到 Lucene 意味着从 ES HTTP API 切换到 Java 库调用的方式,ES 提供的分布式索引管理和秒级近实时索引功能也不再可用。但是 Lucene 提供的接口足以让开发者通过相对简单的编码实现单个索引的创建和搜索功能。并且 Lucene 提供了如 Analyzer 在内的扩展机制让开发者能方便的做一些高级定制。
总体上切换到 Lucene 基于如下考虑:
业务对搜索内容实时性要求不高,可以按天离线创建只读索引,百万量级索引的构建在十分钟内就可以完成,在必要时(如有图片急需上下线)也可以运营通过接口手动触发索引创建。
百万量级索引大小在一百兆字节左右,可以轻松放全量保存在单机内存中,不需要做分片和搜索合并,保证了性能。
不同业务间索引隔离,配合容器可以针对业务独立部署,横向扩展和跨地域部署都极为方便。
更容易定制分词、排序等功能。
图 4 Lucene 搜索框架
服务架构上索引服务(Indexer)服务负责离线索引,定期从图片总库中创建不同业务的 Lucene 索引,搜索服务(Searcher)服务根据配置获取对应的业务 Lucene 索引,在线运行 Lucene 搜索。索引和搜索服务使用了相同的 Analyzer,定制使用了 HanLP 分词器和外部词典(ES IK [4]也支持远程词典,不过同样需要在词典更新后定期更新索引)做 Tokenizer,自定义了否定词结合 TokenFilter,确保如“不开心”这类词不被分词为“不”和“开心”。
索引服务和搜索服务通过 Zookeeper 进行索引信息的同步。不同业务方包含了不同版本号,不同版本号有不同的索引创建规则。不同时间创建的索引信息会写入对应到业务方对应版本号节点下。索引服务定期更新分词器使用的实体、停用词词典,并扫描图片总库将不同业务方需要的图片标签信息和图片 ID 写入不同的 Lucene 索引。完成写入的索引通过 forceMerge 优化索引分段,保存到对象存储保存中,并更新对应 Zookeeper 节点。搜索服务通过监听相关 Zookeeper 节点在线更新索引,并回收旧的索引。
业务服务通过 gRPC 访问搜索服务,再通过图片 ID 获取图片元信息存储返回给用户。图片元信息使用多级缓存优化性能,如内存缓存配合 CouchBase,也可以将原 ES 搜索引擎作为图片元信息的降级存储。
基于 Lucene 的搜索架构 QPS 扩展方便,性能也由原 300 毫秒的 P99 延时下降到 100 毫秒以内,搜索相关度也有提高,满足了业务需求。
语义召回
表情搜索一个比较明显的特点是请求中包含有较多日常情绪、动作类短语,在召回时如果仅使用 TF-IDF 或 BM25 做字符级别的召回,容易导致召回或相关度偏低的情况。通过引入语义层面的召回我们可以解决这个问题,比如,“兴高采烈”在 Lucene 下很难召回图片,通过语义则可以关联到“眉开眼笑”、“喜滋滋”等相关标签图片提高召回;“混吃等死”在 Lucene 下会把“等”字单独分出来导致“等着”、“等你”这类相关度比较低的图片被召回,通过语义相似度过滤也可以避免这种情况。
语义召回可以拆解为用户请求和图片标签的短文本相似度检索问题。将图片库内图片标签编码为固定长度的稠密向量,并存储到向量索引中,就可以实现用户请求和标签的最近邻查找。像 Annoy 或 Faiss 等向量索引都可以做到千万量级下的毫秒级召回。
将短文本映射到稠密向量的方式也有不少,在 NLP 领域可以被归纳到句嵌入(sentence embedding)范畴。句嵌入编码可以利用 RNN、DAN(Deep Averaging Network)、Transformer 等多种编码器对句子做编码的监督学习。下图即为 Google 2019 年提出的多语言句编码器的多任务训练框架[5]。
图 5 Google Universal Sentence Encoder 训练结构
句嵌入编码也可以直接使用预训练模型,先利用大语料非监督学习训练出来的词向量(如 Word2Vec、Fasttext)或上下文编码器(如 BERT、ELMo)进行 token 级别的编码,再通过工程或统计方式(平均、TF-IDF、SIF 等)将非固定长度的 token 编码转化为固定长度的句嵌入。
在对比测试并考虑开发成本的基础上,逗芽目前使用了预训练词向量取平均的方式进行句嵌入编码,并进行了一些针对性优化。
我们采用基础分词加 BiMM (Bi-directional Maximal Matching)的方式来尽可能匹配词向量词典中更长的词,提高短语、短句的匹配度。其次由于词向量本身基于词语在语料中的上下文训练,导致不少反义词或同类词向量空间中距离较近,如“难过”和“开心”,“美女”和“帅哥”,不符合语义召回的预期,我们通过自定义反义词词典进行了词向量 counter-fitting [6]微调,提升语义相关度。
图 6 Glove 词向量最近邻 counter-fitting 前后对比
语义召回的离线索引和在线检索的服务均由 Python 实现,对外提供 gRPC 接口。词向量和文案标签使用 Annoy [7]索引存储,词向量相对需要较多内存(8G 左右),但是 Annoy 通过 mmap 进行加载,多个进程间可以共享内存,内存使用效率较高。服务通过容器平台部署,语义召回平均请求耗时约 15 毫秒,单机 QPS 约 500。
排序策略
在引入语义召回后我们把用户搜索请求做实体与非实体的区分,实体部分直接通过 Lucene 召回。非实体部分 Lucene 和语义都做召回,Lucene 召回部分对匹配的长度和顺序做了较严格的要求,语义召回部分会找到相似的 topN 标签,再过 Lucene 全匹配召回对应图片。
图 7 结合语义召回
搜索服务在拿到语义和实体的召回后再进行统一排序,排序过程中我们将实体部分和非实体部分的相关性得分压缩到同一分数区间内,然后辅助以图片质量分进行排序。其中图片质量分会在索引阶段通过图片来源、时效性、热度等信息预先计算好,在排序时再从索引中读取。在加入语义和完善排序策略后搜索相关度有了明显提升,针对高频和非高频搜索请求测试 NDCG [8]分数提高近 20%。
神配图应用
神配图是将用户输入直接添加到图片上的一种新型表情交互方式,神配图返回的图片也可以作为表情搜索的一个较好补充,尤其在用户搜索不到图片的情况下。
图 8 神配图示例
神配图原始图片的召回和前面语义召回部分类似,原始图片的文案、情绪、动作等标签经过句编码后构建成向量索引,用户请求句编码后在向量索引上做最近邻召回。在接口返回前再对神配图召回和搜索结果进行融合与排序。
由于文字需要实时添加到图片上,我们目前使用了读请求的后端加字方式,即搜索结果返回时并没有真正的图片加字,只有在用户端上请求访问图片的 CDN 地址时并回源时才进行图片的生产,保证了接口性能和整体用户体验。
总结和展望
本文回顾了逗芽表情搜索在不同业务阶段的不同实现方式。在定制化要求不多,实时性和吞吐量需求也不是很强的情况下 ElasticSearch 可以的满足大部分使用场景。在定制化和服务能力需求更高的情况下,可以先基于 ES 做针对性的优化和配置,如果还不能满足需求可以考虑直接使用 Lucene。在语义召回方面,可以优先使用预训练模型,以较低的开发成本获得相对明显的提升。如果服务体量更大可能需要从倒排索引开始自建搜索引擎,就不在本次讨论范围之内了。
目前逗芽表情搜索还有很多待优化的空间,例如标注语料优化句编码模型让语义召回更准确;引入更多维度特征或 L2R 优化排序;支持实时性更高的增量索引等等。产品上还有不少和搜索相关的应用场景可以继续挖掘,如个性化推荐、表情对话、以图搜图、基于视觉语义的图片检索等等。
搜索是一件原型容易,优化难的开发任务。软件工程没有银弹,搜索实现也没有,但是作为开发者我们可以根据团队资源和业务需求找到合适的搜索落地之路。
参考文档
[1] 视觉语义嵌入(Visual-Semantic embedding)Frome, A. 2013. Devise: A deep visual-semantic embedding model
[2] ElasticSearch(ES): https://www.elastic.co/cn/
[3] Lucene: https://lucene.apache.org/
[4] ES IK: https://github.com/medcl/elasticsearch-analysis-ik
[5] 多语言句编码器的多任务训练框架: https://ai.googleblog.com/2019/07/multilingual-universal-sentence-encoder.html
[6] counter-fitting: Nikola Mrki. 2016. Counter-fitting Word Vectors to Linguistic Constraints
[7] Annoy: https://github.com/spotify/annoy
[8] NDCG: https://en.wikipedia.org/wiki/Discounted_cumulative_gain
本文转载自公众号爱奇艺技术产品团队(ID:iQIYI-TP)。
原文链接:
评论