用Golang写一个搜索引擎(0x07)

文章中的奇怪词语和用空格隔开的是oschina识别的敏感词,完全醉了。。。。

最近各类技术盛会太多,朋友圈各类刷屏,有厂商发的各类广告,有讲师发的各类自拍,各类参会的朋友们各类自拍,好不热闹,不知道你的朋友圈是否是也是这样啊,last year还没这么多技术会议,this year 感受爆发了,呵呵,真是一个互联网技术的好时代,并且还有各类Sibi可看,咱们这种码农仍是专 注 技 术 专 注 写 代 码吧。git

你有什么想了解的也能够给我留言哈,欢迎交流,个人工做以前主要作的是搜索的,也作推荐和广告,这部分的东西可能写得多点,对了,嵌入式领域也行(跨得有点大,这个嵌入式不是iOS和Android,是真的嵌入式),没什么高端背景,也不是BAT这种大厂的,就是一小公司写代码的,因此有不少东西仍是不懂,你要是和我交流了发现我答不上来很正常啊,人艰不拆啊。。github

本篇也比较长,可是干货很少,建议上厕所的时候看,或者在地铁一边听歌一边看。算法

前面几篇,基本上把倒排索引的数据结构给讲完了,而且简单的说了一下排序,而后说了一下倒排索引的构建。这一篇主要写一下正排索引以及倒排和正排怎么配合起来造成一个完整的字段索引。数据库

正排索引

正排索引,也叫前向索引,和倒排索引(也叫反向索引)是相对的,正排索引相对倒排来讲简单多了,第二篇文章的时候有下面两个表格(表1和表2)编程

这个是表1数组

文档编号 文档内容
1 这是一个Go语言实现的搜索引擎
2 PHP是世界上最好的语言
3 Linux是C语言和汇编语言实现的
4 XX是一个世界上最好的搜索引擎公司

这个是表2 | 关键词 | 文档编号 | | ---- | ----- | | Go | 1 | | 语言 | 1,2,3 | | 实现 | 1,3 | | 搜索引擎 | 1,4 | | PHP | 2 | | 世界 | 2,4 | | 最好 | 2,4 | | 汇编 | 3 | | 公司 | 4 |数据结构

咱们以前一直在说做为倒排索引的表2,对于表1,咱们认为是数据的详情(detail)信息,最后用来作数据内容展现的,若是是放在一个只支持全文搜索的搜索引擎中的话,那确实表1只是用来作最后的数据展现,可是若是咱们的搜索引擎还想要一些复杂的功能,那么表1就是一个正排索引,若是咱们的搜索引擎同时支持倒排索引和正排索引,咱们能够简单的认为这是一个数据库系统(固然,和真正的数据库还差得远啊)。分布式

首先,咱们看什么状况下要使用正排索引

很明显,若是倒排索引知足不了搜索要求的时候,就须要引入正排索引,好比一个电商的搜索引擎,那么正排索引就是必须的了,假如咱们有如下几个商品须要上架:优化

商品编号 商品 发布时间 价格 品牌
10001 cui子手机T9 2026-06-06 5000 cui子
10002 小米手机10 2020-02-02 1999 小米
10003 华为手机P20 2022-12-12 3999 华为

搜索的时候咱们可能须要搜索价格在一个区间的手机,那么仅仅用全文倒排索引就比较难完成任务了,并且咱们在使用电商的搜索引擎的时候,常常会在搜索结果的上方看到一些汇总的信息【好比品牌,型号,价格汇总】,这一部分的东西也是经过正排索引来实现的,像下面这个图ui

因此说,若是咱们的搜索需求不只仅是进行关键词的匹配,还须要进行一些过滤操做(好比价格区间的过滤),汇总操做(好比结果集中每种品牌数量的统计),那么就必须引入正排索引了。

第二,咱们看看如何实现一个正排索引

实现正排索引有两种方式:

