ELasticsearch组合查询

Scroll Down
组合查询

基于Elsticsearch5.4.3版本如何执行如下的sql查询

SELECT product FROM products WHERE  (price = 20 OR productID = "XHDK-A-1293-#fJ3") AND  (price != 30)

对应的DSL语句

GET /my_store/products/_search
{
   "query" : {
      "bool" : { 
         "filter" : {
            "bool" : {
              "should" : [
                 { "term" : {"price" : 20}}, 
                 { "term" : {"productID" : "XHDK-A-1293-#fJ3"}} 
              ],
              "must_not" : {
                 "term" : {"price" : 30} 
              }
           }
         }
      }
   }
}
SELECT document
FROM   products
WHERE  productID      = "KDKE-B-9947-#kL5"
  OR (     productID = "JODL-X-1937-#pV7"
       AND price     = 30 )

对应的DSL语句

GET /my_store/products/_search
{
  "from": 0,
  "size": 20,
  "sort": [
    {
      "deal_time": "ASC"
    }
  ],
  "query": {
    "bool": {
      "must_not": [
        {
          "terms": {
            "city.keyword": [
              "XXXXXXXXXXX"
            ]
          }
        }
      ],
      "filter": {
        "bool": {
          "should": [
            {
              "terms": {
                "source.keyword": [
                  "XXXXX",
                  "XXXXXXXXX",
                  "XXXXXXXXX"
                ]
              }
            },
            {
              "bool": {
                "must": [
                  {
                    "terms": {
                      "source.keyword": [
                        "XXXX",
                        "XXXXXXXXXXXXXX"
                      ]
                    }
                  },
                  {
                    "range": {
                      "time": {
                        "gte": "20190301000000"
                      }
                    }
                  }
                ]
              }
            }
          ]
        }
      },
      "must": [
        {
          "bool": {
            "must": [
              {
                "range": {
                  "id": {
                    "lt": 9000000000000000
                  }
                }
              }
            ]
          }
        }
      ]
    }
  }
}
数据建模
嵌套文档

由于在 Elasticsearch 中单个文档的增删改都是原子性操作,那么将相关实体数据都存储在同一文档中也就理所当然。 比如说,我们可以将订单及其明细数据存储在一个文档中。如果我们依赖字段自动映射,那么明细字段会自动映射为 object 类型。这就是如果明细数据是数组类型的话,会被扁平化处理。因为es支持 JSON 还有 null 值,数组,和对象。内部对象会被映射为info.model,内部对象数组的话由于数组在es中会被打破相关性,所以会导致查询的结果不准确。这时候可以使用嵌套文档来处理这种场景。

  • 创建文档
PUT /my_index/order/1
{
  "id": "00000000000000000000000001212212",
  "produceName":  "Apple/苹果 iPhone 8",
  "tags":  [ "手机", "通讯设备" ],
  "info":{
  	"model":"金属",
  },
  "transactions": [ 
    {
      "transactionId":    "20190319122312",
      "name":    "White",
      "comment": "购买套餐1",
      "content": "乞丐版",
      "money":     4888,
      "stars":   4,
      "date":    "2019-03-19"
    },
    {
      "transactionId":    "20190318122312",
      "name":    "Alice",
      "comment": "购买套餐2",
      "content": "土豪专属",
      "money":     6999,
      "stars":   5,
      "date":    "2019-03-18"
    }
  ]
}

上述的文档被存放时,由于es的自动映射会把交易流这个数组映射为object类型。所以文档的中交易编号会以数组的形式来扁平化transactions.transactionId:[20190318122312,20190319122312]存储,其余字段以此类推,进而进行检索时候就会丢失相关性.导致查询的数据不准确,可能就会查询到start是5的乞丐版的交易流,这个时候我们要更改这个映射关系为嵌套文档。查询的时候也采用嵌套文档的查询

  • 更改映射

