我以为这个标题应该改改了,我写下来实际上是告诉你们怎么写一个搜索引擎,并无涉及太多的Golang的东西,我以为这样也挺好,熟悉了原理,用什么实现其实并不重要了,并且说说原理比说代码更实在。前端
以前已经说了底层的数据结构了,包括倒排和正排索引。今天咱们上一层,来讲说索引的字段和段。数据库
字段这个上一篇已经介绍过了,字段的概念其实是搜索引擎索引中咱们能看到的最底层的东西,也是对外暴露的最底层的概念,在字段之下是倒排和正排索引,这两项其实对用户是封装起来了,咱们能够认为每一个字段对应一个正排和一个倒排,而实际上也确实是这样的。数组
在字段之上就是咱们这一篇主要说的段了,段这个概念并非搜索引擎特有的,也不是必须的,是我这个项目新增出来的,固然,也不是我原创,不少搜索引擎的引擎系统都有这个概念。数据结构
所谓段,就是最基本的检索系统,一个段包含全部字段,包含一部分连续的文档集合,可以进行完整的检索,能够把它当成一个检索系统最基本单位。分布式
这么说可能仍是有点抽象,咱们打个比方,在数据库中,一行数据是最基本的单位,对应搜索引擎中的是一个文档,而表是全部文档的集合,对应搜索引擎中是一份索引,而段就是一部分表,它包含一部分文档的内容,能够对这一部分文档进行检索,多个段合并起来就是一份完整的索引。搜索引擎
若是一个搜索引擎的数据再建好索引之后并不变化,那么彻底没有必要使用段,直接在创建全量索引的时候把数据都建好就好了spa
若是有增量数据,而且增量数据是不断进入系统的话,那么段的概念就有必要了,新增的数据首先在内存中进行保存,而后周期性的生成一个段,持久化到磁盘中提供检索操做。日志
段还有一个好处就是当系统是一个分布式的系统的时候,进行索引同步的时候,由于各个段持久化之后就不会变化了,只须要把段拷贝到各个机器,就能够提供检索服务了,不须要在各个机器上重建索引。code
一个段损坏了,并不影响其余段的检索,只须要从其余机器上将这个段拷贝过来就能正常检索了,若是只有一个索引的话,一旦索引坏了,就没法提供检索服务了,须要等把正确索引拷贝过来才行。排序
一个段包含几个文件
indexname_{segementNumber}.meta 这里是段的元信息,包括段中字段的名称,类型,也包括段的文档的起始和终止编号。
indexname_{segementNumber}.bt 这里是段的倒排索引的字典文件
indexname_{segementNumber}.idx 这里是段的全部字段的倒排文件
indexname_{segementNumber}.pfl 这里是段的全部数字正排文件的数据,同时也包含字符串类型数据的位置信息
indexname_{segementNumber}.dtl 这里是段的字符串类型数据的详情数据
上面的indexname是这个索引的名称,至关于数据库中的表名,segmentNumber是段编号,这个编号是系统生成的。
多个段合在一块儿就是一个完整的索引,检索的时候其实是每一个段单独检索,而后把数据合并起来就是最后的结果集了。
下面咱们一个一个来讲说这些个文件,看看一堆正排和一堆倒排如何构成一个段的。
一个真正意义上的段的构建由如下几个步骤来构建,咱们以一个实际的例子来讲明一下段的构建,好比咱们如今索引结构是这样,这个索引包括三个字段,分别是姓名(字符串),年龄(数字),自我介绍(带分词的字符串),那么构建段和索引的时候步骤是这样的
首先新建一个段须要先初始化一个段,在初始化段的时候咱们实际上已经知道这个段包含哪些字段,每一个字段的类型。
初始化一个段信息,包含段所包含的字段信息和类型,在这里就是包含姓名(字符串【正排和倒排】),年龄(数字【正排】),自我介绍(带分词的字符串【正排和倒排】)。
给段一个编号,好比1000。
准备开始接收数据。
内存中的段是构建段的第一步,以上述的字段信息为例,咱们会在内存中创建如下几个数据结构,在这里我都是使用语言自动的原始数据结构
姓名须要创建倒排索引,因此创建一个map<string,list>,key是姓名,value是docid,姓名也要创建正排索引,因此创建一个StringArray[],保存每条数据的姓名的详情。
年龄须要创建正排索引,因此创建一个IntegerArray[],保存每条数据的年龄的详情。
自我介绍须要创建倒排索引,因此创建一个map<string,list>,key是自我介绍的分词的term,value是docid,自我介绍也要创建正排索引,因此创建一个StringArray[],保存每条数据的自我介绍的详情。
当新增一条数据的时候{"name":"张三","age":18,"introduce":"我喜欢跑步"}
,首先咱们给他一个docid【假如是0】,而后咱们把数据分别存放到上面的5个数据结构中,若是再来一条数据{"name":"李四","age":28,"introduce":"我喜欢唱歌"}
,咱们给他一个docid【假如是1】,那么数据就变成了下图的样子
这样,随着数据的不停导入,内存中的数据结构不断变化,内存段的数据也愈来愈大,当达到必定阈值的时候(这部分策略之后会说,我把这部分策略放到了引擎层,由引擎来决定何时进行段的持久化),咱们将把数据持久化到磁盘中。
进行持久化的过程当中
若是是map的数据结构,咱们将遍历整个map,首先将value追加写到.idx文件中,而后把key创建B+树,value是刚刚写入的idx文件的偏移位置。
若是是IntegerArray,咱们遍历整个数组,而后把数据写入到pfl文件中,每一个数据占用8个字节。
若是是StringArray,咱们遍历整个数据,首先把value追加写入到dtl文件中,而后把文件偏移量写入到pfl文件中
完成上面的三个步骤,咱们的持久化工做就完成了,完成之后数据结构就变成下面的样子了,你们能够本身脑子里实现一遍。
段构建完成后,这个段就算彻底持久化磁盘中了,不会再进行更改了,至关于提交到索引系统了,能够进行检索了。这时候,咱们再新建一个段,接着接收新的文档数据,而后继续把后续的段持久化到磁盘中。
当检索的时候,依次检索每一个段,而后将结果集合并起来返回给前端。
段创建好了之后,可能须要对段进行合并操做,段的合并方式也不少,最简单的就是新建一个段,而后遍历以前的全部数据,重新创建一个段便可,这比较适合于数据量少的状况,由于新建一个段是在内存中的,若是以前的数据太多的话,内存会撑不住。
还有一种方式是分别将倒排,正排依次合并,这种方式不耗费内存,可是比较耗费磁盘的IO,两种方式你们能够根据本身的业务场景进行选择,第一种的方法和以前段的构建是同样的,这里咱们说说第二种方式。
咱们使用的B+树对倒排索引的字典文件进行存储,B+树自然带排序,那么合并段的时候实际上就是合并多个B+树,咱们只要使用归并排序的方式就能合并多个B+树了。归并排序不清楚的能够本身去查查,每一个B+树的Key就是待归并的元素,一边扫描B+树一边构建一个新的B+树,而后把倒排文件合并起来造成一个新的idx文件,倒排文件就合并完了。
合并正排文件更加简单,只须要按照字段依次遍历每一个段的正排文件,而后一边遍历一边就造成了一个新的正排文件,遍历完正排文件也就合并完了。
合并的方法在FalconIndex/segment/segment.go
的 MergeSegments
中有详细代码,你们能够参考一下这种最简单的合并方式。
段的策略比较自由,通常也不建议固化到索引中。通常有如下几种策略可供选择,具体须要根据本身的业务逻辑来选择一个合适的段的持久化策略。
若是你的系统是一个一旦创建了索引就不怎么变化的系统,那么在作全量索引的时候创建一个段就好了,全量索引构建完了,而后把段持久化到磁盘就好了,若是全量索引量很大,怕内存扛不住,那么能够每10万条创建一个段,当全量索引完成了之后再将全部的段合并成一个段就好了,段的合并后面会说,合并段基本不占用什么内存,能够随时合并,若是有增量数据,每隔一段时间序列化一下段,而后再每隔一段时间将全部非全量数据的段合并一下,那么系统中就基本上只有一个全量的段和一个增量的段,检索起来仍是很是快的。
若是你的系统是一个实时变化比较大的系统,好比日志系统,那么全量索引实际就没什么意义了,因为日志系统的检索其实实时性要求没有那么高,那么段的策略能够是每新增10万条数据持久化一个段,没到10个段将全部段合并成一个段。或者按照时间戳来合并段,方便剔除老的数据。
若是你的系统是一个实时性要求很高的系统,那么能够按照时间(好比10秒)持久化一次段,每当系统空闲的时候将小的段合并成一个大的段。
总之,段的策略比较自由,彻底由引擎层来实现,根据本身的业务场景来选择重写一个段合并的策略都是能够的。
段是索引的一部分,也是一个微型的索引,下面一篇咱们将会介绍索引层了,索引层介绍玩之后搜索引擎的数据层就彻底结束了,上面就是各类引擎的策略了,有了索引层之后,其实对上你要变成一个搜索引擎仍是要变成一个数据库,或者变成一个KVDB的数据库都是能够的,反正基础的东西不会有太多变化。
好了,若是你想看以前的文章,能够关注个人公众号哈:)