程序员

Bleve代码详解

2018-09-04  本文已影响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在测试中很容易发现,说明其缺乏生产环境的大规模验证

上一篇下一篇

猜你喜欢

热点阅读