美文网首页程序员
Bleve代码详解

Bleve代码详解

作者: 贺大伟 | 来源:发表于2018-09-04 18:46 被阅读631次

    概述

    Bleve是一个由Couchbase团队基于Go语言开发的索引/检索库,它支持常用的检索和索引功能,如索引、检索、过滤、排序、聚合、高亮等。Bleve包括常见的文本分析组件,且能够使用现有的K/V存储系统进行存储。Bleve具有以下主要特性:

    1. 支持所有Go数据结构的索引,如JSON 、结构体、Slices、字符串等

    2. 具有强大、智能的配置功能

    3. 具有丰富的Field类型,如文本、数字、日期等

    4. 具有丰富查询类型,如Term、短语、模糊/精确匹配、前缀、逻辑与(Conjunction)、逻辑或(Disjunction)、布尔(Boolean)、数字范围、日期范围等查询

    5. 具有简单的查询语法,且能够实现复杂的查询

    6. 具有丰富的接口,且能够实现功能扩展

    7. 具有易用且高级API能够索引数据模型中的任何对象

    8. 基于标准的TF-IDF加权评分算法

    9. 支持查询匹配结果的高亮显示

    10. 支持多种聚合功能(Facet),如能够根据Term、数字范围、日期范围聚合等

    11. 文本解析组件现已支持众多分析组件,支持将近二十种语言,如丹麦语、荷兰语、英国、法语、德语、泰语、土耳其语等

    Bleve组件

    从bleve的目录结构可以看出bleve的核心模块:

    1. Analysis 分词模块

    2. Document 文档模块,定义bleve内部的文档结构

    3. Index 索引引擎,生成和持久化倒排索引bleve索引目前即支持KV存储也支持文件存储

    4. Mapping 解析文档模块,文档按照schema的定义解析成内部使用的document

    5. Registry 模块,bleve组件化注册中心

    6. Search 模块,负责search的执行

    一个文档的创建流程如下:

    Doc -> mapping -> document ->Index -> analysis -> store

    下面我们详细讲述每一个过程

    Mapping

    Mapping的入口:

    前面也说了,bleve的组件化做的很好,这是一个接口,实例如下:

    walkDocument会按照schema的定义(即docMapping)解析文档,结果保存在walkContext中。

    下一段中“_all”field的处理,熟悉Elaticsearch的同学都应该知道这个的含义,这里不做过多解释,需要说明的是,bleve的_all的处理,并不是按照ES的方式,把各个field的value组成一个string再分词处理,而是直接merge各个field的分词结果。

    Bleve中的schema结构如下:

    这里需要特别解释的是bleve中对这个结构的解释跟ES的mapping的格式略有不同,我们看一下ES中schema的定义,如下图:

    直观上看bleve的mapping的格式跟这个定义和温和,但是奇葩的是(bleve就是按照自己的逻辑解释的),bleve的mapping翻译过来是如下图的样子,这个很有意思。

    从上图可以你可以对比看出其中的区别,这个在使用bleve的时候需要特别注意。

    Bleve对doc的解析是基于反射处理的,被go的反射搞晕的同学可以认真阅读这部分代码,一定收益匪浅。

    Document

    文档经过mapping解析成bleve内部的document对象,如下图:

    Bleve对文档中的field都抽象成一个Filed接口。

    Bleve内部支持的数据类型包括:text,bool,number,geo,datetime。其中对于datetime,geo,number,bleve都会编码成int64的整数,然后再编码成[]byte。而对于bool类型,bleve转换成一个字节编码(‘T’,‘F’),这样所有的类型都编码成了[]byte。

    大家都知道ES的mapping是定义了filed的基本类型,但是field的value可以是单值也可以是数组,bleve也支持这个特性。举个例子:

    一个文档

    doc: {      "name":"doc",      

       "fields":[          

         {              

              "id":"2",              

              "vals":[                 

                      {"vval":"hello"}                

                ]           

          },          

          {              

               "id":"3",              

               "vals":[                 

                      {"vval":"word"},                  

                      {"vval":"bleve"}               

                  ]          

           }       

        ]    

    }

    Mapping之后的结构:

    &document.TextField{Name:name, Options:INDEXED, DV, Value: doc, ArrayPositions: []}

    &document.TextField{Name:fields.id,Options: INDEXED, DV, Value: 2, ArrayPositions: [0]}

    &document.TextField{Name:fields.id,Options: INDEXED, DV, Value: 3, ArrayPositions: [1]}

    &document.TextField{Name:fields.vals.vval,Options: INDEXED, DV, Value: hello, ArrayPositions: [0 0]}

    &document.TextField{Name:fields.vals.vval,Options: INDEXED, DV, Value: word, ArrayPositions: [1 0]}

    &document.TextField{Name:fields.vals.vval,Options: INDEXED, DV, Value: bleve, ArrayPositions: [1 1]}

    大家可以自行研究这里的arrayPositions的含义。

    Index

    Bleve目前支持两种索引引擎upsidedown和scorch(不知道为什么叫这两个名字),其中upsidedown底层是KV存储,scorch底层是文件存储。

    无论是那种索引引擎,对外都提供了统一的访问接口


    Upsidedown

    相比于scorch,upsidedown比较简单(主要是底层的kv存储引擎复杂度被屏蔽了)。

    我们这里主要看文档的写入,这是upsidedown的核心流程。

    我们看看batch接口在upsidedown是如何实现的。

    首先创建分词任务,放入分词队列异步处理分词,结果写入resultChan,后面会等待分词结束。

    这里我们看bleve的写文档有一把读写锁,这个读写锁防止并发对同一个文档的修改,bleve没有document version,这个实现严重制约了bleve单实例的写性能(作者提出使用bleves的模式解决写性能问题,详细可以看作者的benchmark说明)。

    分词的处理比较简单,代码大家可以自己查看,不难看懂,有三点要说明一下:

    1. Upsidedown维护一个fieldcache,目的是维护field name到field ID的映射,规则很简单,顺序递增,先到先得(不是按照field name的字典顺序排序)。补充一点bleve在分词的时候会频繁访问这个fieldcache,测试你会发现对这个cache的访问会严重影响性能,频繁的读锁获取和释放对性能影响还是很大的。

    2. Mapping定义的时候可以配置是否支持docvalue,但是在分词的时候(整个存储过程中)根本没有处理,直接忽略了。

    3. Upsidedown 会为每一个文档生成一个BackIndexRow,这个可以认为是document的摘要,里面记录了wend的fields,以及每一个field的terms信息,对一个文档的更新,删除等操作都依赖这个结构,只要get这个结构,就可以知道存储的文档的所有信息(通过倒排索引没办法在指定docID的情况获得term信息)。Bleve的聚合(很简单的几个聚合功能,没办法跟ES相比)在upsidedown中也是visitor这个结构实现的。

    Upsidedown的就是通过get BackIndexRow来确认一个文档是否存在。如果存在那么更新,否则新增。

    这里会等待分词结果,下面会根据是否存在value区分是update还是delete操作。

    这里会写入kv存储引擎。不同的数据在KV存储引擎中的编码格式如下:

    Version: 保存bleve的版本,不可修改,目前是7,主要用于schema校验

           key: {'v'}{0xff}       

           value: {version}{0xff}

    Schema:schema按照field拆解后存储

           key: {'f'}{field index}{0xff}.    field index是按照field name字典序的方式递增获得  

           value: {field name}{0xff}

    Term Dict : 存储倒排索引统计

           key:{‘d’}{field index}{term}{docID}……   

           value: {term count}

    Back index:     方便删除回滚  

           key: {'b'}{doc ID}{0xff}  

           value: {json term entries and store entries}  

    Store field:

           key: {'s'}{doc ID}{0xff}{array pos index...}  

          value: {field type}{field value}           反序列化的时候需要 

    Internel:         存储一些内部临时使用的数据,比如schema

          key:{'i'}{raw key}  

         value: {row value}

    Term Freq: 存储position,freq信息

           key:{'t'}{field index}{term}{0xff}{doc ID}  

           value: {freq}{norm}[{field index}{pos}{start}{end}{array pos len}{posindex ...}]

    补充一点,upsidedown为了将来search方便,每次写入document的时候,都会统计term的数量,并merge已经存储的数量,写入存储引擎。这也导致写阻塞。

    另外吐槽一点,upsidedown会在内存中维护总的文档数量,但是重启的时候它通过迭代的方式获得文档的数量,如果存储的数据量很大的话,这个过程会比较长,这一点大家需要注意。

    Scorch

    [后续补充]

    Analysis

    分词部分,bleve支持自定义分词器,注册之后就可以使用啦。Bleve提供的分词器不支持中文分词,网络上有人使用go封装了结巴中文分词,兼容bleve分词接口,可以直接拿来使用。

    Bleve对number,geo,datetime也做了分词处理,它们首先都被处理成int64,然后调用numeric中的PrefixCode可以按照前缀编码,这样做的目的主要是方便范围查找。大家可以移步到document包中查看相关的逻辑。

    Search

    Bleve支持绝大部分ES的query,基本上所有的query都会处理成term query。

    Bleve对query抽象了一个接口:

    Search的入口是SearchInContext()。

    1. 创建一个TopN的collector,collector会根据sort规则排序查询结果,并按照size,from保留文档集。

    2. 创建searcher

    3. 如果有聚合的话,创建聚合器

    4. 查询文档并获得查询结果hits

    5. 高亮处理

    6. 根据请求中指定的fields返回实际的文档

    至此我们基本清楚了bleve的执行流程和要点。

    目前bleve的代码存在三个问题:

    1. 代码注释不多,阅读起来比较费劲,尤其是scorch的代码,如果没有Lucene中segment相关的知识,基本上读不懂。

    2. Bleve的读写性能很差,upsidedown在search的时候与scorch之间存在很大的差距(这也是为什么作者又搞了一个scorch的原因吧)

    3. Bleve的代码中存在一些bug,这些bug在测试中很容易发现,说明其缺乏生产环境的大规模验证

    相关文章

      网友评论

        本文标题:Bleve代码详解

        本文链接:https://www.haomeiwen.com/subject/bbjnwftx.html