这是ElasticSearch 2.4 版本系列的第九篇:
- ElasticSearch入门 第一篇:Windows下安装ElasticSearch
- ElasticSearch入门 第二篇:集群配置
- ElasticSearch入门 第三篇:索引
- ElasticSearch入门 第四篇:使用C#添加和更新文档
- ElasticSearch入门 第五篇:使用C#查询文档
- ElasticSearch入门 第六篇:复合数据类型——数组,对象和嵌套
- ElasticSearch入门 第七篇:分析器
- ElasticSearch入门 第八篇:存储
- ElasticSearch入门 第九篇:实现正则表达式查询的思路
ElasticSearch 2.4版本支持Java正则表达式查询,但是,在对大段的文本(Text Block)进行挖掘之前,必须了解正则表达式查询的特殊之处。由于分析器会对文本字段进行分词,移除停用词,小写转换等操作,最终存储在倒转索引中的是小写的标记流(Token Stream),默认情况下,每一个标记是一个分词(Term),这无法满足正则表达式查询的一般要求,这就是说,正则表达式查询的是原始文本,需要注意的是,ElasticSearch引擎都是从原始文本的第一个字符开始执行正则表达式匹配。
在ElasticSearch 2.4版本中启用正则表达式查询之前,需要考虑两个问题:分词吗?大小写敏感吗?
一,分词还是不分词?
通常情况下,ElasticSearch引擎对文本字段进行分词,移除停用词,转换成小写,这是全文搜索的标准配置,在这种设置下,正则表达式只能匹配文本字段的单个分词,而无法对原始文本执行正则表达式查询,为了实现正则表达式查询,必须设置文本字段不被分词,也就是是设置该字段的index属性为not_analyzed。在实际的产品环境中,对一个字段同时执行正则表达式查询和全文搜索的情况是经常存在的,ElasticSearch 2.4版本提供的映射参数 fields 能够满足该需求。
fields 参数:多元字段(multi-fields),用不同的处理方式,把一个相同的字段编入索引,以实现不同的目的。多元字段使用相同的数据派生新的字段,例如,一个字段field被编入索引作为分析字段(analyzed field)以执行全文搜索,把该字段设置为多元字段,那么ElasticSearch引擎派生一个新的字段field .raw,该字段文本作为一个词被编入索引,只对该字段执行排序或聚合操作。
注:多元字段(multi-fields)不同于多值字段(multi-values field),字段的多值是ElasticSearch内在支持的特性,“开箱即用”,不需要做任何配置。每个字段都能存储多个数据值,这就是意味着,每个字段都是数组类型,只不过在字段中存储的数据,其数据类型都是相同的。
1,多元字段使用示例
在示例索引映射中,eventdescription是一个多元字段,其index属性是analyzed,表示该字段是分析字段,ElasticSearch引擎把该字段的文本分析成分词流,编入索引,以执行全文搜索,这就意味着,倒排索引中存储的不是该字段的原始文本,而是分割的单个分词;而eventdescription.raw是多元字段的派生字段,其index属性是not_analyzed,表示该派生字段不会被分词,整个文本字段整体作为一个分词被编入索引,这就意味着,倒排索引中存储的是派生字段原始的文本值。
"eventdescription":{ "type":"string",
"index":"analyzed", "fields":{ "raw":{ "type":"string",
"index":"not_analyzed" } } }
2,对原始文本执行正则表达式查询
ElasticSearch引擎在处理分析字段(analyzed field)时,使用指定的分析器执行分析操作,包括分词,移除停用词,转换大小写等,如果一个字段不是分析字段,那么ElasticSearch引擎不会对其执行任何分析工作。
映射参数index:决定ElasticSearch引擎是否对文本字段执行分析操作,也就是说分析操作将分割文本,把分词编入索引,并使分词能够被搜索到:
- 当参数值为analyzed时,该字段是分析字段,ElasticSearch引擎对该字段执行分析操作,把文本分割成分词流,存储在倒排索引中,使其支持全文搜索;
- 当参数值为not_analyzed时,该字段不会被分析,ElasticSearch引擎把原始文本作为单个分词存储在倒排索引中,不支持全文搜索,但是支持词条级别的搜索;也就是说,字段的原始文本不经过分析而存储在倒排索引中,使用原始文本编入索引,在搜索的过程中,查询条件必须全部匹配整个原始文本;
- 当参数值为no时,该字段不会被存储到倒排索引中,也不会被搜索到;
这也就意味着,要对原始文本执行正则表达式查询,必须设置index属性为not_analyzed,这也就意味着,保留文本的原始形式,例如大小写,空格等。
3,单个分词的最大长度
如果设置字段的index属性为not_analyzed,原始文本将作为单个分词,其最大长度跟UTF8 编码有关,默认的最大长度是32766Bytes,如果字段的文本超过该限制,那么ElasticSearch将跳过(Skip)该文档,并在Response中抛出异常消息:
operation[607]: index returned 400 _index: ebrite _type: events _id: 76860 _version: 0 error: Type: illegal_argument_exception Reason: "Document contains at least one immense term in field="event_raw" (whose UTF8 encoding is longer than the max length 32766), all of which were skipped. Please correct the analyzer to not produce such terms. The prefix of the first immense term is: '[112, 114,... 115]...', original message: bytes can be at most 32766 in length; got 35100" CausedBy:Type: max_bytes_length_exceeded_exception Reason: "bytes can be at most 32766 in length; got 35100"
可以在字段中设置ignore_above属性,该属性值指的是字符数量,而不是字节数量;由于一个UTF8字符最多占用3个字节,因此,可以设置
“ignore_above”:10000
这样,超过30000字节之后的字符将会被分析器忽略,单个分词(Term)的最大长度是30000Bytes。
The value for
ignore_above
is the character count, but Lucene counts bytes. If you use UTF-8 text with many non-ASCII characters, you may want to set the limit to32766 / 3 = 10922
since UTF-8 characters may occupy at most 3 bytes.
二,大小写敏感?
正则表达式查询一般是区分大小写的,有时,我们可能会希望,正则表达式查询忽略大小写,在这种情况下,多元字段(fields)就无法满足需求了,多元字段不能执行文本大小写转换。为了解决这个问题,我们可以新建一个字段,在更新索引时,把原始文本导入到分析字段,把相同的数据转换成小写形式导入到另一个字段中,这样做以后,分析字段及其派生字段,用于支持全文搜索和大小写敏感的正则表达式查询,另外一个字段用于忽略大小写的正则表达式搜索。
"eventdescription":{ "type":"string",
"index":"analyzed", "fields":{ "raw":{ "type":"string",
"index":"not_analyzed" } } }, "eventdescription_lowcase":{ "type":"string",
"index":"not_analyzed" } }
三,存储控制
为了实现正则表达式查询,上例为一个数据创建三个字段,eventdescription、eventdescription.raw和 eventdescription_lowcase,这三个字段都需要存储在倒排索引中,ElasticSearch引擎是否使用3倍的容量来存储这个数据?
默认情况下,一旦字段值被编入索引,该字段能够被搜索,但是,字段的原始值没有存储到倒排索引中,这就意味着,该字段能够被搜索,却不能从倒排索引中获取该字段的原始值。通常情况下,这样设计能够节省硬盘存储空间,不会对应用程序有什么影响,实际上,ElasticSearch引擎把该字段的原始值存储在_source元字段中,默认情况下,_source元字段是存储的。
1,存储属性(store)
字段的原始值是否被存储到倒排索引,是由映射参数store决定的,默认值是false,也就是,原始值不存储到倒排索引中。在特定的情况下,存储字段的原始值是有意义的。例如,为了存储一篇博客,文档必须有title,date和一个非常大的正文字段(content),如果仅仅是获取title和date,而不用获取正文字段,那么你可以存储title和date,并把content字段的store属性设置为false。
"title":{ "type":"string", "store":true, "index":"analyzed" }, "date":{ "type":"date", "store":true, "index":"not_analyzed" }, "content":{ "type":"string", "store":false, "index":"analyzed" }
映射参数index和store的区别在于:
- store用于获取(Retrieve)字段的原始值,不支持查询,可以使用投影参数fields,对stroe属性为true的字段进行过滤,只获取(Retrieve)特定的字段,减少网络负载;
- index用于查询(Search)字段,当index为analyzed时,对字段的分词执行全文查询;当index为not_analyzed时,字段的原始值作为一个分词,只能对字段的原始文本执行词条查询;
2,源字段(_source)
当把原始的JSON文档传递到ElasticSearch引擎时,ElasticSearch引擎使用_source字段存储最原始的JSON文档。_source字段本身不会被索引,也不会被搜索,但是,该字段会存储在倒排索引中,用于返回查询的结果。
_source字段会导致索引存储空间的增加,因此,可以禁用_source字段,但是,在禁用_source字段之前,请认真阅读官方文档《_source field》。
"mappings": { "tweet": { "_source": { "enabled": false } } }
一般情况下,不要禁用_source字段,当需要考虑占用的Disk空间时,请有限考虑压缩存储,提高压缩等级。在配置文档 中,压缩选项是 index.codec,默认值是LZ4压缩,设置best_compression 能够提供更高的压缩率,代价是降低数据存储的性能。
四,一个字段包含所有文本?
为了实现正则表达式查询,上例为一个数据创建三个字段,eventdescription、eventdescription.raw和 eventdescription_lowcase,这三个字段都需要存储在倒排索引中,通常做法是,同时对这三个字段执行正则表达式查询,但是在ElasticSearch中,在编码上,可以更简单。元字段 _all是一个特殊的“包罗万象”(catch-all)的字段,把其他字段的值拼接成一个大的字符串,字段值之间使用空格分隔。ElasticSearch引擎先分析_all字段,然后编入索引,但是,默认情况下,不会存储字段的原始值,这就意味着,_all字段能够被搜索,但是不会返回原始值。_all 字段把所有字段的原始值,都视为字符类型,并把字段的原始值通过分隔符空格拼接在一起。
注意,添加到_all字段的是原始值,而不是字段分析之后的词条(term)。字段的原始值是否包含到_all字段,是由该字段的属性 include_in_all控制的,默认值是true。启用_all字段是需要付出代价的,_all字段会消耗额外的CPU时钟周期和更多的硬盘空间,如果不是必需,推荐把_all字段禁用掉。
"content": { "type": "string", "include_in_all": false },
当把_all字段禁用之后,用户可以创建自定义的"_all"字段:新建一个数据类型为string的字段,并在需要拼接的字段中设置属性“copy_to”。
在ElasticSearch中,每个索引只有一个_all字段,通过字段的属性copy_to能够创建自定义的"_all"字段。例如,字段 first_name和 last_name能够通过分隔符空格被拼接到一起,作为full_name字段的值。
{ "mappings": { "mytype": { "properties": { "first_name": { "type": "string", "copy_to": "full_name" }, "last_name": { "type": "string", "copy_to": "full_name" }, "full_name": { "type": "string" } } } } } PUT myindex/mytype/1 { "first_name": "John", "last_name": "Smith" } GET myindex/_search { "query": { "match": { "full_name": "John Smith" } } }
默认情况下,_all字段不会存储_source字段的值,也不会存储原始值,这是因为_all字段是其他字段结合在一起组成的,存储_all字段会占用大量的硬盘存储空间,如果设置_all字段的属性store为true,那么ElasticSearch引擎将会存储_all字段的原始值,其原始值也能够被获取到。
五,示例
综上所述,为了实现正则表达式查询,为了实现正则表达式的查询,有两个设计思路
示例1,原始文本和小写文本各使用一个字段
通过bool查询的should子句,对多个字段执行正则表达式查询,当字段较多,或者字段的文本特别大时,使用该方式,节省硬盘空间,但是要编写更多的查询代码:
"eventdescription":{ "type":"string", "index":"analyzed", "fields":{ "raw":{ "type":"string", "index":"not_analyzed", "ignore_above":10000 } } }, "eventdescription_lowcase":{ "type":"string", "index":"not_analyzed", "ignore_above":10000 } }
示例2,添加冗余字段
在冗余字段上执行正则表达式查询,当在多个字段上执行相同的正则表达式时,使用该方式,便于编程,但是要注意,拼接字段的大小不能超过限制(32766Bytes):
"eventdescription":{ "type":"string", "index":"analyzed", "copy_to":"eventdescription_regexp" }, "eventdescription_lowcase":{ "type":"string", "index":"not_analyzed", "copy_to":"eventdescription_regexp" }, "eventdescription_regexp":{ "type":"string", "index":"not_analyzed" }
参考文档:
Elasticsearch Reference [2.4] » Mapping » Meta-Fields
Elasticsearch Reference [2.4] » Mapping » Mapping parameters
Elasticsearch Reference [2.4] » Query DSL » Term level queries » Regexp Query