作为经典的文档检索数据库,ElasticSearch提供了丰富的接口来搜索数据,满足用户不同的搜索需求。我们的系统为了支持实时字段明细查询功能,会将数据实时写入ES。整个系统数据量较多且维度丰富,每天写入的文档数据达到10多亿条,单个最大索引存储达到2T以上,除了满足业务的查询功能需求之外,数据的时效性以及存储空间都是系统必须优化考虑的问题。本文对ES中一些常用的匹配查询方式原理及其在业务系统中的使用场景进行介绍。
匹配查询
ElasticSearch官方对文本查询介绍了两种方式:基于词项和基于全文的查询。
基于词项
基于词项的查询只对倒排索引的词项精确匹配,查询时没有分析阶段,不会对词的多样性进行处理,假若使用“Hello”进行关键词检索时,不会搜索到倒排索引中包含单词“hello”的词条。
典型的基于词项查询我们以term query为具体案例进行介绍:
创建索引数据
在解释查询模式之前,首先创建索引数据,在索引中分别定义exact_value和full_text字段作为keyword和text两种数据类型字符串。
PUT my_index/_mapping/my_type
{
"properties": {
"exact_value": {
"type": "keyword"
},
"full_text": {
"type": "text"
}
}
}
PUT my_index/my_type/1
{
"exact_value": "Hello World!",
"full_text": "Hello World!"
}
索引中keyword和text两种不同的数据类型,我来看看两个字段分别在倒排索引中的创建索引的方式。
Keyword:exact_value字段
{
“tokens”: [
{
“token”: “Hello World!”,
“start_offset”: 0,
“end_offset”: 12,
“type”: “word”,
“position”: 0
}
]
}
Text:full_text字段
{
“tokens”: [
{
“token”: “hello”,
“start_offset”: 0,
“end_offset”: 5,
“type”: “”,
“position”: 0
},
{
“token”: “world”,
“start_offset”: 6,
“end_offset”: 11,
“type”: “”,
“position”: 1
}
]
}
由于keyword类型字段不会经过分析器处理,exact_value在倒排索引中以[Hello Wrold!]的完整形式建立索引;text类型经过分析器处理,分析器会将内容进行字符过滤、分词、大小写转换等操作,full_text字段在倒排索引中拆分为[hello、world]两个单词元素形式建立索引。
Term Query
对上述写入索引的文档,使用term查询方式,我们来看看效果
1、GET my_index/my_type/_search
{
"query": {
"term": {
"exact_value": "Hello World!"
}
}
}
2、GET my_index/my_type/_search
{
"query": {
"term": {
"full_text": "Hello World!"
}
}
}
3、GET my_index/my_type/_search
{
"query": {
"term": {
"full_text": "world"
}
}
}
4、GET my_index/my_type/_search
{
"query": {
"term": {
"full_text": "World"
}
}
}
查询结果:
1、查询成功,因为exact_value字段索引中包含完整的内容Hello World!。
2、查询失败,因为full_text字段索引中只包含hello和world这两个词。 它不包含完整的字符串Hello World !。
3、查询成功,词语world匹配full_text的索引单词。
4、查询失败,必须精确匹配world单词,大小写区分导致无法搜索成功。
所以,很明显term查询是一种精确的匹配模式,只有当与倒排索引中的元素完全匹配时,返回结果。
基于全文
基于全文的查询是更高级的查询方式,它首先会了解字段映射的信息。如果查询一个未经过分析的keyword类型字段,它会将整个查询字符串作为单个词项对待;如果要查询一个已分析的text类型字段,它也会先将查询字符串传递到一个到分析器,然后生成一个供查询的词项列表,一旦组成了词项列表,这个查询会对每个词项逐一执行底层的查询,再将结果合并返回。
基于全文的查询我们以match query为具体案例分析,可以发现它与term query的区别。
Match Query
还是以上例中创建的文档,将term查询改为match查询方式:
1、GET my_index/my_type/_search
{
"query": {
"match": {
"exact_value": "Hello World!"
}
}
}
2、GET my_index/my_type/_search
{
"query": {
"match": {
"full_text": "Hello World!"
}
}
}
3、GET my_index/my_type/_search
{
"query": {
"match": {
"full_text": "world"
}
}
}
4、GET my_index/my_type/_search
{
"query": {
"match": {
"full_text": "World"
}
}
}
查询结果:
1、查询成功,因为exact_value字段索引中包含完整的内容Hello World!。
2、查询成功,因为full_text字段在建立倒排索引时经过了分析器处理,match查询也会将输入查询的内容经过分析器处理,所以查询匹配成功。
3、查询成功,词语world匹配full_text的索引单词。
4、查询成功,match查询结果分析器处理后也会对大小写字符进行过滤,因此匹配成功。
由上结果可以发现,相对term而言,match 会是更灵活合适的一种查询方式,它是一个高级全文查询 ,既能处理全文字段,又能处理精确字段。
Match Phrase Query
Match query有一个特点,能够提供单个或多个元素的搜索,但它并不关心元素在文本中的顺序,如果我们需要一种严格的顺序查询方式,这时就可以用到match phrase(短语匹配)查询了,它对查询条件中的语句,要求所有的元素都出现在倒排中,并且连续且顺序一致的排列(分词时,ES已对每个元素标记一个位置以便排序)。
例如:
```bash
1、GET my_index/my_type/_search
{
"query": {
"match_phrase": {
"full_text": "Hello World"
}
}
}
2、GET my_index/my_type/_search
{
"query": {
"match_phrase": {
"full_text": "world hello"
}
}
}
3、GET my_index/my_type/_search
{
"query": {
"match": {
"full_text":{
"query":"world hello",
"operator": "and"
}
}
}
}
查询结果:
1、查询成功。
2、查询失败,因为单词顺序不匹配。
3、查询成功,match query并不关心单词顺序。
部分匹配
上面介绍的所有查询都是针对整个词的操作,只能查找倒排索引中存在的词,最小的单元为单个词。为了提供更灵活的匹配方式,ES也允许用户指定查找词的一部分,并找出所有包含这部分片段的词。类似于SQL中的like “%abc%”的匹配效果。
Prefix Query
它假定传入前缀就正是要查找的前缀,语句例子如下:
```bash
GET my_index/my_type/_search
{ "query": {
"prefix" : { "exact_value" : "Hel" }
}
}
前
缀匹配的执行顺序:
1、扫描词列表并查找到第一个以“Hel”开始的词。
2、搜集关联的文档 ID 。
3、移动到下一个词。
4、如果这个词也是以“Hel”开头,查询跳回到第二步再重复执行,直到所有索引扫描完毕。
Wildcard Query
匹配索引元素中包含字符内容的文档。另外,它也可以使用标准的 shell 通配符查询: ? 匹配任意字符, * 匹配 0个 或多个字符。一般为了防止极慢的通配符查询,通配符术语不应以通配符*或?之一开头。其匹配的执行顺序与前缀匹配相似,语句例子如下:
GET /_search
{
"query": {
"wildcard":{ "exact_value" : "Hel*"}
}
}
Regexp Query
匹配索引元素中满足正则表达式的文档。一般正则表达式对该查询方式的性能也会有很大影响,像[ .* ?+]这样的通配符匹配器会降低性能。其匹配的执行顺序与前缀匹配相似,语句例子如下:
GET /_search
{
"query": {
"regexp":{
"num": "W[0-9].+"
}
}
}
这三种方式都是基于词项查询,它们也需要遍历倒排索引中的词条列表来找到所有的匹配词条,然后逐个词条地收集对应的文档ID。像wildcard query和regexp query的匹配模式灵活多变,但也更为复杂,很有可能需要对中的逐个字符进行严格匹配,会降低查询性能。
业务场景及使用
我们系统的字段内容中主要有两种数据格式,一种为参数值(如pageId=ZZZZ),另一种为json串形式的自定义参数(如param= {“barnd”:“huawei”})。针对前者,业务一般在搜索时会精确知道字段值,我们会对字段设置为keyword的类型,省去在写入时分析步骤,不做索引的分词处理,会明显提升写入效率和节省存储空间;针对后者,json格式不好直接表达,就需要提供模糊搜索的功能,我们会对字段设置为text的类型。
一开始为提供最全面的搜索方式,系统使用wildcard query以支持字符匹配级别的模糊查询,但系统在查询自定义参数等大字段耗时很长,甚至查询超时,导致用户体验不佳。因此系统剔除了部分字符匹配功能,对keyword字段类型,我们会使用match query的查询方式;而json是一种key-value的存储格式,有key在前,value在后的特点,text字段类型就使用match phrase query的方式。
如下我们的业务数据做测试,在我们的测试场景下,部分匹配查询耗时较长,设定为整词匹配模式性能能提升10倍之上。
文档数
查询方式 7亿 7000万 1000万
wildcard 25s 4.7s 1.6s
match 2.3s 400ms 130ms
match phrase 2.5s 500ms 180ms