ElasticSearch查询DSL之Term级别查询(ids、exists、prefix、range、wildcard、regexp、trem、terms、fuzzy)

Term级别查询

term这个单词汉语翻译是术语、条款等意思,在es中翻译过来我总感觉怪怪的。es官网将ids、term、terms、fuzzy等查询方式放在这个分类下,他们是dsl语句中最基本的语句,大都是单条件查询。其中ids、esists、range、term、terms等查询方式是精确匹配,而fuzzy、wildcard、regexp、prefix都是模糊匹配。接下来让我们一起看看他们应该怎么用吧。

ids(Ids Query)

ids是相对来说比较简单的一种dsl,类似于mysql的where id in ()的语义,他支持value属性,可以传入一个数组,里面填上你想要查询的ID的文档即可

GET /_search
{
  "query": {
    "ids" : {
      "values" : ["1", "4", "100"]  # 返回属性_id为1、4、100的文档,如果存在的话
    }
  }
}

exists(Exists Query)

exists是用来匹配文档的mapping中是否包含某一个字段,如果是就返回。比如我有下面两个文档:ID为4的文档有两个字段:title和body,ID为5的文档还有一个字段content。那么如果我们想要查询包含content字段的文档,就可以用exists去做。

POST baike/_doc/4
{
  "title": "Quick brown rabbits",
  "body": "Brown rabbits are commonly seen."
}

POST baike/_doc/5
{
  "title": "Keeping pets healthy",
  "body": "My quick brown fox eats rabbits on a regular basis.",
  "content": "test exists"
}

这条dsl只会返回ID为5的文档

POST /baike/_search
{
  "query": {
    "exists": {"field": "content"}
  }
}

还有一点需要注意的是,如果content的value值为null或者[],那么是不会被匹配到的。

prefix (Prefix Query)

prefix查询很好理解,就是将查询关键字作为一个前缀进行匹配,比如下面的例子,如果title是以Ela作为前缀的都会被匹配到,这里是区分大小写的,如果要设置不区分大小写,可以将case_insensitive设置为true。

POST /baike/_search
{
  "query": {
    "prefix": {
      "title.keyword": {
        "value": "Ela"  # title是以Ela作为前缀的都可以匹配到,注意ela并不会被匹配到
      }
    }
  }
}

"hits" : {
    "total" : {
        "value" : 1,
        "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
        {
            "_index" : "baike",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 1.0,
            "_source" : {
                "title" : "ElasticSearch",
                "body" : "the learn note about es"
            }
        }
    ]
}

上面的查询可以简写为下面这样:

POST /baike/_search
{
  "query": {
    "prefix": { "title.keyword":"Ela"}
  }
}

range(Range Query)

range查询,更好理解了,就是范围查询嘛,他的语法可以参考下面的代码。这里呢出现了gte、lte是什么意思呢,其实就是ES中对于范围的描述,这里给大家总结一下:

gt:大于
gte:大于等于
lt:小于
lte:小于等于

查询在10到20之间的

GET /_search
{
  "query": {
    "range": {
      "age": {
        "gte": 10,
        "lte": 20,
        "boost": 2.0
      }
    }
  }
}

除了上面说到的表示范围的参数之外,range查询还支持下面的一些参数:

format:string类型,支持对查询的时间进行格式化,如果我们不提供会用默认的格式:"yyyy-MM-dd"
time_zone:用来指定时区,可以是UTC时区的偏移量,比如+01:00,或者IANA时区,比如America/Los_Angeles
relation:用来表示我们指定的范围和文档中数据的关系。一共有三种枚举值:
INTERSECTS (Default):匹配的文档中的范围字段的值和我们的指定的查询范围是一个交集的关系
CONTAINS:匹配的文档中的范围字段的值完全包含了我们指定的查询范围
WITHIN:匹配的文档中的范围字段的值在我们的查询范围之中
range查询也可以用在时间上,并且还可以做时间的计算,在es中,用y表示年,M表示月,d表示天,h表示小时。。。因此可以用+1d、-2h这种方法来进行时间的计算,可以通过/d表示四舍五入到最近的一天。也可以支持now取当前时间。关于时间计算的更多资料可以参考:Date Math

