原文:Elasticsearch入门教程(六):Elasticsearch查询(二)
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。
地理坐标点geo-point
地理坐标点:是指地球表面可以用经纬度描述的一个点。 地理坐标点可以用来计算两个坐标间的距离,还可以判断一个坐标是否在一个区域中,或在聚合中。
地理坐标点不能被动态映射(dynamic mapping)自动检测,而是需要显式声明对应字段类型为geo-point,经纬度信息的形式可以是字符串(“lat,lon”)、数组([lon,lat])、对象({“lat”: xxx, “lon”: xxx}), 注意字符串和数组的经纬度顺序不一致。
longitude:经度
latitude:纬度
PUT /attractions
{
"mappings": {
"restaurant": {
"properties": {
"name": {
"type": "text"
},
"location": {
"type": "geo_point",
"lat_lon": true // location.lat 和 location.lon 字段将被分别索引。它们可以被用于检索,但是不会在检索结果中返回
}
}
}
}
}
// 字符串
PUT /attractions/restaurant/1
{
"name": "Chipotle Mexican Grill",
"location": "40.715, -74.011"
}
// 对象
PUT /attractions/restaurant/2
{
"name": "Pala Pizza",
"location": {
"lat": 40.722,
"lon": -73.989
}
}
// 数组
PUT /attractions/restaurant/3
{
"name": "Mini Munchies Pizza",
"location": [ -73.983, 40.719 ]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
有四种地理坐标点相关的过滤器可以用来选中或者排除文档:
- geo_bounding_box 找出落在指定矩形框中的点。
- geo_distance 找出与指定位置在给定距离内的点。
- geo_distance_range 找出与指定点距离在给定最小距离和最大距离之间的点。
- geo_polygon 找出落在多边形中的点。 这个过滤器使用代价很大
地理坐标过滤器使用代价昂贵 — 所以最好在文档集合尽可能少的场景下使用。你可以先使用那些简单快捷的过滤器,比如 term 或 range ,来过滤掉尽可能多的文档,最后才交给地理坐标过滤器处理。
地理坐标盒模型过滤器:这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。 你指定一个矩形的 左上和右下坐标点,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间:
// 这些坐标也可以用 bottom_left 和 top_right 来表示。
GET /attractions/restaurant/_search
{
"query": {
"geo_bounding_box":{
"type": "indexed",
"location": {
"top_left": {
"lat": 40.8,
"lon": -74.0
},
"bottom_right": {
"lat": 40.7,
"lon": -73.0
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
设置 type 参数为 indexed (替代默认值 memory )来明确告诉 Elasticsearch 对这个过滤器使用倒排索引。
地理距离过滤器:地理距离过滤器( geo_distance )以给定位置为圆心画一个圆,来找出那些地理坐标落在其中的文档:
// 找出所有与指定点距离在 1km 内的 location 字段
GET /attractions/restaurant/_search
{
"query": {
"geo_distance": {
"distance": "1km",
"distance_type": "plane",
"location": {
"lat": 40.715,
"lon": -73.988
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
地理距离过滤器计算代价昂贵。为了优化性能,Elasticsearch 先画一个矩形框来围住整个圆形,这样就可以先用消耗较少的盒模型计算方式来排除掉尽可能多的文档。 然后只对落在盒模型内的这部分点用地理距离计算方式处理。你需要判断你的用户,是否需要如此精确的使用圆模型来做距离过滤?通常使用矩形模型 bounding box 是比地理距离更高效的方式,并且往往也能满足应用需求。
按距离排序:检索结果可以按与指定点的距离排序:当你可以按距离排序时, 按距离打分通常是一个更好的解决方案
GET /attractions/restaurant/_search
{
"query": {
"geo_distance": {
"distance": "1km",
"location": {
"lat": 40.715,
"lon": -73.988
}
}
},
"sort": [
{
"_geo_distance": {
"location": { // 计算每个文档中 location 字段与指定的 lat/lon 点间的距离。
"lat": 40.715,
"lon": -73.998
},
"order": "asc",
"unit": "km", // 将距离以 km 为单位写入到每个返回结果的 sort 键中
"distance_type": "plane" // 使用快速但精度略差的 plane 计算方式
}
}
]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
更快的地理距离计算,两点间的距离计算,有多种牺牲性能换取精度的算法:
arc 最慢但最精确的是 arc 计算方式,这种方式把世界当作球体来处理。不过这种方式的精度有限,因为这个世界并不是完全的球体。
plane plane计算方式把地球当成是平坦的,这种方式快一些但是精度略逊。在赤道附近的位置精度最好,而靠近两极则变差。
sloppy_arc 如此命名,是因为它使用了 Lucene 的 SloppyMath 类。这是一种用精度换取速度的计算方式, 它使用 Haversine formula 来计算距离。它比 arc 计算方式快 4 到 5 倍,并且距离精度达 99.9%。这也是默认的计算方式。
text 和 keyword
text: 文本类型,会对内容进行分词,如果不指定分词则使用默认的standard分词,支持模糊查询
keyword: 关键字类型,不会对内容进行分词,只能按其精确值搜索。,通常用于过滤、排序和聚合
PUT /db
{
"mappings": {
"test": {
"properties": {
"title": {
"type": "keyword"
}
}
}
}
}
PUT /db/test/1
{
"title": "犯我中华者,虽远必诛"
}
// 因为keyword不分词,所以查不到
GET /db/test/_search
{
"query": {
"match": {
"title": "中华"
}
}
}
// 因为keyword不分词,所以可以精确查询
GET /db/test/_search
{
"query": {
"match": {
"title": "犯我中华者,虽远必诛"
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
object 和 nested
object:对象
nested : 嵌套对象,可以看成是数组类型,object和nested不同之处在于对于数组中的元素是对象的元素的索引方式不同。
// 存储一篇博客,同时将博客对应的评论也存储到该博客里面,评论comments是一个数组,数组的每一个元素是一个对象object, 因为没有预先显式的创建索引,所以es会字段自动映射,将comments的类型映射为object
PUT /my_index/blogpost/1
{
"title": "Nest eggs",
"body": "Making your money work...",
"tags": [ "cash", "shares" ],
"comments": [
{
"name": "John Smith",
"comment": "Great article",
"age": 28,
"stars": 4,
"date": "2014-09-01"
},
{
"name": "Alice White",
"comment": "More like this please",
"age": 31,
"stars": 5,
"date": "2014-10-22"
}
]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
// 对于类型为object的对象数组es会被处理成如下的扁平式键值对的结构
// 这种处理方式是将数组中的相同的字段的所有值作为一个数组来作为整个字段的值,如果这样处理的话,就和下面的文档索引方式是一致的,但是这两个文档的数据类型明显是不同的,一个是普通对象,对象中的每个属性的值都是数组;另一个是字段是数组类型(虽然ES中的数据类型是没有数组类型),数组中的每个元素是一个普通对象,每个对象的字段值都是基本数据类型,非数组类型
{
"comments": {
"name": [ alice, john, smith, white ],
"comment": [ article, great, like, more, please, this ],
"age": [ 28, 31 ],
"stars": [ 4, 5 ],
"date": [ 2014-09-01, 2014-10-22 ]
}
}
{
"title": [ eggs, nest ],
"body": [ making, money, work, your ],
"tags": [ cash, shares ],
"comments.name": [ alice, john, smith, white ],
"comments.comment": [ article, great, like, more, please, this ],
"comments.age": [ 28, 31 ],
"comments.stars": [ 4, 5 ],
"comments.date": [ 2014-09-01, 2014-10-22 ]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
// 该查询类似于 select * from blogpost where comments.name like "%Alice%" and comments.age like "%28%"
// 根据es对类型为object的类型的分析,可以看到 comments.name 是一个数组,数组中有个元素是alice,comments.age是一个数组,数组中有一个元素是28,所以两个条件都满足,所以该文档满足查询条件
// 现实结果是该文档满足这两个条件,但是这是我们想要的结果吗???我们的意思是像表达,数组中的某一个元素如果满足这两个条件就查询出来,也就是说如果数组中有一个元素同时满足这两个条件,我们看数组中有两个元素,其中 name like '%Alice%' and age like '%28%' 同时满足这个条件的数组元素是没有的,name包含"alice"的那个元素对应的age是31岁,不是28岁,28岁是name为”john smith“的, 所以这并不是我们想要的查询结果
GET /my_index/blogpost/_search
{
"query": {
"bool": {
"must": [
{ "match": { "comments.name": "Alice" }},
{ "match": { "comments.age": 28 }}
]
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
嵌套对象nested对于类型为数组,数组中的元素是普通对象的这样的数据类型的处理是不一样的, 嵌套对象会将数组中的每个对象作为独立的文档,对于数组es会自动映射为object类型,所以要想使用嵌套对象类型就必须手动显式创建索引,明确指定字段的类型为nested
// 先删除索引
DELETE /my_index
// 重新创建索引,comments的type:nested
PUT /my_index
{
"mappings": {
"blogpost": {
"properties": {
"comments": {
"type": "nested",
"properties": {
"name": { "type": "string" },
"comment": { "type": "string" },
"age": { "type": "short" },
"stars": { "type": "short" },
"date": { "type": "date" }
}
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
// 对于type为nested类型,es会对每个数组元素作为一个单独的索引,对于非数组元素作为一个单独索引,这些都属于同一个文档的索引
{ // 第一个嵌套文档
"comments.name": [ john, smith ],
"comments.comment": [ article, great ],
"comments.age": [ 28 ],
"comments.stars": [ 4 ],
"comments.date": [ 2014-09-01 ]
}
{ // 第二个嵌套文档
"comments.name": [ alice, white ],
"comments.comment": [ like, more, please, this ],
"comments.age": [ 31 ],
"comments.stars": [ 5 ],
"comments.date": [ 2014-10-22 ]
}
{ // 根文档或者称为父文档
"title": [ eggs, nest ],
"body": [ making, money, work, your ],
"tags": [ cash, shares ]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
对于嵌套类型的查询需要专门使用nested查询
GET /my_index/blogpost/_search
{
"query": {
"bool": {
"must": [
{"match": { "title": "eggs"} },
{"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{"match": {"comments.name": "john"}},
{"match": {"comments.age": 28}}
]
}
}
}}
]
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
注意:
must中直接子元素即一级元,即{“match”: { “title”: “eggs”} }是作用在根文档(父文档)上;
nested 子句作用于嵌套字段 comments 。在此查询中,既不能查询根文档字段,也不能查询其他嵌套文档。;
comments.name 和 comments.age 子句操作在同一个嵌套文档中。
nested 字段可以包含其他的 nested 字段。同样地,nested 查询也可以包含其他的 nested 查询。而嵌套的层次会按照你所期待的被应用。
嵌套对象排序
GET /my_index/blogpost/_search
{
"query": {
"nested": {
"path": "comments",
"query": {
"bool": {
"filter": {
"range": {
"comments.date": {
"gte": "2014-10-01",
"lt": "2014-11-01"
}
}
}
}
}
}
},
"sort": [
{
"comments.stars": {
"order": "asc",
"mode": "min"
}
}
]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
// 嵌套文档聚合
GET /my_index/blogpost/_search
{
"aggs": {
"comments": {
"nested": {
"path": "comments"
},
"aggs": {
"by_month": {
"date_histogram": {
"field": "comments.date",
"interval": "month",
"format": "yyyy-MM"
},
"aggs": {
"avg_starts": {
"avg": {
"field": "comments.stars"
}
}
}
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
逆向嵌套聚合:
nested 聚合 只能对嵌套文档的字段进行操作。 根文档或者其他嵌套文档的字段对它是不可见的。 然而,通过 reverse_nested 聚合,我们可以 走出 嵌套层级,回到父级文档进行操作。
reverse_nested 聚合退回根文档.
GET /my_index/blogpost/_search
{
"aggs": {
"comments": {
"nested": {
"path": "comments"
},
"aggs": {
"age_group": {
"histogram": {
"field": "comments.age",
"interval": 10
},
"aggs": {
"blogposts": {
"reverse_nested": { },
"aggs": {
"tags": {
"terms": {
"field": "tags"
}
}
}
}
}
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
嵌套对象的使用时机
嵌套对象 在只有一个主要实体时非常有用,这个主要实体包含有限个紧密关联但又不是很重要的实体,例如我们的 blogpost 对象包含评论对象。 在基于评论的内容查找博客文章时, nested 查询有很大的用处,并且可以提供更快的查询效率。
嵌套模型的缺点如下:
当对嵌套文档做增加、修改或者删除时,整个文档都要重新被索引。嵌套文档越多,这带来的成本就越大。
查询结果返回的是整个文档,而不仅仅是匹配的嵌套文档。尽管目前有计划支持只返回根文档中最佳匹配的嵌套文档,但目前还不支持。
有时你需要在主文档和其关联实体之间做一个完整的隔离设计。这个隔离是由 父子关联 提供的。