其他
微信全文搜索耗时降94%?我们用了这种方案
目录
1 IOS 微信全文搜索技术的现状2 全文搜索引擎的选型与优化 2.1 搜索引擎选型 2.2 实现 FTS5 的 Segment 自动 Merge 机制 2.3 分词器优化 2.4 索引内容支持多级分隔符3 全文搜索应用逻辑优化 3.1 数据库表格式优化 3.2 索引更新逻辑优化 3.3 搜索逻辑优化4 总结01
02
全文搜索引擎的选型与优化
2.1 搜索引擎选型
在事务能力方面:Lucene 没有提供完整的事务能力,因为 Lucene 使用了多文件的存储结构,没有保证事务的原子性。SQLite 的 FTS 组件因为底层使用普通的表来实现,可以完美继承 SQLite 的事务能力。
在技术风险方面:Lucene 主要应用于服务端,在客户端没有大规模应用的案例。自 2013 年起 CLucene 和 Lucy 官方都停止维护了,技术风险较高。SQLite 的 FTS3 和 FTS4 组件则是属于 SQLite 的旧版本引擎,官方维护不多,而且这两个版本都是将一个词的索引存到一条记录中,极端情况下有超出 SQLite 单条记录最大长度限制的风险。SQLite 的 FTS5 组件作为最新版本引擎已推出超六年,在安卓微信上也已经全量应用,所以其技术风险是最低的。
在搜索能力方面:Lucene 的发展历史比 SQLite 的 FTS 组件长很多,搜索能力相比而言最丰富。Lucene 有丰富的搜索结果评分排序机制,但这个在微信客户端没有应用场景。因为微信搜索结果要么是按照时间排序,要么是按照一些简单的自定义规则排序。在 SQLite 几个版本的引擎中,FTS5 的搜索语法更加完备严谨。它提供了很多接口给用户自定义搜索函数,所以搜索能力相对强一点。
在读写性能方面,下面是用不同引擎对 100 万条长度为 10 的随机生成中文语句生成 Optimize 状态的索引的性能数据,其中每个语句的汉字出现频率按照实际的汉字使用频率:
2.2 实现 FTS5 的 Segment 自动 Merge 机制
某一个 level 的 segment 达到4时,就开始在写入内容时自动执行一部分 merge 操作,称为一次 automerge 。每次 automerge 的写入量跟本次更新的写入量成正比,需要多次 automerge 才能完整合并成一个新 segment 。automerge 在完整生成一个新的 segment 前,需要多次裁剪旧的 segment 的已合并内容,引入多余的写入量。 本次写入后某一个 level 的 segment 数量达到 16 时,一次性合并这个 level 的 segment ,称为 crisismerge 。
首先,监听有 FTS5 索引的数据库每个事务变更到的 FTS5 索引表,抛通知到子线程触发 WCDB 的自动 merge 操作。 其次,merge 线程检查所有 FTS5 索引表中 segment 数,超过 1 的 level 执行一次 merge 。 最后,merge时每写入 16 页数据检查一次有没有其他线程的写入操作因为 merge 操作阻塞,如果有就立即 commit ,尽量减小 merge 对业务性能的影响。
自动 merge 逻辑执行的流程如下:
2.3 分词器优化
2.3.1 分词器性能优化
2.3.2 分词器能力扩展
第一,支持在分词时将繁体字转换成简体字。这样用户可以用繁体字搜到简体字内容,用简体字也能搜到繁体字内容,避免了因为汉字的简体和繁体字形相近导致用户输错的问题。
第二,支持 Unicode 归一化。Unicode 支持相同字形的字符用不同的编码来表示。比如编码为\ ue9 的 é 和编码为\ u65 \ u301 的 é 有相同的字形,这会导致用户用看上去一样的内容去搜但却搜不到的问题。Unicode 归一化就是把字形相同的字符用同一个编码表示。
第三,支持过滤符号。大部分情况下,我们不需要支持对符号建索引,符号的重复量大而且用户一般也不会用符号去搜索内容。但是联系人搜索这个业务场景需要支持符号搜索,因为用户的昵称里面经常出现颜文字,符号的使用量不低。
第四,支持用Porter Stemming算法对英文单词取词干。取词干的好处是允许用户搜索内容的单复数和时态跟命中内容不一致,让用户更容易搜到内容。但是取词干也有弊端,比如用户要搜索的内容是 “happyday” ,输入 “happy” 作为前缀去搜索却会搜不到,因为 “happyday” 取词干变成 “happydai” , “happy” 取词干变成 “happi” ,后者就不能成为前者的前缀。这种 badcase 在内容为多个英文单词拼接一起时容易出现。联系人昵称的拼接英文很常见,所以在联系人的索引中没有取词干,在其他业务场景中都用上了。
第五,支持将字母全部转成小写。这样用户可以用小写搜到大写,反之亦然。
2.4 索引内容支持多级分隔符
03
全文搜索应用逻辑优化
3.1 数据库表格式优化
3.1.1 非文本搜索内容的保存方式
3.1.2 避免冗余索引内容
3.1.3 降低索引内容的大小
3.1.4 索引文件大小优化数据
3.2 索引更新逻辑优化
3.2.1 保证索引和数据的一致
一个是保证所有业务数据都有索引,用户的搜索结果就不会有缺漏; 二个是保证所有索引都对应一个有效的业务数据,这样用户就不会搜到无效的结果。
一个是冗余索引会导致搜索速度变慢。这个问题出现概率很小,这个影响可以忽略不计; 第二个问题是会导致用户搜到无效数据。这个是要避免的。
3.2.2 建索引速度优化
第一,减少磁盘的写入次数,提高平均建索引速度; 第二,在一个事务中,建索引 SQL 语句的解析结果可以反复使用,可以减少 SQL 语句的解析次数,进而提高平均建索引速度; 第三,减少生成 segment 的数量,从而减少 merge segment 带来的读写消耗。
3.2.3 删除索引速度优化
第一,建一个业务数据 id 到 FTS 索引的 rowid 的普通索引; 第二,在 FTS 索引表中去掉业务数据id 那一列的 UNINDEXED 约束,给业务数据 id 添加倒排索引。
第一,FTS 索引相比普通索引还带了很多额外信息,搜索效率低一些; 第二,如果需要多个业务字段才能确定一条 FTS 索引时,FTS 索引是建不了联合索引的,只能匹配其中一个业务字段,其他字段就是遍历匹配,这种情况搜索效率会很低。
3.2.4 索引更新性能优化数据
3.3 搜索逻辑优化
3.3.1 单个搜索任务支持并行执行
第一,对于搜索数据量大的业务(比如聊天记录搜索),可以将索引数据均分存储到多个 FTS 索引表(注意这里不均分的话还是会存在短板效应),这样搜索时可以并行搜索各个索引表,然后汇总各个表的搜索结果,再进行统一排序。这里拆分的索引表数量既不能太多也不能太少,太多会超出手机实际的并行处理能力,也会影响其他搜索任务的性能;太少又不能充分利用并行处理能力。以前微信用了十个 FTS 表存储聊天记录索引,现在改为使用四个 FTS 表。 第二,对于搜索逻辑复杂的业务(比如联系人搜索),可以将可独立执行的搜索逻辑并行执行。比如在联系人搜索任务中,我们将联系人的普通文本搜索、拼音搜索、标签和地区的搜索、多群成员的搜索并行执行,搜完之后再合并结果进行排序。这里为什么不也用拆表的方式呢?因为这种搜索结果数量少的场景,搜索的耗时主要是集中在搜索索引的环节。索引可以看做一颗 B 树,将一颗 B 树拆分成多个,搜索耗时并不会成比例下降。
3.3.2 搜索任务支持中断
第一,从数据库读取所有结果之后再排序。我们可以在读取结果时将用于排序的字段一并读出,然后在读完所有结果之后再对所有结果执行排序。因为排序的耗时占总搜索耗时的比例很低、排序算法的性能大同小异,这种做法对搜索速度的影响可以忽略。 第二,不能使用分段查询。在全文搜索这个场景中,分段查询其实没有什么作用。因为分段查询就要对结果排序,对结果排序就要遍历所有结果,所以分段查询并不能降低搜索耗时(除非按照 FTS 索引的 Rowid 分段查询,但是 Rowid 不包含实际的业务信息)。
3.3.3 搜索读取内容最少化
3.3.4 搜索性能优化数据
04
总结
目前 IOS 微信已经将这套新全文搜索技术方案全量应用到聊天记录、联系人和收藏的搜索业务中。使用新方案之后,全文搜索的索引文件占用空间更小,索引更新耗时更少、搜索速度也更快,可以说全文搜索的性能得到了全方位提升。
以上便是本次分享的全部内容,欢迎各位开发者在评论区交流讨论。
你可能感兴趣的腾讯工程师作品| 一文读懂 Redis 架构演化之路| 十问ChatGPT:一个新的时代正拉开序幕| 7天DAU超亿级,《羊了个羊》技术架构升级实战
🔹关注我并点亮星标🔹
工作日晚8点 看腾讯技术、学专家经验
点赞|分享|在看 传递好技术