爱奇艺逗芽表情搜索分析与实践
爱奇艺逗芽表情(https://douya.iqiyi.com)是一款通过视频AI算法算法,针对UGC、PGC等来源进行表情图片生产,并在爱奇艺内外部多渠道分发的创新产品。用户通过文字输入搜索好玩有趣的表情图片是逗芽的核心功能之一。
通过文字进行表情搜索常见的请求类别包括:
1. 实体名称,比如热门的明星名、角色名、影视剧名等,以及实体的别名与缩写;
2. 偏口语化的感情、动作描述,如“开心”,“抱抱”,“想睡了”等;
实体与动作的组合,如“加油蔡徐坤”,“虞书欣说的好”;
流行的梗、短语,如“奥力给”,“专业团队”,“我是谁我在哪”;
表达完整含义的句子,如“你好,很高兴认识你”。
在不考虑视觉语义嵌入(Visual-Semantic embedding)[1]及其他相关度比较方式的情况下,表情图片的搜索主要依赖于用户请求与图片标签(文本格式)的相关度做检索。标签可能包括图片中的实体、文案、动作、情绪、类别等内容,具体标签的生产一般会结合算法与人工标注,不在本次讨论范围之内。对于不同类别的标签,在匹配时可能有不同的排序策略。另外在搜索口语化的短语、短句时,需要有一定的泛化能力来满足召回。在业务层面,不同业务可能还需要过滤不同的图片来源、类型、大小等。
目前关于“全文检索”比较流行的技术选型当属ElasticSearch(ES)[2],它是一款基于Lucene [3]的开源分布式近实时搜索引擎,对外提供了简单易用的HTTP API。
图1 ElasticSearch索引分层
一个图片文档主要包括三类字段:
一类,是图片元信息如各个尺寸的大小、分辨率、CDN地址等;
二类,是运营相关信息如图片的来源、入库时间、审核、推荐状态等;
三类,是前面提到的标签信息如图片的文案、人物、表情、动作、分类标签等。
依赖上述字段ES search API可以通过布尔查询过滤符合业务需求的图片,再通过function_score自定义评分函数进行图片排序,比如不同标签字段和运营字段在匹配时可以有不同的权重。因此对不同的业务会去维护不同的相对复杂的查询JSON文件。
图2 ES图片搜索框架
图3 Lucene架构
总体上切换到Lucene基于如下考虑:
1. 业务对搜索内容实时性要求不高,可以按天离线创建只读索引,百万量级索引的构建在十分钟内就可以完成,在必要时(如有图片急需上下线)也可以运营通过接口手动触发索引创建。
2. 百万量级索引大小在一百兆字节左右,可以轻松放全量保存在单机内存中,不需要做分片和搜索合并,保证了性能。
3. 不同业务间索引隔离,配合容器可以针对业务独立部署,横向扩展和跨地域部署都极为方便。
4. 更容易定制分词、排序等功能。
图4 Lucene搜索框架
索引服务和搜索服务通过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前后对比
排序策略
在引入语义召回后我们把用户搜索请求做实体与非实体的区分,实体部分直接通过Lucene召回。非实体部分Lucene和语义都做召回,Lucene召回部分对匹配的长度和顺序做了较严格的要求,语义召回部分会找到相似的topN标签,再过Lucene全匹配召回对应图片。
图7 结合语义召回
神配图是将用户输入直接添加到图片上的一种新型表情交互方式,神配图返回的图片也可以作为表情搜索的一个较好补充,尤其在用户搜索不到图片的情况下。
图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
也许你还想看