更改映射的语法如下使用put请求在properties中定义为transactions的type为nested即可。

  PUT /my_index
  {
    "mappings": {
      "order": {
        "properties": {
          "transactions": {
            "type": "nested", 
            "properties": {
              "transactionId":    { "type": "long"  },
              "name":    { "type": "string"  },
              "comment": { "type": "string"  },
              "content": { "type": "string"  },
              "money":     { "type": "short"   },
              "stars":   { "type": "short"   },
              "date":    { "type": "date"    }
            }
          }
        }
      }
    }
  }
  • 嵌套文档查询
    查询语法如下,因为交易流被指定为嵌套文档之后就隐藏在根文档中,无法向搜索根文档中的produceName使用mathch之类的搜索到。必须使用nested子句才可以检索到。
GET /my_index/order/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "produceName": "苹果" 
          }
        },
        {
          "nested": {
            "path": "transactions", 
            "query": {
              "bool": {
                "must": [ 
                  {
                    "match": {
                      "transactions.name": "Alice"
                    }
                  },
                  {
                    "match": {
                      "transactions.money": 6999
                    }
                  }
                ]
              }
            }
          }
        }
      ]
}}}
父子文档模型

场景:有一个公司在多个城市有分公司,并且每一个分公司下面都有很多员工。有这样的需求:按照分公司、员工的维度去搜索,并且把员工和他们工作的分公司联系起。在应用层实现当然也可以 先去查询该地的分公司,然后再去分公司文档查询员工。或者使用不规则文档存储也可以。

例如如下的不规则文档存储可以可以实现的。就是冗余存储。

PUT /myIndex/company/branchcompany/1
{
  "title":    "富土康河南分部",
  "business":  "回收二手航母,维修原子弹。。。",
  "employee":     {
    "id":       1,
    "name":     "张全蛋" ,
    "age":      35
  }
}

使用父子文档模型存储

更改员工的文档映射

PUT /company
{
  "mappings": {
    "branchcompany": {},
    "employee": {
      "_parent": {
        "type": "branchcompany" 
      }
    }
  }
}

父文档的的索引添加和正常的文档添加一样,唯一不同的是在子文档添加的时候需要添加parent参数,该参数的值
为父文档的_id;父文档 ID 有两个作用:创建了父文档和子文档之间的关系,并且保证了父文档和子文档都在同一个分片上。

PUT /company/employee/1?parent=1 
{
  "id":       1,
  "name":  "张全蛋",
  "age":   "25",
  "hobby": "回收二手航母,维修原子弹"
}
PUT /company/employee/2?parent=1 
{
  "id":       12,
  "name":  "黄佳星",
  "age":   "24",
  "hobby": "sleep"
}

has_child 的查询和过滤可以通过子文档的内容来查询父文档,使用如下查询即可查询到张全蛋的信息了。

GET /company/branchcompany/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": {
          "age": {
            "gt": "24"
          }
        }
      }
    }
  }
}

使用 has_parent 语句可以基于父文档来查询子文档。查询将会返回所有在 富土康河南分部 工作的员工

GET /company/employee/_search
{
  "query": {
    "has_parent": {
      "type": "branchcompany",
      "query": {
        "match_phrase": {
           "title": "富土康河南分部"
        }
      }
    }
  }
}

基于父子文档这种建模方式,我们可以建立多种层级关系的模式。当然层级越深,检索的性能就会很差。而且要保证所有的祖孙文档要在同一个分片上。所以在创建子孙辈的文档时需要指定routing用来指定该子孙文档建立在祖宗辈的分片上。


分页查询
深分页(from+size)

考虑到分页过深以及一次请求太多结果的情况,结果集在返回之前先进行排序。但一个请求经常跨越多个分片,每个分片都产生自己的排序结果,这些结果需要进行集中排序以保证整体顺序是正确的。当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。最坏情况下当我们请求第 1000 页--结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。这就可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。

{
 "from": 0,
  "size": 20,
  "query" : {
    "bool" : {
      "must_not" : [ ],
      "must" : [ {
        "bool" : {
          "must" : [ {
            "term" : {
              "id" : 12121121
            }
          } ]
        }
      }, {
        "bool" : {
          "should" : [ ]
        }
      } ]
    }
  },
  "sort" : [ ]
}
scroll模式

由于es深分页(from+size)模式有【index.max_result_window】限制默认是小于10000 而且性能较低,所以采用游标方式来高效获取全部数据。游标模式的下的分页不具有实时性。属于快照模式。第一次访问时候打开游标会返回_scroll_id用于之后的查询,第二次的查询需要携带者第一次返回的_scroll_id

