ES Query DSL之term level query
Query DSL简介
在公司用ES多是因为ES的开箱即用的分布式和查询速度快,用的较多的查询基本就是term level query,全文查询用的不多,本文总结下常用的term level query。ES提供了一个基于JOSN格式的查询语句-Query DSL,它包含2种类型:叶子查询和组合查询。
叶子查询
叶子查询语句是在一个特定字段(field)上查询特定的值(value),例如match,term或range查询,它们可以单独使用。例如:
GET /pigg/_search
{
"query": {
"term": {
"age": 32
}
}
}
组合查询
组合查询套在别的叶子查询或组合查询外面,它是用来组合逻辑上复杂多样的查询。例如:
GET /pigg/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"age": 32
}
},
{
"match": {
"name": "王"
}
}
]
}
}
}
Query Context 和 Filter Context
一条查询语句的行为取决于是被用在Query Context还是Filter Context |
Query Context
- 在Query Context时,查询语句在问"这个文档匹配程度如何?"。除了判断文档是否匹配,还计算得出一个相关分数放在_score这个元字段上。
- ES根据相关分数来对结果排序,相关分数表示每个文档的匹配程度,相关分数越高说明文档越匹配。
Filter Context
- 在Filter Context时,查询语句在问"这个文档是否匹配?"。返回就是简单的是或否,无需计算相关分数,这通常用作过滤结构化数据。
- 为了提高查询性能,通常Elasticsearch会自动缓存常用的过滤结果。缓存的细节可以看《深入理解ElasticSearch 》,这本书适合看过ES官网后再想了解ES原理时看。
GET /pigg/_doc/_search
{
"query":{
"bool":{
"must":{
"match":{ //这里的match就是query context,会计算相关分数
"first_name":"wang dong"
}
},
"filter":{ //这里的filter就是filtercontext,不计算相关分数
"range":{
"age":{
"gt":20,
"lt":30
}
}
}
}
}
}
词项查询(Term Level Query)
Term Level Query是在字段上查询精确的值,而不是进行全文匹配(比如match)。通常用到的词项查询有term,terms,ids,range,prefix,exists等等,下面一一介绍。
准备数据
插入5条人员数据,其中interest是兴趣,interest_count是指兴趣的个数。
PUT /pigg/_doc/1
{
"name": "王冬冬",
"ename": "winter",
"age": 32,
"about": "I am a good coder",
"interest": ["eat", "coding"],
"interest_count": 2
}
PUT /pigg/_doc/2
{
"name": "朱大旬",
"ename": "vissy",
"age": 29,
"about": "I am a tester",
"interest": ["eat", "testing"],
"interest_count": 2,
"job": null
}
PUT /pigg/_doc/3
{
"name": "王佳冬",
"ename": "micoo",
"age": 3,
"about": "I am a baby",
"interest": ["eat", "play", "sleep"],
"interest_count": 3,
"job": []
}
PUT /pigg/_doc/4
{
"name": "蓝波",
"ename": "lanbo",
"age": 2,
"about": "I am a cat",
"interest": ["eat", "sleep"],
"interest_count": 2,
"job": ""
}
PUT /pigg/_doc/5
{
"name": "波波",
"ename": "bobo",
"age": 1,
"about": "I am a cat",
"interest": ["eat"],
"interest_count": 1,
"job": [null]
}
查看mapping
GET /pigg/_mapping
因为没有提前定义mapping,所以是es自生成的mapping。其中name属性如下,name本身是text类型,内部包含一个keyword类型,用精确查询时,就用name.keyword属性。
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
1 term
term query判断文档的某个字段(field)是否包含某一个确定的值。一般都是用在结构化数据上,比如keyword,int,long,ip,date等,避免用在text类型的字段上,在text上应该用match匹配查询。
1.1 查询age=32
GET /pigg/_search
{
"query": {
"term": {
"age": 32
}
}
}
1.2 查询name.keyword=“王冬冬”
GET /pigg/_search
{
"query": {
"term": {
"name.keyword": "王冬冬"
}
}
}
返回结果如下:
注意返回结果里的相关分数_score |
"hits" : {
"total" : 1,
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "pigg",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,//注意这个相关分数_score
"_source" : {
"name" : "王冬冬",
"ename" : "winter",
"age" : 32,
"about" : "I am a good coder",
"interest" : [
"eat",
"coding"
],
"interest_count" : 2
}
}
]
}
1.3 过滤name.keyword=“王冬冬”
一般做精确查询的时候,没有必要计算相关分数,在上面讲Filter Context的时候,说过用filter不会计算相关分数,也提高了性能,下面就把上面语句改成filter过滤查询试试。
GET /pigg/_search
{
"query": {
"bool": {
"filter": {
"term": {
"name.keyword": "王冬冬"
}
}
}
}
}
结果如下:
"hits" : {
"total" : 1,
"max_score" : 0.0,
"hits" : [
{
"_index" : "pigg",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.0,//没有计算相关分数,默认0.0
"_source" : {
"name" : "王冬冬",
"ename" : "winter",
"age" : 32,
"about" : "I am a good coder",
"interest" : [
"eat",
"coding"
],
"interest_count" : 2
}
}
]
}
1.4 为何要避免在text类型上用Term Level Query
name是text类型,默认下es会分词
1.“王冬冬”被分词为“王”,“冬”这2个词项
2.“王佳冬”被分词为“王”,“佳”,“冬”这3个词项
倒排索引如下:
词项 | 文档ID |
---|---|
王 | 1,3 |
冬 | 1,3 |
佳 | 3 |
如果term查询name=“王冬冬”,因为没有"王冬冬"这个词项,所以下面语句是查不到数据的。
GET /pigg/_search
{
"query": {
"term": {
"name": "王冬冬"
}
}
}
1.5 一定要理解term是包含的意思,不是等于
在工作中经常用term查询id=1,status=1等类似的查询,久而久之有term就是等于的错觉,其实不然。
ES里数据如下:
id | name | interest | interest_count |
---|---|---|---|
1 | 王冬冬 | [“eat”, “coding”] | 2 |
2 | 朱大旬 | [“eat”, “testing”] | 2 |
3 | 王佳冬 | [“eat”, “play”, “sleep”] | 3 |
4 | 蓝波 | [“eat”, “sleep”] | 2 |
5 | 波波 | [“eat”] | 1 |
查询兴趣为吃(interest=“eat”)的数据 |
GET /pigg/_search
{
"query": {
"term": {
"interest.keyword": "eat"
}
}
}
我们本来以为查询结果就1条id=5的数据,只有波波的兴趣只为“eat”。
然而查询结果返回了5条所有数据,因为每条记录都包含了“eat”。
结果如下:
1.6 结合interest_count查询interest
interest_count记录这interest数组里元素个数。
结合interest_count查询查询兴趣只有吃(interest=“eat”)的数据。
GET /pigg/_search
{
"query":{
"bool":{
"must":[
{
"term":{
"interest.keyword":"eat"
}
},
{
"term":{
"interest_count":"1"
}
}
]
}
}
}
注意点:这个只有interest_count=1时才有效 |
- 在下面terms查询时,查询(interest=[“eat”, “sleep”] and interest_count=2)时,用上面的组合查询方式是不会达到预期效果。因为term和terms是包含的意思,[“eat”, “coding”]和[“eat”, “testing”]也符合条件,毕竟它们都包含“eat”且interest_count=2。
- 要想达到预期效果,除了用must+term组成组合查询,还可以用terms_set查询,ES6才有的。
2 terms
- terms和term类似,它判断文档的某个字段(field)是否包含某一个或多个确定的值(value)。
- value参数是一个数组,里面包含多个你想要查询的值,只要有一个值命中就算符合。
- value数组里的最多能放65536个值,如果想修改这个限制,可以修改setting里index.max_terms_count这个配置。
term和terms是包含的意思,上面准备数据中interest字段是数组,下面用查询interest这个字段来说明。 |
2.1 查询interest有play或sleep的人
GET /pigg/_search
{
"query": {
"terms": {
"interest": ["play", "sleep"]
}
}
}
查询结果返回id为3和4的数据,因为他们兴趣包含play或sleep。
id | name | interest | interest_count |
---|---|---|---|
3 | 王佳冬 | [“eat”, “play”, “sleep”] | 3 |
4 | 蓝波 | [“eat”, “sleep”] | 2 |
2.2 用must组合查询interest=[“eat”, “sleep”]
上面也说了用(interest=[“eat”, “sleep”] and interest_count=2)查询是不对的,ES6之前可以用must+term组合查询。相当于interest.contains(“eat”) and interest.contains(“sleep”) and interest_count=2
GET /pigg/_search
{
"query":{
"bool":{
"must":[
{
"term":{
"interest.keyword":"eat"
}
},
{
"term":{
"interest.keyword":"sleep"
}
},
{
"term":{
"interest_count":2
}
}
]
}
}
}
3 terms_set
terms_set查询和terms类似,但它定义了一个最小命中数。比如value数组里有3个值,最小命中数定义为2,说明字段里的值至少命中数组里的2个,才算这个文档符合。而terms查询只要命中数组里任意1个,就算文档符合查询条件。
3.1 至少命中[“eat”, “play”, “sleep”]中2个
GET /pigg/_search
{
"query": {
"terms_set": {
"interest.keyword":{
"terms": ["eat", "play", "sleep"],
"minimum_should_match_script": {
"source": "2"
}
}
}
}
}
3.2 用terms_set解决interest=[“eat”, “sleep”]
GET /pigg/_search
{
"query":{
"bool":{
"must":[
{
"terms_set":{
"interest.keyword":{
"terms":[
"eat",
"sleep"
],
"minimum_should_match_script":{
"source":"2"
}
}
}
},
{
"term": {
"interest_count": {
"value": 2
}
}
}
]
}
}
}
"hits" : {
"total" : 1,
"max_score" : 1.8754687,
"hits" : [
{
"_index" : "pigg",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.8754687,//注意这个评分
"_source" : {
"name" : "蓝波",
"ename" : "lanbo",
"age" : 2,
"about" : "I am a cat",
"interest" : [
"eat",
"sleep"
],
"interest_count" : 2,
"job" : ""
}
}
]
}
3.3 用terms_set解决interest=[“eat”, “sleep”]且不评分
文章最上面讲过用filter context,这样不评分也提高查询性能,以后如果是无需评分的,最好放到filter里面。
GET /pigg/_search
{
"query":{
"bool":{
"filter":{
"bool":{
"must":[
{
"terms_set":{
"interest.keyword":{
"terms":[
"eat",
"sleep"
],
"minimum_should_match_script":{
"source":"2"
}
}
}
},
{
"term":{
"interest_count":{
"value":2
}
}
}
]
}
}
}
}
}
4 ids
根据文档ID查询文档,下面返回1和4两个文档,因为id=100的不存在。
GET /pigg/_search
{
"query": {
"ids": {
"values" : ["1", "4", "100"]
}
}
}
5 exists
在MySQL中,常用 is null和 is not null,ES用exists。
下面情况认定字段不存在 |
- 源JSON中的字段是null或[]
- 字段在mapping中设置为"index" : false
- 字段值的长度超出了mapping中设置的ignore_above
- 字段值格式错误,并且mapping中设置的了ignore_malformed
下面特殊情况认定字段存在 |
- 空字符串,例如"“或”-"
- 包含null和另一个值的数组,例如[null, “foo”]
- 在mapping中设置的自定义null-value(如果不熟,避免使用,用不好坑部门同事)
测试数据如下:
id | name | job |
---|---|---|
1 | 王冬冬 | 缺少job字段 |
2 | 朱大旬 | null |
3 | 王佳冬 | [] |
4 | 蓝波 | ”“ |
5 | 波波 | [null] |
5.1 查询job值存在的文档
GET /pigg/_search
{
"query": {
"exists": {
"field": "job"
}
}
}
返回结果只有id=4的数据。
5.2 查询job值不存在的文档
利用must_not+exists组合查询,代替之前老版本ES的missing查询
GET /pigg/_search
{
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "job"
}
}
]
}
}
}
6 range
range查询判断某个字段是否在某个范围内。range有如下参数:
参数 | 说明 |
---|---|
gte | >= |
gt | > |
lte | <= |
lt | < |
boost | 查询权重,默认1.0 |
6.1 查询age在[3,30)之间的数据
GET /pigg/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gte": 3,
"lt": 30
}
}
}
}
}
}
7 prefix
prefix查询返回在指定字段中包含特定前缀的文档。这个就像我们SQL语句的 like “xx%”。
您可以使用mapping设置中的index_prefixes参数来加快前缀查询的速度。如果启用该参数,Elasticsearch只会为单独的字段索引2到5个字符的前缀。这使Elasticsearch在较大的索引上更有效率执行前缀查询,从而减少成本。
查询ename以“wi”开头的数据。
GET /pigg/_search
{
"query": {
"prefix": {
"ename.keyword": "wi"
}
}
}