一种仍是基于倒排索引,以前的倒排索引不是经过B+树构建的么,B+树自然的带排序功能,因此是能够进行范围查找的,好比上面那个表格,咱们要搜索的关键词为手机,价格区间在1500–4000之间

  • 咱们把价格字段和商品标题字段分别创建一个倒排。
  • 首先,经过标题的倒排索引,检索出全部的带手机这个关键词的商品的结果集,他们是【1,2,3】
  • 而后进行价格区间的检索,由于B+树最下面的叶子节点是经过指针连在一块儿的,咱们只须要经过指针遍历叶子节点,就能够遍历出价格区间中全部价格的倒排链,而后把这些链求并集,获得的结果集是【2,3】,就是知足这个价格区间的因此商品了。
  • 最后再和关键词查出来的商品求交集,就是最后的结果了。

这是第一种实现方式,汇总操做你们能够本身想一想怎么作,也能作,就是麻烦点。这种实现方式有下面几个特色

  • 没有单独的正排文件,和倒排文件合在一块儿了,同时也不占用额外的空间。
  • 可是它限制了倒排索引的实现方式只能是B+树这种带排序的字典,若是倒排文件使用哈希表来实现的话,就不能这么干了。
  • 检索的时候若是是区间搜索的话,须要进行屡次求并集操做,效率上须要进行优化。
  • 因为只有倒排文件,那么最后用来作数据展现的时候还须要一个辅助的Detail文件或者和数据库绑定在一块儿才能进行最终的结果展现。

除了上面那个,还有一种实现方式,就是经过一个数组来实现,数组的下表就是文档编号(docid,不是商品编号,商品编号是主键),因为在搜索引擎中,docid是自增的,并且不会进行删除,因此也是惟一的,正好能够和一个一维数组的下标对上,因此能够用一个数组来存储正排索引,就像下面这个表格,分别表示价格和品牌创建的正排索引,其实就是把表1的数据拆开来进行存储了而已。(为了节省空间,我把两个写在一块儿了)

DOCID 价格 DOCID 品牌
0 5000 0 cui子
1 1999 1 小米
2 3999 2 华为

这么存的话,检索的时候怎么作呢?若是仍是上面那个检索条件要搜索的关键词为手机,价格区间在1500–4000之间

  • 只把标题创建倒排,价格字段创建一个一维数组的正排
  • 首先,经过标题的倒排索引,检索出全部的带手机这个关键词的商品的结果集,他们的DOCID是【1,2,3】
  • 遍历结果集,每遍历一个docid,直接经过那个一维数组和对应的正排文件进行比对,看是否知足条件,知足的留下,不知足的丢弃。
  • 遍历完成之后,获得最终的结果集【2,3】

若是是汇总操做的话,和上述相似,在第二步遍历结果集的时候顺便就能够进行统计了,遍历完了也就统计完了。

条条大路通罗马,经过两种不一样的数据结构,最后获得了同样的结果,第二方式有如下几个特色

  • 要为须要进行范围查找的字段单独创建正排索引,不能和倒排的数据结构合并。
  • 经过倒排获取到结果集之后须要对结果集进行一次遍历,而后获得一个新的结果集做为最后的结果,若是结果集特别巨大,那么也须要时间进行遍历。
  • 由于是一维数组来实现的正排,若是文档数很是多的话,内存中是装不下这么多正排文件的,须要在磁盘上来实现这个一维数组。
  • 若是咱们将每个字段都创建一个正排索引的话,那就不须要单独的detail文件或者和数据库对接了,直接正排文件合起来就是一个完整的文档信息,少了外部依赖。

上面就是正排索引的两种实现方式,使用哪种要看具体的业务需求,好比像百度这种全文搜索引擎,主要的需求其实就是查找关键字,不多用到过滤,汇总操做,那么不用单独来实现正排索引,用第一种方式就好了,而若是是电商类型的搜索引擎的话,有大量的过滤啊,汇总操做,那么经过第二种方式来实现正排索引仍是比较必要的。

个人代码里面就是用的第二种方式,而且实现的时候是用mmap的方式在磁盘上实现的,若是内存够大,能够全载入到内存提升检索速度。

索引设计管理

正排索引和倒排索引终于都说完了,这要是搜索引擎最关键的数据结构了,其余全部的东西都是在这个基础上发展起来的,咱们已经有了正排和倒排索引的结构,那么若是来构建一个索引系统的,我是这么来作的。