http://x.x.x.x:9200/my_index/order/_search?scroll=2m  post
{
	"size": 2995,
	"sort": [{
		"id": "desc"
	}],
	"_source": ["id"],
	"query": {
		"bool": {
			"must_not": [],
			"must": [{
				"bool": {
					"must": [{
						"term": {
							"status": "SUCCEEDED"
						}
					}]
				}
			}, {
				"bool": {
					"should": []
				}
			}]
		}
	}
}
打开游标返回结果
{
	"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAJvI4WVFB6OGYyQVlTUDI1VE5HZXI3Sm1FQQ==",
	"took": 2,
	"timed_out": false,
	"_shards": {
		"total": 1,
		"successful": 1,
		"failed": 0
	},
	"hits": {
		"total": 5450,
		"max_score": null,
		"hits": [{
			"_index": "my_index",
			"_type": "order",
			"_id": "1000040677",
			"_score": null,
			"_source": {
				"id": "1000040677"
			},
			"sort": [
				1000040677
			]
		}]
	}
}

再查询需要包含一个_scroll_id,并且在没有数据时候返回的hits节点的hits数据节点为空数组。

http://x.x.x.x:9200/_search/scroll  post
{
  "scroll" : "2m",
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAtMmpoWeDlqSUZMeDNRdC11akN5SFl5SS1mdw=="
}
有数据返回结果
{
	"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAtPBkUWeDlqSUZMeDNRdC11akN5SFl5SS1mdw==",
	"took": 4,
	"timed_out": false,
	"_shards": {
		"total": 1,
		"successful": 1,
		"failed": 0
	},
	"hits": {
		"total": 5450,
		"max_score": null,
		"hits": [{
			"_index": "my_index",
			"_type": "order",
			"_id": "1000040671",
			"_score": null,
			"_source": {
				"id": "1000040671"
			},
			"sort": [
				1000040671
			]
		}]
	}
}

无数据返回结果
{
	"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAJvCIWVFB6OGYyQVlTUDI1VE5HZXI3Sm1FQQ==",
	"took": 2,
	"timed_out": false,
	"_shards": {
		"total": 1,
		"successful": 1,
		"failed": 0
	},
	"hits": {
		"total": 5450,
		"max_score": null,
		"hits": []
	}
}

清除游标

http://x.x.x.x:9200/_search/scroll?scroll_id={scroll_id}  DELETE
Search After模式

Pagination of results can be done by using the from and size but the cost becomes prohibitive when the deep pagination is reached. The index.max_result_window which defaults to 10,000 is a safeguard, search requests take heap memory and time proportional to from + size. The Scroll api is recommended for efficient deep scrolling but scroll contexts are costly and it is not recommended to use it for real time user requests. The search_after parameter circumvents this problem by providing a live cursor. The idea is to use the results from the previous page to help the retrieval of the next page. --摘自官网 最佳解释。提升逼格

针对深分页和快照模式的游标分页的试用场景。针对数据fetch不太多,而且集群规模不大的的情况 可以试用深分页,针对数据要求实时性不是很高的情况,可以使用scroll模式,但是需要实时性比较高的情况,就需要使用Elasticsearch提供的第三种分页模式了。该模式需要配合排序字段来使用,下一次的查询使用活动的光标去fetch下一页的数据。也就是说需要把第一次返回文档的最后一个排序字段的sort结果放在search_after节点中。和游标查询每次传输scroll_id的方式类似,但是search_after的方式可以感知最新的文档结果,也就说改分页查询是实时的。使用Search After查询模式。from不设置,或者设置为0或者-1

http://x.x.x.x:9200/my_index/order/_search  post
{
  "size": 10,
  "sort": [
    {
      "id": "desc"
    }
  ],
  "_source": [
    "id"
  ],
  "search_after": [
    1000040592//该值为上一次查询的最后一个文档的sort的值
  ],
  "query": {
    "bool": {
      "must_not": [],
      "must": [
        {
          "bool": {
            "must": [
              {
                "term": {
                  "status": "SUCCEEDED"
                }
              }
            ]
          }
        },
        {
          "bool": {
            "should": []
          }
        }
      ]
    }
  }
}