GET /_search
{
  "query": {
    "range": {
      "timestamp": {
        "time_zone": "+01:00", 
        "gte": "2020-01-01T00:00:00", 
        "lte": "now"
      }
    }
  }
}

wildcard(Wildcard Query)

waildcard中文是通配符的意思,通配符是匹配一个或者多个字符的占位符,我们平时也总会遇到。es也为我们提供了这种查询方式。例如下面的例子,只要文档中的user.id以ki开始,以y结尾,那么都能匹配到。这里的 * 表示可以匹配多个字符。wildcard查询支持下面几个参数:

value:查询条件,就是我们指定的通配符,目前支持两种:表示可以匹配0到多个字符,?表示匹配任何单个字符。注意在正则表达式中,不能用或者?开头,因为这样可能匹配到大量的数据,导致性能下降严重。

boost:用来减少或增加查询相关性算分的参数。

case_insensitive:默认值false,如果设置为true,表示不区分大小写。

rewrite:可以重写查询方法,目前还没实践到,关于更多说明可以参考:rewrite parameter

GET /_search
{
  "query": {
    "wildcard": {
      "user.id": {
        "value": "ki*y",
        "boost": 1.0,
        "rewrite": "constant_score"
      }
    }
  }
}

regexp(Regexp Query)

上面提到的通配符查询,功能还是太有限了,如果能支持正则表达式岂不是堪称完美?说曹操曹操到,正则表达式查询,ES也为我们提供了。例如下面的例子,会匹配到user.id是以k开头,y结尾的单词。中间的.*表示匹配任意长度的任意字符像ky、kay、kimchy等都可以被匹配到。

在regexp中,有这么几个参数,需要关注一下,除过value,其他都是可选参数:

value:查询条件,指定我们输入的正则表达式,关于更多的正则表达式语句可以参考:Regular expression syntax
flags:可以通过这个参数为正则表达式提供一些更丰富的选项
ALL:默认值,允许所有的操作符
COMPLEMENT:允许操作符,用来实现一个求反的效果,例如:abc可以匹配到adc,aec,但是不会匹配到abc。
INTERVAL:允许操作符<>,用来匹配数字范围,例如:foo<01-100>可以匹配到foo01一直到foo100
INTERSECTION:允许操作符&,用来and的意思,就是说&符号两遍的表达式都满足才能被匹配到,例如aaa.+&.+bbb可以匹配到aaabbb
ANYSTRING:允许操作符@,用来匹配任何一个完整的字符串。例如@&~(abc.+)匹配任何字符串,除过以abc开头的
case_insensitive:默认值false,如果设置为true,表示正则表达式不区分大小写。
max_determinized_states:据官网介绍,es底层对正则表达式的解析是通过lucene来实现的,那么在lucene解析正则表达式的过程中,会将每个正则表达式转换为包含多个确定状态的状态机,默认值是10000,我们也可以通过这个参数去修改。而且据官网说这个生成状态机的过程是一个很耗时的过程,为了防止资源耗尽,我们应该控制这个参数。如果一个正则表达式特别复杂的话,也可以适当的去把这个参数往大调整。这块目前没有进行测试和深入研究,后续有机会补充。这里有一篇es中文社区的博客,对这个参数做了一个较为详细的说明:ElasticSearch集群故障案例分析: 警惕通配符查询
rewrite:可以重写查询方法,目前还没实践到,关于更多说明可以参考:rewrite parameter

GET /_search
{
  "query": {
    "regexp": {
      "user.id": {
        "value": "k.*y",
        "flags": "ALL",
        "case_insensitive": true,
        "max_determinized_states": 10000,
        "rewrite": "constant_score"
      }
    }
  }
}

term(Term Query)

term汉语翻译是术语、条款等意思,大概我觉得可以理解为一个最小单位的概念,他在ES中用来做精确查询。但是有一点需要注意的是,在ES中保存的文档,都会对单词进行分词处理,然后建立倒排索引,比如我们存了一个HelloWorld,但是很有可能会被分词成hello和world两个单词,这个时候如果你用Term查询,通过HelloWorld是查不到的,因为Term是不做分词处理的。这个时候可以通过match或者keyword来代替。

通过term这种查询方式在blogs这个索引中查找title为Quick disjunction的文档,是找不到的,因为Quick disjunction已经做了分词处理

