概述
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在测试中很容易发现,说明其缺乏生产环境的大规模验证
网友评论