首先,咱们须要定一个规矩,所谓规矩就是咱们的这个搜索引擎哪些操做我支持,哪些操做我不支持,好比,我为了简单,我就支持全文检索,其余都不支持,那么只须要好好的实现一个倒排索引结构,那数据结构部分就设计的差很少了。而我在作这个搜索引擎的时候,想实现的是下面这些个功能。

  • 支持关键词的倒排,也支持彻底匹配类型的倒排。
  • 支持过滤操做,可是只支持整数类型(若是是浮点数根据保留的小数位数转成整数)和日期类型的过滤,对于字符串只提供检索操做,不提供过滤操做。
  • 对于过滤操做,支持大于,小于,等于,不等于,区间的过滤。
  • 支持字段的汇总。
  • 不要外接数据库系统进行数据详情的展现。

既然是这么来实现,那对于每一个字段,他可能的类型就是

字段类型 行为 备注举例
完整匹配的字符串 创建倒排,正排(正排只展现,不进行过滤操做) 主键,型号
关键词字符串 创建倒排,正排(正排只展现,不进行过滤操做) 标题,描述
数字 只创建正排 价格,库存
日期 只创建正排 上架
仅展现 只创建正排(正排只展现,不进行过滤操做) 商品详情描述

这样,咱们实现的时候,首先实现一个倒排索引(src/FalconIndex/segment/invert.go),而后实现一个正排索引(src/FalconIndex/segment/profile.go),而后实现一个字段类(src/FalconIndex/segment/field.go)用来管理倒排和正排,那么搜索引擎最最基本的数据结构就OK了,对外来讲倒排和正排是隐藏的,只有Field类对外暴露,对检索操做来讲主要提供几个接口方法:

  • addDocument 添加文档(创建正排或者倒排)
  • query 经过倒排检索文档
  • filter 经过正排过滤文档
  • getValue 经过正排文件获取这个字段的值

文章中我尽可能少列或者不列代码,主要是对搜索引擎的原理有了解,原理了解了能够本身来实现代码,实在不会能够本身去看个人代码,毕竟编程这东西只要知道了原理和算法,怎么实现并非麻烦事。

写在后面的话

以前我一直作C++开发的,写的搜索代码也是C++的,如今用Golang,也没啥特别的难度,固然由于我对Golang的特性并非很熟悉,因此基本没有用Golang的高级功能,写出来的代码固然不够Golang范,但这也不影响个人实现。

OK,字段部分介绍完了,搜索引擎的核心数据结构也介绍完了,后面接下来会继续往上走,先到段层,而后到索引层,而后会说一下检索逻辑实现,合并逻辑之类的,索引之上会继续说一下搜索引擎的引擎部分,后面还会遇到一些数据结构,好比bitmap,哦,还会单独写一到两篇来介绍分词,至于排序和索引结构优化也会单独拿出来讲。

另外,个人代码基本完成了,包括分布式的部分,会在最近提交到github上去,因此后面也会有几篇来讲搜索引擎的分布式实现,仍是本着原生的原则,没用第三方库,因此分布式部分没有PAXOS这种高端的理论,也没有ZooKeeper这种高端玩意,到时候你们看吧。

目前个人代码初步测 试,8G,24核的机器中,1000万条数据*(微博数据,每条不超过140个字,我不是微博的人哈,不存在数据泄密,数据是某号称亚二爬的博士爬来的,我只是下下来用而已)*,单个term的平均检索时间在5ms,用AB进行单个URL测 试,QPS大概在7000,若是是随机关键词测 试,QPS大约在2000,基本达到我以前本身定的目标了,并且还有优化空间。下次测测ElasticSearch,目前感受比它报出来的数据要快,可是环境不同,下次部一个比较一下,并且功能上还彻底达不到ElasticSearch的水平,不过它那一套要实现出来也是没什么问题的,须要的是坚持,我会把这个项目维护下去,不过最近实在是太忙了,苦逼啊。。。

最后,继续发个二维码,你懂的,关注一下呗:)

输入图片说明

相关文章
相关标签/搜索