这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记。
搜索引擎关键组件:
1.1 倒排索引
倒排索引用于实现全文搜索模块,对文章进行分词后,针对每个关键词构建其倒排索引列表,通过noSQL数据库查询获得文章id列表,然后进行关联度计算,最后根据id即可获得对应的文章内容进行展示。数据格式为key:string,value:idList。
1.2 正排索引
正排索引包含对应文档的文章id以及关键字列表,其中关键字列表是文档的分词结果,用于去重和关键词输出。数据格式为key:id,value:keyWordsList。
1.3 文档仓库(用于检索内容反馈)
文档仓库用于存储对应id文章的内容以及图片地址,在倒排索引获得结果后进行输出以及图片解析,也可以存储其他相关内容。数据格式为key:id,value:IndexDoc。
1.4 相关搜索前缀树
相关搜索方面采用了前缀树实现,首先可以根据用户搜索历史(在该数据集中由于搜索历史过少采用文本内容代替)进行分词并去掉停顿词以此构建前缀树,采用前缀树能够有效地减少内存的消耗,并且快速得到相关内容。
搜索引擎实现详解:
2.1 文本分词及停顿词处理模块
由于汉字之间连接紧密无法直接进行相应的处理,本项目采用了jiebago对文章进行分词,其中对文章采用精准分词模式存储以及构建相关搜索前缀树,针对用户搜索语句采用搜索引擎模式增加分词数目提高召回率,过滤词文档采用精准分词模式,示例如下:
【精确模式】:我 / 来到 / 北京 / 清华大学 /
【搜索引擎模式】:小明 / 硕士 / 毕业 / 于 / 中国 / 科学 / 学院 / 科学院 / 中国科学院 / 计算 / 计算所 /
由于文章中有很多类似 “的”,“是”等无信息词语,因此加入了停顿词表去除文章停顿词提高搜索效率,根据关键词获取文档列表的时候应该尽可能的快,同时考虑到数据量规模问题,即如果数据量较大的话,一定还会存储到磁盘上,那就涉及到一个存取效率的问题,因此该项目选用noSQL数据库LevelDB作为索引和文档仓存储引擎。
对文本进行分词后,针对倒排索引:将key word和文档idList转换为byte[]类型存入LevelDB中;针对正排索引:将文档id及文档分词列表转换为byte[]类型存入LevelDB中;针对文档仓库:构建IndexDoc类型表示文档id、text内容及url内容转换为byte[]类型存入LevelDB中。
2.2 文字、图片搜索及排序模块
- 用户输入搜索字符串后,首先对其进行搜索引擎模式分词,对获得的
keyWordList进行搜索,获得每个keyWord的idList之后循环遍历每个list进行去重及打分,然后merge起来根据打分高低进行排序,最后根据打分情况利用id搜索文档仓获得文本内容及url进行展示,在具体实现上,整合倒排索引的过程中采用了管道机制。
- 打分模块根据用户输入的关键字对每个文章的匹配个数进行计算并且高亮展示:
- 最后考虑到检索压力,对三个关键组件进行了分片处理,采用取余运算获取分片键值,在单机上构建单行多列的数据库,后续如果加入多台服务器后,将其部署为多行多列的形式,每次搜索发出分片数个RPC,然后在每列中随机选取一个块,降低单台服务器的CPU使用率,提高搜索引擎可用性。
2.3 用户过滤词模块
考虑用户屏蔽词语,对搜索过程采取两部分过滤,一个是对搜索语句分词时过滤到屏蔽词,另一个是在进行倒排索引检索的时候,需要对过滤词词语进行一次文档id检索,搜索语句的查询出来的id需要过滤一次屏蔽词语的id。
例如:搜索“深圳北站” ,同时用户屏蔽 “深圳” 的时候,在分词阶段会频闭掉 “深圳” ,即倒排索引会检索“深圳北站”及 “北站” 的内容(用户输入采用搜索引擎模式分词),由于“深圳北站”本身也是一个检索词,所以还需要过滤掉“深圳”的倒排索引,因为还有“深圳北站”的词语一定含有“深圳”,索引实现了二次过滤,最终只会展示包含“北站”的文章。
2.4 相关搜索模块
相关搜索模块采用前缀树的思想保存,这样能够有效减少内存的占用情况,当用户输入搜索内容时,将用户搜索信息进行分词并将其分成多个string串,以bfs去搜索前缀树,获得10个结果后退出。
例如如下数据:文档仓包含“Ubuntu 安装GoLang、Ubuntu卸载G++、Ubuntu卸载C++ ”,构建如下前缀树,到达文章结尾标识尾结点,当用户输入“Ubuntu卸载”后,构建两个字符串列表分别是“Ubuntu”以及“Ubuntu卸载”,以bfs的方式搜索前缀树,当获取到至多10个尾结点即返回搜索结果。
2.5 数据库删除,全量索引替换及恢复模块
- 在数据库删除方面,采用了WAL的思想,建立一个deleteSet保存已经删除的文章id,每次删除后添加文章id到deleteSet结构中,在输出的时候进行过滤即可,并不需要每次都进行数据库删除,以此降低磁盘IO次数,当deleteSet累计到一定数量后,表示删除的文章数已经到达一个阈值,此时开启一个协程读取当前数据库快照,并且将deleteSet中的内容进行过滤后存入一个新的数据库中完成删除落盘,实现全量索引的替换。
counts := 0
c := cron.New()
spec := "0 */60 * * * ?" //设定60分钟检测一次
err := c.AddFunc(spec, func() {
if len(webSearch.SearchEngine.DeleteSet) > int(float32(webSearch.SearchEngine.GetDocumentCount())*0.1) {
//设定检测规则:如果删除的文章数量大于当前数据库数量的百分之10,重新更换引擎数据库,并将其保存为一个新版本
wg := &sync.WaitGroup{}
wg.Add(1)
counts++
newEngine := webSearch.SearchEngine.TransformToNewEngine(counts, wg)
wg.Wait()
webSearch.SearchEngine = newEngine
log.Println("重新初始化引擎!")
}
})
if err != nil {
log.Printf("AddFunc error : %v", err)
return
}
c.Start()
- 在数据库恢复方面,为了防止因宕机引起的数据库损失,每次重建数据库的时候根据当前版本号保存LevelDB内容,deleteSet内容由于是存在内存中的,所以每次修改的时候会sync顺序写入磁盘中,用来保证数据的完整性。数据库发生宕机后会选择当前保留的最大版本号加载数据库,并且读取磁盘上的deleteSet加载到内存中,能够有效保证数据库的完整性。