认识Apache Lucene
为了更深入地理解Elasticsearch的工作原理,特别是索引和查询这两个过程,理解Lucene的工作原理至关重要。本质上,Elasticsearch是用Lucene来实现索引的查询功能的。
Lucene架构基本概念
- Document: 它是在索引和搜索过程中数据的主要表现形式,或者称“载体”,承载着我们索引和搜索的数据,它由一个或者多个域(Field)组成。
- Field: 它是Document的组成部分,由两部分组成,名称(name)和值(value)。
- Term: 它是搜索的基本单位,其表现形式为文本中的一个词。
- Token: 它是单个Term在所属Field中文本的呈现形式,包含了Term内容、Term类型、Term在文本中的起始及偏移位置。
Apache Lucene把所有的信息都写入到一个称为倒排索引的数据结构中。这种数据结构把索引中的每个Term与相应的Document映射起来,这与关系型数据库存储数据的方式有很大的不同。读者可以把倒排索引想象成这样的一种数据结构:数据以Term为导向,而不是以Document为导向。下面看看一个简单的倒排索引是什么样的,假定我们的Document只有title域(Field)被编入索引,Document如下:
- ElasticSearch Servier (document 1)
- Mastering ElasticSearch (document 2)
- Apache Solr 4 Cookbook (document 3)
所以索引(以一种直观的形式)展现如下:
Term | count | Docs |
---|---|---|
4 | 1 | <3> |
Apache | 1 | <3> |
Cookbook | 1 | <3> |
ElasticSearch | 2 | <1> <2> |
Mastering | 1 | <1> |
Server | 1 | <1> |
Solr | 1 | <3> |
正如所看到的那样,每个词都指向它所在的文档号(Document Number/Document ID)。这样的存储方式使得高效的信息检索成为可能,比如基于词的检索(term-based query)。此外,每个词映射着一个数值(Count),它代表着Term在文档集中出现的频繁程度。
当然,Lucene创建的真实索引远比上文复杂和先进。这是因为在Lucene中,词向量(由单独的一个Field形成的小型倒排索引,通过它能够获取这个特殊Field的所有Token信息)可以存储;所有Field的原始信息可以存储;删除Document的标记信息可以存储……。核心在于了解数据的组织方式,而非存储细节。
每个索引被分成了多个段(Segment),段具有一次写入,多次读取的特点。只要形成了,段就无法被修改。例如:被删除文档的信息被存储到一个单独的文件,但是其它的段文件并没有被修改。
需要注意的是,多个段是可以合并的,这个合并的过程称为segments merge。经过强制合并或者Lucene的合并策略触发的合并操作后,原来的多个段就会被Lucene创建的更大的一个段所代替了。很显然,段合并的过程是一个I/O密集型的任务。这个过程会清理一些信息,比如会删除.del文件。除了精减文件数量,段合并还能够提高搜索的效率,毕竟同样的信息,在一个段中读取会比在多个段中读取要快得多。但是,由于段合并是I/O密集型任务,建议不好强制合并,小心地配置好合并策略就可以了。
分析你的文本
问题到这里就变得稍微复杂了一些。传入到Document中的数据是如何转变成倒排索引的?查询语句是如何转换成一个个Term使高效率文本搜索变得可行?这种转换数据的过程就称为文本分析(analysis)
文本分析工作由analyzer组件负责。analyzer由一个分词器(tokenizer)和0个或者多个过滤器(filter)组成,也可能会有0个或者多个字符映射器(character mappers)组成。
Lucene中的tokenizer用来把文本拆分成一个个的Token。Token包含了比较多的信息,比如Term在文本的中的位置及Term原始文本,以及Term的长度。文本经过tokenizer处理后的结果称为token stream。token stream其实就是一个个Token的顺序排列。token stream将等待着filter来处理。
除了tokenizer外,Lucene的另一个重要组成部分就是filter链,filter链将用来处理Token Stream中的每一个token。这些处理方式包括删除Token,改变Token,甚至添加新的Token。Lucene中内置了许多filter,读者也可以轻松地自己实现一个filter。有如下内置的filter:
- Lowercase filter:把所有token中的字符都变成小写
- ASCII folding filter:去除tonken中非ASCII码的部分
- Synonyms filter:根据同义词替换规则替换相应的token
- Multiple language-stemming filters:把Token(实际上是Token的文本内容)转化成词根或者词干的形式
所以通过Filter可以让analyzer有几乎无限的处理能力:因为新的需求添加新的Filter就可以了。
索引和查询
在我们用Lucene实现搜索功能时,也许会有读者不明觉历:上述的原理是如何对索引过程和搜索过程产生影响?
索引过程:Lucene用用户指定好的analyzer解析用户添加的Document。当然Document中不同的Field可以指定不同的analyzer。如果用户的Document中有title和description两个Field,那么这两个Field可以指定不同的analyzer。
搜索过程:用户的输入查询语句将被选定的查询解析器(query parser)所解析,生成多个Query对象。当然用户也可以选择不解析查询语句,使查询语句保留原始的状态。在ElasticSearch中,有的Query对象会被解析(analyzed),有的不会,比如:前缀查询(prefix query)就不会被解析,精确匹配查询(match query)就会被解析。对用户来说,理解这一点至关重要。
对于索引过程和搜索过程的数据解析这一环节,我们需要把握的重点在于:倒排索引中词应该和查询语句中的词正确匹配。如果无法匹配,那么Lucene也不会返回我们喜闻乐见的结果。举个例子:如果在索引阶段对文本进行了转小写(lowercasing)和转变成词根形式(stemming)处理,那么查询语句也必须进行相同的处理,不然就搜索结果就会是竹篮打水——一场空。