本文作者: [禾丹、刘杰]
随着用户持续使用云音乐,红心歌曲、收藏歌单、关注艺人等用户私域数据资产也在不断积累,面向私域数据的检索诉求也越来越迫切;本文主要介绍云音乐本地私域数据检索功能的实现方案,包含本地轻量级搜索引擎的技术选型、整体技术方案以及搜索耗时的优化方案。
用户本地私域数据检索
云音乐有着强大的推荐系统,用户在使用云音乐过程中,会通过红心标记喜欢的歌曲,会通过收藏标记感兴趣的歌单专辑,会通过关注持续获取喜爱艺人的信息,这些因用户行为而被关联的资源、状态数据都属于用户自己的私域数据。私域数据会被App在本地记录,然后通过云端在不同的设备上进行数据同步。
随着用户持续使用云音乐,私域数据持续不断积累,私域数据搜索的诉求反馈也越来越多,比如找到自己众多歌单里五月天的歌曲,某首只记得若干关键字的红心歌曲,等等。目前云音乐已通过内置一个轻量级本地搜索引擎实现了该功能,相比由服务端完成用户私域数据检索,返回结果由客户端展示的方案,本地搜索引擎在保护用户数据隐私、节省云端存储成本、降低检索耗时、支持离线搜索上有着天然优势。
本文主要介绍该功能的技术方案,包含本地轻量级搜索引擎的技术选型、整体设计方案、性能优化和总结展望等。
<img src = "https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/35121511182/6c17/3ebb/c566/186d456753108858452d240742056197.png">
搜索引擎
本地私域数据搜索功能是通过在云音乐内置轻量级全文搜索引擎来实现的,为了更好地描述和理解后续的技术方案,先来回顾下搜索引擎的一些基础知识。
搜索引擎工作流程
搜索引擎主要分为:爬取(Crawl)、解析(Analyze)、索引(Index)、检索(Search)和排序(Rank) 5个阶段。
<img src = "https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/35243090641/c746/b27a/d659/2867b6dadd547fbf8cb5a139f0fdd92f.png">
- 爬取:即常见的应用实现爬虫程序,爬虫程序以深度或广度的方式扩展搜索web页面,并进行页面元数据的保存;
- 解析:对爬虫爬取到的数据进行格式化、过滤、重建等处理,复杂的解析器还会进行如标题抽取、摘要生成、关键词提取、内容标签等处理;
- 索引:索引器对解析处理后的爬取数据进行信息索引表构建,索引表可以帮助搜索引擎快速检索到相关信息,常见的索引建立方式有正排索引、倒排索引等;
- 检索:当用户键入查询内容(Query)后,搜索引擎会通过自然语言处理技术理解用户Query,对query进行分词,再通过对索引的查找返回Query关联的查询结果,这个阶段也被称为初筛或召回;
- 排序:在检索结果的基础上,搜索引擎首先会基于算法模型对检索结果进行排序,然后通常会引入用户特征、内容特征等信息对搜索结果进行再排序,以使结果更加符合用户的搜索期望;搜索引擎结果排序通常会包含粗排、精排等多个排序阶段。
全文搜索引擎
全文搜索引擎是目前最广泛应用的主流搜索引擎,面向文本检索,以网页文字为主。全文检索下,当一条文档数据被存储时,解析器与分词器会将该文档数据划分成各自独立的词项,并为每个词项建立一个倒排索引。当进行查询时,查询数据也会被解析器与分词器进行词项划分,然后遍历倒排索引,找到匹配的已存储文档数据,最后基于文档数据与查询条件的相关性进行排序,返回最终查询结果。
<img src = "https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/35122035182/17b3/5a34/3620/1d5b1e203652a844525e19fe2e23b1b9.png">
轻量级全文搜索引擎方案调研
搜索引擎方案选型
通过对现有搜索引擎方案的调研,基于端侧集成成本考量,最终将引擎方案选定在 NSearch 和 SQLite FTS之间:
NSearch 是云音乐自研的高性能检索引擎,其特性有:
- 融合了搜索系统的信息检索和推荐系统的召回能力;
- 在召回类型上支持关键字查询,文本查询,数字范围查询和向量召回等;
- 召回后按照文本相关性、向量相似性进行排序,并支持自定义排序规则,最终尽可能多的返回正确的结果。
SQLite FTS(Full-text Search) 是 SQLite 提供的全文搜索引擎,提供了强大的文本搜索功能,其特性有:
- 支持多列文本数据的全文搜索;
- 使用高效的倒排索引技术,允许快速搜索;
- 支持自然语言查询,可以处理如停用词、词干形式等;
- 支持布尔操作符(AND, OR, NOT)进行复杂搜索;
- 支持简体中文、繁体中文、英语等多种语言的全文搜索;
<img src = "https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/35090156752/7d66/f574/a1b8/15237f8b0a417a97294d532f51fd9cd8.png">
进行全文检索的核心是建倒排索引,建索引的核心是分词器,分词的效果直接影响了搜索的结果;通过对两套方案的分析,考虑到研发成本和对包大小的影响,若 SQLite FTS 能够很好的支持中文分词,则会是更优选项。
SQLite FTS 介绍
SQLite 作为目前移动端使用最为广泛的嵌入式数据库,SQLite3其实已经内嵌了离线全文搜索的扩展模块——FTS,包含 分词解析、倒排索引构建、文本匹配查询 等核心功能,并支持分词解析的三方插件扩展。FTS当前已发布了5个版本,现在大部分使用的主要为FTS4、FTS5,FTS5相比FTS4进行了诸多兼容性修复和存储优化,其详细差异参见官方介绍文档:FTS5 与 FTS3/4 的比较。
在实际使用时,FTS可以理解成是一个表,为数据库应用程序提供全文本搜索功能,相比于普通表,FTS其实是种虚拟表模块。基于FTS5的全文检索使用SQL语法,使用时包含以下4个关键步骤(以歌曲检索为例):
创建信息表,用于存储要被检索的信息,信息扩展可以通过空格字符串拼接来实现;
// 创建歌曲信息表 CREATE TABLE IF NOT EXISTS songindex ( song_idINTEGER PRIMARY KEY, name TEXT, alias TEXT, artist_name TEXT, other TEXT )
通过关键字 USING fts5 创建一个包含一列或多列的 FTS5 虚拟表;虚拟表创建时,SQLite会在数据库中创建若干个普通表用于存储物理数据,它们被称为影子表(shadow tables);
// 创建对应歌曲索引表的fts表 CREATE VIRTUAL TABLE IF NOT EXISTS songindexfts5 USING fts5 ( song_id UNINDEXED, name, alias, artist_name, other, content='songindex', content_rowid='song_id', tokenize='simple' )
通过关键字 TRIGGER 创建FTS虚表更新器,其作用在于,当信息表中的数据发生变化时,FTS虚表会进行同步更新;
// 使用trigger创建fts表更新器 CREATE TRIGGER IF NOT EXISTS triggerinsert AFTER INSERT ON songindex BEGIN INSERT INTO songindexfts5 ( rowid, name, alias, artist_name, other ) VALUES ( new.song_id, new.name, new.alias, new.artist_name, new.other ); END;
- 通过关键字 MATCH 检索获取结果
// 进行全文搜索匹配
SELECT * FROM songindexfts5 WHERE songindexfts5 MATCH 'keyword'
更多详情及函数接口参见官方介绍文档:SQLite FTS5 扩展。
分词器
SQLite FTS 内置分词
分词器运行在 建索引 和 查询 两个阶段,承担 建索引分词 和 查询分词,是FTS的核心,没有分词器模块,FTS就没法工作;例如一段文本“网易云音乐”,可能被拆分为“网易、云音乐”,也可能本拆分为“网、易、云、音、乐”,最终检索结果也完全取决于分词器的拆分。
SQLite也提供了相关分词器插件,比如simple、icu、unicode61等,只有icu、unicode61支持中文;但unicode61按标点拆分,不可用;icu是按字拆分的,可以用,但检索结果比较乱,不符合中文检索的习惯和诉求,中文检索需要能够支持 字、字组、词、拼音、拼音首字母缩写等检索。
三方分词插件simple
simple是微信开源的一个支持中文和拼音的SQLite FTS5 三方分词插件,在其原有中文分字能力上,支持通过cppjieba 实现更精准的词组匹配。更多实现原理和细节可参考其开源介绍:simple: 一个支持中文和拼音搜索的 sqlite fts5插件。
simple分词处理
检索分词
- 空白符跳过不处理;
- 连续的数字被当作整体,转换为一个索引;
- 连续的英文字母被当作整体,并转换成小写索引;
- 中文字单独建索引,并会对中文字拼音也建搜索,这样就能同时支持中文和拼音检索;另外拼音首字母也会建索引,这样搜索 "zjl" 就能命中 "周杰伦";
- 其他字符统一单独建索引,也可以被搜索到;
查询分词
- 如果查数字,要把搜索词当作前缀来用,比如用户搜索 123, query 就需要换成 123*,这样如果索引里面有 12345 也能被搜索出来;
- 对于英文,除了要当作前缀,还需要把搜索词转成小写,比如用护搜索 Hello,query 就需要换成 hello*, 这样如果索引里面有 HelloWorld 也能被命中;
- 对于中文和其他字符,都要拆成单个的才能命中索引;
- 对于拼音(其实我们没办法区分英文和拼音,统一当作拼音处理就行),需要把拼音按照规则拆分,因为我们的拼音索引是单字建立的;这样如果用户搜索 "zhangliangy",拼音就可以被拆成 'zhang AND liang AND y*',从而命中"张靓颖”;
基于simple的检索效果测试
simple能够很好的支持 字、字组、词、拼音、拼音首字母缩写 等检索。
测试数据
- 100条云音乐单曲数据
- 每条数据包含7个字段:ID, Name, ArtistName, AlbumName, Alias, ArtistAlias, AlbumAlias;
- 100条数据DB大小约为128KB,按照线性评估,1w条数据大约12MB左右;
测试结果
ICU 和 simple 分词插件效果比对
simple在中文检索上效果要远好于 ICU,更符合中文检索习惯。
测试数据
songName = "三里屯的夜"
albumName = "署前街少年"
artistName = "赵雷"
测试结果
综上调研分析可知,SQLite3 FTS5 + Simple 分词插件 是本地全文搜索引擎的最佳方案。
云音乐本地私域搜索设计方案
产品设计上,功能入口基于主搜页面做扩展,当用户输入搜索query触发云端搜索时,会同步进行本地私域数据搜索;本地搜索可搜索内容包含用户 创建/收藏歌单、红心歌曲、订阅艺人、已购专辑、最近收听 数据。
技术方案
客户端内置一个轻量级全文搜索引擎进行数据检索,考量到排序策略需要不断迭代调优,对灵活性和动态性要求较高,基于云音乐跨端基建考量,排序跨端选型JS来实现,检索结果通过JS执行排序并返回最终展示结果给客户端做渲染展示。整体方案如下图所示。
轻量级搜索引擎
- 基于 SQLite3 FTS5 + simple分词插件 实现;
索引更新时机
- 版本第一次启动 和 用户发生内容消费行为,如 播放、红心、收藏等;
关联检索和模糊检索
- simple分词器不支持Query的关联分析和纠错,因而本地搜索引擎也就不具备关联检索和模糊检索的能力;
- 但可以通过向服务端发送Query修正请求来实现,服务端返回关联query和纠错后的Query,再交由本地搜索引擎进行文本匹配检索;
结果排序
- 结果排序包含 2 轮排序逻辑运算;
- 第一轮排序,计算文本匹配分,按照匹配度降序排列;
- 第二轮排序,基于文本匹配排序,计算用户行为(红心、收藏、收听次数等)加权分,得到最终排序结果;
性能优化
搜索耗时是用户搜索体验和内容消费的关键影响指标,耗时越少,用户体验越好。
耗时分析
通过对搜索过程每个步骤环节的耗时分析(各步骤的耗时统计见下图),发现高耗时主要集中在以下3个环节:
- 检索结果的资源数据组装(1w条数据约 3000+ms);
- JS与Native的数据传输(1w条数据约 1600+ms);
- JS排序时的文本匹配度计算(当query长度在6个中文字符时,1w条数据约 230ms);
资源数据组装耗时优化
资源数组组装耗时主要来自 SQLite查询串行执行、资源数据反序列化。优化方案上,根据实际业务逻辑,将SQL查询优化为多线程并发执行,并延迟数据反序列化时机到展示时执行。
优化后,复测 7k 条数据耗时由 2400+ms 下降到 810ms 左右(基于xiaomi8测试)。
JS数据传输耗时优化
客户端本地通过内置的JS脚本实现搜索结果排序,该JS脚本可动态发布更新。 JS在与Native代码函数进行数据通信时,以Android系统为例,需要将java线程切换到native线程再切换到js线程,并且一次完整的流程上存在4次的线程切换以及4次的内存拷贝的情况。
针对这个问题,采用 JNI 和 C 调用 JSC 引擎来提升通信效率,方案落地细节参见文章 Android本地搜索优化,iOS优化思路一样,这里就不做过多论述。
优化后,本地检索JS数据传输耗时大幅度降低,复测 1w 条数据耗时由 1400+ms 下降到 310ms 左右(基于iPhoneX测试)。
文本匹配度计算耗时优化
SQlite FTS提供了bm25()函数来做文本匹配度计算,在返回检索结果的同时返回文本匹配值,可以替代JS脚本的文本匹配计算,继续减少耗时。
BM25 算法
BM25是信息索引领域用来计算query与文档相似度得分的经典算法。算法原理简单概括描述起来,就是先对搜索词query进行切分得到一组单词,然后求和每个单词的相关性得分,就得到了query和文档之间的分数,单词的相关性得分由三部分组成:
- 单词和文档的相关性
- 单词和query的相关性
- 单词权重
BM25算法公式如下图,详细介绍可参考文档 bm25算法介绍。
调用SQL
// 在进行全文搜索匹配时调用,bm25()函数可以将查询结果按照字符匹配度进行排序
SELECT *, bm25(songindexfts5) as BM FROM songindexfts5
WHERE songindexfts5 MATCH 'keyword'
ORDER BY bm25(songindexfts5)
优化结果
通过上述优化方案,总检索耗时下降了 75%,检索耗时的优化也有效促进了搜索业务指标的提升,通过将本地搜索结果展示数量由 1000条 提升至 4000条,结果有效点击率提升了 13%, 人均播放时长也提升了 17s。
- 优化前:1s 内可完成的检索数据量约在 1000条,8000条数据检索耗时在 5800 ms;
- 优化后:1s 内可完成的检索数据量约在 4000条,1w条数据检索耗时在 2600 ms;
优化后的各阶段耗时统计参见下图:
从图中可以看出,资源数据组装成为优化后的最大耗时占用,后续我们将持续进行拆解分析,优化数据查询耗时和JS数据传输耗时。
总结展望
本文详细介绍了云音乐本地轻量级搜索引擎的技术实现方案和耗时优化方案,通过在云音乐私域数据搜索中的落地运用,对其技术能力和业务能力进行了有效验证。未来将通过自研分词器进一步优化分词效果提升检索准确性,优化SQLite数据查询耗时和JS数据传输效率进一步缩短检索耗时,并推进落地更多的业务场景,为用户提供更好更准确的检索服务。
受限于自身能力,文中如有不足之处还请大家斧正,欢迎一起学习交流。
参考文档
- https://juejin.cn/post/7033227795996246052
- https://www.sqlite.org/fts5.html
- https://huili.github.io/fts1/fts.html
- https://github.com/wangfenjin/simple
- https://segmentfault.com/a/1190000041474107
- https://developer.android.com/reference/android/database/sqli...
- https://cloud.tencent.com/developer/article/1006159
- https://segmentfault.com/a/1190000043777969
- https://blog.csdn.net/laobai1015/article/details/120143102
最后
更多岗位,可进入网易招聘官网查看 https://hr.163.com/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。