POST blogs/_search
{
  "query": {
    "term": {
      "title": {  # 查找的字段
        "value": "Quick disjunction"  # 查找的数据
      }
    }
  }
}

将上面的语句稍作改动即可

POST blogs/_search
{
  "query": {
    "term": {
      "title.keyword": { # 每个字段都有一个keyword这个属性,是没有分词之前元数据
        "value": "Quick disjunction"
      }
    }
  }
}

通过上面的方式去做查询,返回的结果会给每一个文档做一个相关性算分,用_score来表示,如果一个查询匹配到多条数据,那么_score最高的会排在最前面,表示匹配度最高。这个算分的过程其实是比较消耗性能的,如果我们不关注这个属性的话,可以通过Filter的方式绕过算分这个环节,避免一些开销,并且Filter还可以利用缓存,提升查询效率。

POST blogs/_search
{
  "query": {
    "constant_score": {  # constant:常数,表示跳过算分,返回的_score是一个常数
      "filter": {
        "term": {
          "title.keyword": {
            "value": "Quick disjunction"
          }
        }
      }
    }
  }
}

但是可能大多数情况下,我们还是比较关注score这个分数的,甚至有可能希望为某一次特殊的查询去调整这个分数,比如百度竞价排名的广告。。那么这个时候可以通过另一个参数boost (提高,使增长)这个参数去实现。他是一个浮点类型的数字,默认是1.0,如果我们想去降低分数,只需要把他控制在0.0-1.0之间,越小分数越低,如果我们想提高分数,只需要把他从1.0开始往大调整,越大算出来的分数越高。

POST blogs/_search
{
  "query": {
    "term": {
      "title.keyword": {
        "value": "Quick disjunction",
        "boost": 2
      }
    }
  }
}

terms(Terms Query)

term查询是单字符串单字段的查询方式,terms就是多字符串单字段的查询,下面是一个例子:

POST /baike/_search
{
  "query": {
    "terms": {
      "title.keyword": [   # 这里是一个数组,可以写多个关键字作为查询条件,只要title符合其中一个就可以匹配到
        "ElasticSearch",
        "Keeping pets healthy"
      ]
    }
  }
}

terms除了上述特性外,还有一个特别有用的功能,那就是关联索引查询,官网叫做Terms lookup,可以实现类似数据库的join查询。这个可以用在什么地方呢?比如我是一个B站的用户,那么每次我进入B站后,他都会根据我的喜好就行定制化推荐,比如我经常浏览鬼畜、二次元、计算机方面的视频,就会给我推荐这方面的视频和up主。但是如果是另一个同学,可能喜欢的是游戏,旅游,音乐,那么B站也应该推荐这些方面的内容。这个需求呢就可以通过terms lookup来做。我们一起来看下。

假设这是B站的用户信息,id是用户名,categories里记录了B大数据分析得出的他平时喜欢的视频分类

PUT /user-profiles/_doc/hxy
{
  "categories" : ["technology","java","ghost livestock"]
}

PUT /user-profiles/_doc/yj
{
  "categories" : ["tourism"]
}

这里是B站的一些视频,其中每一个视频有一个分类,用category来表示

PUT /bilibili/_bulk
{"index":{"_id":"elasticsearch-definitive-guide"}}
{"name":"Elasticsearch - The definitive guide","category":"technology"}
{"index":{"_id":"seven-databases"}}
{"name":"Seven Databases in Seven Weeks","category":"technology"}
{"index":{"_id":"SpringBoot"}}
{"name":"Springboot learn note","category":"java"}
{"index":{"_id":"The potala palace"}}

接下来我们通过Terms lookup的方式进行查询,这里将索引bilibili和user-profiles进行了关联,首先在user-profiles这个索引里通过用户ID查询到用户的categories,然后将他作为动态的参数,最后在bilibili这个索引中,将前面的动态参数作为检索条件,从bilibili中查询,如果他里面的某个文档的category字段的数据和我们的动态参数中的能匹配上,就会被命中。

这里有四个参数,对上面说的过程进行了抽象

index:用来表示你从中获取字段值的索引名称,这个是能确定的,比如上文提到的user-profiles

id:你的文档ID,这个也是能确定的,用户一登录你就可以获取到

path:用来表示你从中获取字段值的字段名称

routing:这是一个非必选参数,在有自定义路由的时候使用,后续有机会在探讨

POST /bilibili/_search
{
  "query": {
    "terms": {
      "category": {
        "index":"user-profiles",
        "id": "hxy",
        "path": "categories"
      }
    }
  }
}

"hits" : {
    "total" : {
        "value" : 3,
        "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
        {
            "_index" : "bilibili",
            "_type" : "_doc",
            "_id" : "elasticsearch-definitive-guide",
            "_score" : 1.0,
            "_source" : {
                "name" : "Elasticsearch - The definitive guide",
                "category" : "technology"
            }
        },
        {
            "_index" : "bilibili",
            "_type" : "_doc",
            "_id" : "seven-databases",
            "_score" : 1.0,
            "_source" : {
                "name" : "Seven Databases in Seven Weeks",
                "category" : "technology"
            }
        },
        {
            "_index" : "bilibili",
            "_type" : "_doc",
            "_id" : "SpringBoot",
            "_score" : 1.0,
            "_source" : {
                "name" : "Springboot learn note",
                "category" : "java"
            }
        }
    ]
}

fuzzy(Fuzzy Query)

上面提到的term查询是一种精确查询,必须要求你输入的查询条件和文档中的数据完全匹配才可以。但是有时候可能用户忘了一个单词怎么写,只记得前3个字母,或者拼写错了,那么如果用term是查不到的。这个时候我们就需要根据用户的输入做一个猜测,进行一个模糊匹配。那么fuzzy正好是为了解决这个问题而出现的。

为了理解fuzzy的用法,这里我们需要先了解一个概念:编辑距离,他是一个单词要变成另一个单词,需要经过几次转换的这个次数。比如java这个单词要变成jbva,只需要将b替换成a,因此这个编辑距离就是1。那么这个转换总共有四种手段

改变其中一个字母,比如box -> fox
删除其中一个字符,比如black -> lack
插入一个新的字母,比如sic -> sick
调整两个相邻字母的位置,比如cat -> act
fuzzy实现模糊查询就是基于这个编辑距离去做的,他会将用户输入的搜索条件,结合一个编辑距离,然后基于我们上面提到的四种手段去做一个扩展和变形,得到一个集合,这个过程我们可以叫做扩展模糊选项,然后将扩展后的模糊选项作为查询条件展开精确匹配,最终将所有的结果进行返回。那么这个编辑距离就需要我们去指定,在es中是通过fuzziness去指定的,他的含义就是最大编辑距离。

POST /baike/_search
{
  "query": {
    "fuzzy": {
      "title.keyword": {
        "value": "ElasticSearh",   # es中存储的是ElasticSearch,编辑距离是2,那么只要你的输入经过两次转换都变成ElasticSearch,就会匹配到
        "fuzziness": 2
      }
    }
  }
}

fuzzy除了可以指定最大编辑距离之外,还有其他几个参数,这里在做一个总结:

fuzziness:最大编辑距离,值可以是数字类型,也可以是AUTO:[low],[high]这种格式,表示如果查询的单词长度在 [0,low) 这个范围内,编辑距离为0,如果在 [low,high) 这个范围之内,编辑距离为1,如果大于high,则编辑距离为2;除了这两种格式外,还支持AUTO这种写法,等同于AUTO:3,6

max_expansions:最大扩展数量,前面我们提到了扩展模糊选项,假如一个查询扩展了3到5个扩展选项,那么是是很有意义的,如果扩展了1000个模糊选项,其实也就意义不大了,会让我们又迷失在海量的数据中。因此有了max_expansions这个参数,限制最大扩展数量,默认值是50。切记这个值不可以太大,否则会导致性能问题

prefix_length:指定开始多少个字符不可以被模糊

transpositions:boolean值,默认true,表示扩展模糊选项的时候,是否允许两个相邻字符的位置互换。实践过程设置了false,理论上我存储ElasticSearch,检索ElasticSaerch应该搜不到,但是却搜索到了。有点不太理解。希望得到大佬的指点

rewrite:参考上文

posted @ 2022-07-23 13:56  方东信  阅读(2081)  评论(0编辑  收藏  举报