Elasticsearch:Geo Point 和 Geo Shape 查询解释

1,837 阅读14分钟

在本文中,我们将了解 Elasticsearch 的地理查询、如何设置映射和索引,并提供一些示例来说明如何查询数据。

Elasticsearch 中的地理数据

Elasticsearch 允许你以两种方式表示 GeoData:geo_shapegeo_point

Geo Point 允许你将数据存储为纬度和经度坐标对。 当你要针对点之间的距离过滤数据、在边界框内搜索或使用聚合时,请使用此字段类型。 你可以指定许多超出本文范围的功能和选项。 我们将在这里介绍几个,但你可以在 Elasticsearch 的文档中查看地理边界框、地理距离和地理聚合的选项。

当你拥有表示形状的 GeoData 时,或者当你想要查询形状内的点时,请使用 Geo-Shape。 geo_shape 数据必须以 GeoJSON 格式编码,该格式被转换为表示 Geohash 单元格网格上的长/纬度坐标对的字符串。 由于 Elasticsearch 将形状作为术语进行索引,因此很容易确定形状之间的关系,这些关系可以使用相交、不相交、包含或在查询空间关系运算符中进行查询。更多关于 geohash 的学习,请参阅 “Elasticsearch:理解 Elastic Maps 中的 geohash 及其聚合”。

遗憾的是 geo-point 和 geo-shape 不能一起查询。 例如,如果要获取指定多边形内的所有城市,则不能使用以地理点为索引的城市。 它们必须使用 GeoJSON 中的 “type": "point" 进行索引,并作为 geo-shape 进行索引。

Geo Point 字段类型

geo_point 类型的字段接受经纬度对,可以使用:

Geo point 映射

我们可以使用如下的方式来定义一个带有 geo_point 数据类型的索引:



1.  PUT location_index
2.  {
3.    "mappings": {
4.      "properties": {
5.        "text" : {
6.          "type" : "text"
7.        },
8.        "location": {
9.          "type": "geo_point"
10.        }
11.      }
12.    }
13.  }


我们可以用五种不同的方式存储 Geo Point。

Geo point 作为一个对象

对象可以与 lat 和 lon 等属性一起使用。



1.  PUT location_index/_doc/1
2.  {
3.    "text": "Geopoint as an object",
4.    "location": { 
5.      "lat": 41.12,
6.      "lon": -71.34
7.    }
8.  }


Geo Point 作为一个字符串

一个纯字符串,可以使用 “,” 分隔,格式为 lat, lon 。



1.  PUT location_index/_doc/2
2.  {
3.    "text": "Geopoint as a string",
4.    "location": "41.12,-71.34" 
5.  }


Geo Point 作为 Geohash

哈希值用于表示 lat 和 lon 。 有一个在线网站可以这样做:



1.  PUT location_index/_doc/3
2.  {
3.    "text": "Geopoint as a geohash",
4.    "location": "drm3btev3e86" 
5.  }


Geo point 作为数组

坐标可以用数组 [lon, lat] 的形式表示,值为 double 。



1.  PUT location_index/_doc/4
2.  {
3.    "text": "Geopoint as an array",
4.    "location": [ -71.34, 41.12 ] 
5.  }


Geo Point 作为 WKT Point

坐标可以用函数 POINT(lon lat) 的形式表示。



1.  PUT location_index/_doc/5
2.  {
3.    "text": "Geopoint as a WKT POINT primitive",
4.    "location" : "POINT (-71.34 41.12)" 
5.  }


注意:无论地理点以何种格式保存,我们也可以查询其他格式。 但要小心正确定义格式。 不要替换为 lat 和 lon 值。 这可以给出未预先确定的值。

Geo shape 字段类型

geo_shape 数据类型有助于对任意地理形状(例如矩形和多边形)进行索引和搜索。 当被索引的数据或被执行的查询包含形状而不仅仅是点时,应该使用它。

你可以使用 geo_shape 查询使用此类型查询文档。

Geo shape 映射



1.  PUT geo_shape_indx
2.  {
3.    "mappings": {
4.      "properties": {
5.        "location": {
6.          "type": "geo_shape"
7.        }
8.      }
9.    }
10.  }


我们有如下的几种方式来存储 geo_shape 数据。

Geo Json 类型 POINT

单个地理坐标。 注意:Elasticsearch 仅使用 WGS-84 坐标。



1.  POST geo_shape_indx/_doc/1
2.  {
3.    "location" : {
4.      "type" : "point",
5.      "coordinates" : [-77.03653, 38.897676]
6.    }
7.  }


Geo Json 类型 LINESTRING

给定两个或多个点的任意线。



1.  POST geo_shape_indx/_doc/2
2.  {
3.    "location" : {
4.      "type" : "linestring",
5.      "coordinates" : [[-77.03653, 38.897676], [-77.009051, 38.889939]]
6.    }
7.  }


Geo Json 类型 POLYGON

一个封闭的多边形,其首点和末点必须匹配,因此需要 n + 1 个顶点来创建 n 边多边形,并且最少需要 4 个顶点。



1.  POST geo_shape_indx/_doc/3
2.  {
3.    "location" : {
4.      "type" : "polygon",
5.      "coordinates" : [
6.        [ [-77.03653, 38.897676], [-77.03653, 37.897676], [-76.03653, 38.897676], [-77.03653, 38.997676], [-77.03653, 38.897676] ]
7.      ]
8.    }
9.  }


Geo Json 类型 MULTIPOLYGON

一组单独的多边形:



1.  POST geo_shape_indx/_doc/4
2.  {
3.    "location" : {
4.      "type" : "MultiPolygon",
5.      "coordinates" : [
6.        [ [[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]] ],
7.        [ [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],
8.          [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]] ]
9.      ]
10.    }
11.  }


Geo Json 类型 MULTIPOINT

一组未连接但可能相关的点:



1.  POST geo_shape_indx/_doc/5
2.  {
3.    "location" : {
4.      "type" : "multipoint",
5.      "coordinates" : [
6.        [-78.0, 38.0], [-79.0, 38.0]
7.      ]
8.    }
9.  }


Geo Json 类型 MULTILINESTRING

一组单独的线字符串:



1.  POST geo_shape_indx/_doc/6
2.  {
3.    "location" : {
4.      "type" : "multilinestring",
5.      "coordinates" : [
6.        [ [-77.03, 38.89], [-78.03, 38.89], [-78.03, 39.89], [-78.03, 39.89] ],
7.        [ [-76.03, 36.89], [-77.03, 36.89], [-77.03, 37.89], [-76.03, 37.89] ],
8.        [ [-76.23, 36.69], [-76.03, 36.89], [-76.23, 36.89], [-76.23, 36.09] ]
9.      ]
10.    }
11.  }


Geo Json 类型 GEOMETRYCOLLECTION

GeoJSON 形状类似于 multi* 形状,只是多种类型可以共存(例如,Point 和 LineString)。



1.  POST geo_shape_indx/_doc/7
2.  {
3.    "location" : {
4.      "type": "geometrycollection",
5.      "geometries": [
6.        {
7.          "type": "point",
8.          "coordinates" : [-77.03653, 38.897676]
9.        },
10.        {
11.          "type": "linestring",
12.          "coordinates" : [[-77.03653, 38.897676], [-77.009051, 38.889939]]
13.        }
14.      ]
15.    }
16.  }


Geo Json 类型 BBOX(Elastic Search 中的 ENVELOPE)

边界矩形或信封,通过仅指定左上角和右下角点来指定。



1.  POST geo_shape_indx/_doc/8
2.  {
3.    "location" : {
4.      "type" : "envelope",
5.      "coordinates" : [ [-77.03653, 38.897676], [-76.03653, 37.897676] ]
6.    }
7.  }


通过使用 geo_point 或 geo_shape,Elasticsearch 将自动查找坐标,根据需要的格式对其进行验证,并对其进行索引。

将数据加载到 Elasticsearch

安装 Elasticsearch 及 Kibana

如果你还没有安装好自己的 Elasticsearch 及 Kibana 的话,请参考我之前的文章 “Elasticsearch:如何在 Docker 上运行 Elasticsearch 8.x 进行本地开发”。我们采用 docker-compose 来进行安装。为了方便测试,我们将不使用安全配置。你也可以参考文章 “Elastic Stack 8.0 安装 - 保护你的 Elastic Stack 现在比以往任何时候都简单” 中的 “如何配置 Elasticsearch 不带安全性” 章节来进行安装。在我们今天测试中,我们将使用最新的 Elastic Stack 8.6.2 来进行测试。

我们将在本演练中使用的数据取自华盛顿州交通部 (WSDOT) 地理数据目录。 下载 “City Points” 和 “WSDOT Regions 24k” 的 shapefile。 City Points 将为我们提供华盛顿的城市,而 WSDOT Regions 将为我们提供 WSDOT 指定的区域。 你可以在下载前单击下载链接旁边的查看来查看数据。 我已将 shapefile 转换为 GeoJSON 格式。

我创建了一个 nodejs 应用程序来创建索引和加载数据。 请按照 Github 链接中的步骤操作,并按照 README 文件加载数据。我们使用如下的命令来下载代码:

git clone https://github.com/liu-xiao-guo/node_playground
1.  $ pwd
2.  /Users/liuxg/nodejs/node_playground/elastic-geo-spatial
3.  $ ls
4.  README.md           cities.json         countys.json        package-lock.json
5.  cities.js           countys.js          docker-compose.yaml package.json
6.  $ npm install
7.  npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/
8.  npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/

10.  added 6 packages in 2s
11.  npm notice 
12.  npm notice New major version of npm available! 8.19.2 -> 9.6.2
13.  npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.6.2
14.  npm notice Run npm install -g npm@9.6.2 to update!
15.  npm notice 

我们运行如下的命令来写入索引 geo_cities_point:

 node cities.js

我们运行如下的命令来写入索引 geo_cities_shapes:

node countys.js

我们通过如下的命令来查看最新写入的索引:

GET _cat/indices

我们可以通过如下的命令来查看两个索引的 mapping:

GET geo_cities_point/_mapping


1.  {
2.    "geo_cities_point": {
3.      "mappings": {
4.        "properties": {
5.          "GNIS": {
6.            "type": "integer"
7.          },
8.          "location": {
9.            "type": "geo_point"
10.          },
11.          "name": {
12.            "type": "text"
13.          },
14.          "objectId": {
15.            "type": "integer"
16.          }
17.        }
18.      }
19.    }
20.  }


 上面显示 loclation 字段为 geo_point 类型。我们通过如下的命令来查看 geo_cities_shapes 的 mapping:

GET geo_cities_shapes/_mapping


1.  {
2.    "geo_cities_shapes": {
3.      "mappings": {
4.        "properties": {
5.          "location": {
6.            "type": "geo_shape"
7.          },
8.          "name": {
9.            "type": "text"
10.          }
11.        }
12.      }
13.    }
14.  }


上面表明 location 字段为一个 geo_shape 的类型。

Geo POINT 查询

Elasticsearch 使用术语查询和过滤器。 查询依赖于 “评分”,或者文档是否与查询匹配以及匹配的程度如何。 另一方面,过滤是 “非评分” 的,它确定文档是否与查询匹配。 根据 Elasticsearch,从 2.x 开始,查询和过滤已成为同义词,因为你可以同时拥有评分和非评分查询。 使用评分或非评分查询有各种性能优势和缺点,但经验法则是当相关性分数很重要时使用评分查询,而对其他一切使用非评分查询。

由于我们的索引中有一些数据,是时候开始查询了。 我们将了解一些可用于 geo_point 和 geo_shape 的基本查询。

与地理点的距离

要获得任意两点之间的距离,我们的数据必须使用 geo_point 类型存储。 该文档提供了各种数据格式作为示例。 在 GeoPoint 的给定距离内匹配 geo_point 和 geo_shape 值。 以下查询列出了 10 英里距离内的位置。



1.  GET geo_cities_point/_search
2.  {
3.    "query": {
4.      "bool": {
5.        "must": {
6.          "match_all": {}
7.        },
8.        "filter": {
9.          "geo_distance": {
10.            "distance": "10mi",
11.            "location": [
12.              -122.3375,
13.              47.6112
14.            ]
15.          }
16.        }
17.      }
18.    }
19.  }


Geo Distance Aggregation

适用于 geo_point 字段的多桶聚合,在概念上与范围聚合非常相似。 用户可以定义一个原点和一组距离范围桶。 聚合评估每个文档值与原点的距离,并根据范围确定其所属的桶(如果文档与原点之间的距离在桶的距离范围内,则文档属于该桶)。

有时我们需要知道一个范围内的坐标数。 这是一个用于列出结果的聚合函数。 现在我们将尝试找到直到 10 MI 、从 10 MI 到 50 MI 、从 50 MI 到 100 MI 和从 100 MI 的坐标。 这应该返回范围内匹配的文档数。



1.  GET geo_cities_point/_search?size=0&filter_path=aggregations
2.  {
3.    "aggs": {
4.      "data_around_city": {
5.        "geo_distance": {
6.          "unit": "mi",
7.          "field": "location",
8.          "origin": "47.6112, -122.3375",
9.          "ranges": [
10.            {
11.              "to": 10
12.            },
13.            {
14.              "from": 10,
15.              "to": 50
16.            },
17.            {
18.              "from": 50,
19.              "to": 100
20.            },
21.            {
22.              "from": 100
23.            }
24.          ]
25.        }
26.      }
27.    }
28.  }


上述命令的返回值为:



1.  {
2.    "aggregations": {
3.      "data_around_city": {
4.        "buckets": [
5.          {
6.            "key": "*-10.0",
7.            "from": 0,
8.            "to": 10,
9.            "doc_count": 12
10.          },
11.          {
12.            "key": "10.0-50.0",
13.            "from": 10,
14.            "to": 50,
15.            "doc_count": 77
16.          },
17.          {
18.            "key": "50.0-100.0",
19.            "from": 50,
20.            "to": 100,
21.            "doc_count": 57
22.          },
23.          {
24.            "key": "100.0-*",
25.            "from": 100,
26.            "doc_count": 135
27.          }
28.        ]
29.      }
30.    }
31.  }


地理多边形里的地理点

我们也可以返回仅落在多边形点内的命中的查询:



1.  GET geo_cities_point/_search?filter_path=**.hits
2.  {
3.    "_source": false, 
4.    "fields": [
5.      "objectId",
6.      "name"
7.    ], 
8.    "query": {
9.      "bool": {
10.        "must": {
11.          "match_all": {}
12.        },
13.        "filter": {
14.          "geo_shape": {
15.            "location": {
16.              "shape": {
17.                "type": "polygon",
18.                "relation": "within",
19.                "coordinates": [
20.                  [
21.                    [
22.                      -122.35610961914062,
23.                      47.70514099299205
24.                    ],
25.                    [
26.                      -122.48519897460936,
27.                      47.5626274374099
28.                    ],
29.                    [
30.                      -122.28744506835938,
31.                      47.44852243794931
32.                    ],
33.                    [
34.                      -122.15972900390624,
35.                      47.558920607496525
36.                    ],
37.                    [
38.                      -122.2283935546875,
39.                      47.719001413201916
40.                    ],
41.                    [
42.                      -122.35610961914062,
43.                      47.70514099299205
44.                    ]
45.                  ]
46.                ]
47.              }
48.            }
49.          }
50.        }
51.      }
52.    }
53.  }


上面的查询返回结果:



1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "geo_cities_point",
6.          "_id": "83",
7.          "_score": 1,
8.          "fields": {
9.            "name": [
10.              "Seattle"
11.            ],
12.            "objectId": [
13.              83
14.            ]
15.          }
16.        },
17.        {
18.          "_index": "geo_cities_point",
19.          "_id": "97",
20.          "_score": 1,
21.          "fields": {
22.            "name": [
23.              "Bellevue"
24.            ],
25.            "objectId": [
26.              97
27.            ]
28.          }
29.        },
30.        {
31.          "_index": "geo_cities_point",
32.          "_id": "101",
33.          "_score": 1,
34.          "fields": {
35.            "name": [
36.              "Yarrow Point"
37.            ],
38.            "objectId": [
39.              101
40.            ]
41.          }
42.        },
43.        {
44.          "_index": "geo_cities_point",
45.          "_id": "102",
46.          "_score": 1,
47.          "fields": {
48.            "name": [
49.              "Hunts Point"
50.            ],
51.            "objectId": [
52.              102
53.            ]
54.          }
55.        },
56.        {
57.          "_index": "geo_cities_point",
58.          "_id": "103",
59.          "_score": 1,
60.          "fields": {
61.            "name": [
62.              "Medina"
63.            ],
64.            "objectId": [
65.              103
66.            ]
67.          }
68.        },
69.        {
70.          "_index": "geo_cities_point",
71.          "_id": "104",
72.          "_score": 1,
73.          "fields": {
74.            "name": [
75.              "Clyde Hill"
76.            ],
77.            "objectId": [
78.              104
79.            ]
80.          }
81.        },
82.        {
83.          "_index": "geo_cities_point",
84.          "_id": "108",
85.          "_score": 1,
86.          "fields": {
87.            "name": [
88.              "Mercer Island"
89.            ],
90.            "objectId": [
91.              108
92.            ]
93.          }
94.        },
95.        {
96.          "_index": "geo_cities_point",
97.          "_id": "110",
98.          "_score": 1,
99.          "fields": {
100.            "name": [
101.              "Beaux Arts"
102.            ],
103.            "objectId": [
104.              110
105.            ]
106.          }
107.        }
108.      ]
109.    }
110.  }


地理边界框里的地理点

匹配与边界框相交的 geo_point 和 geo_shape 值。 当 GeoHashes 用于指定边界框边缘的边界时,GeoHashes 被视为矩形。 边界框的定义方式是,其左上角对应于 top_left 参数中指定的 GeoHash 的左上角,其右下角定义为 bottom_right 参数中指定的 GeoHash 的右下角。

地理点(geo_point)的精度有限,并且在索引时间内始终向下舍入。 在查询期间,边界框的上边界向下舍入,而下边界向上舍入。 因此,由于舍入误差,下边界上的点(边界框的底部和左边缘)可能无法进入边界框。 同时,查询可能会选择上边界(顶部和右边缘)旁边的点,即使它们稍微位于边缘之外。 纬度上的舍入误差应小于 4.20e-8 度,经度上的舍入误差应小于 8.39e-8 度,这意味着即使在赤道上也小于 1 厘米的误差。



1.  GET geo_cities_point/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "bool": {
5.        "must": {
6.          "match_all": {}
7.        },
8.        "filter": {
9.          "geo_bounding_box": {
10.            "location": {
11.              "top_left": {
12.                "lat": 47.7328,
13.                "lon": -122.448
14.              },
15.              "bottom_right": {
16.                "lat": 47.468,
17.                "lon": -122.0924
18.              }
19.            }
20.          }
21.        }
22.      }
23.    }
24.  }


Geo Shape 查询

所有 geo-shape 查询都需要使用 geo_shape 映射来映射你的数据。 使用 geo-shapes,我们可以找到与查询形状相交的文档。

Geo Shape Query

过滤使用 geo_shape 或 geo_point 类型索引的文档。需要 geo_shape 映射geo_point 映射

geo_shape 查询使用与 geo_shape 映射相同的网格正方形表示来查找具有与查询形状相交的形状的文档。 它还将使用为字段映射定义的相同前缀树配置。 查询支持两种定义查询形状的方法,通过提供整个形状定义或通过引用在另一个索引中预索引的形状的名称。 这两种格式都在下面通过示例进行了定义。

空间关系

geo_shape 策略映射参数确定在搜索时可以使用哪些空间关系运算符。以下是搜索地理字段时可用的空间关系运算符的完整列表:

  • INTERSECTS -(默认)返回其 geo_shape 或 geo_point 字段与查询几何相交的所有文档。
  • DISJOINT - 返回其 geo_shape 或 geo_point 字段与查询几何没有共同点的所有文档。
  • WITHIN - 返回其 geo_shape 或 geo_point 字段在查询几何内的所有文档。 不支持线几何。
  • CONTAINS - 返回其 geo_shape 或 geo_point 字段包含查询几何的所有文档。

比如:



1.  GET geo_cities_shapes/_search
2.  {
3.    "query": {
4.      "bool": {
5.        "must": {
6.          "match_all": {}
7.        },
8.        "filter": {
9.          "geo_shape": {
10.            "location": {
11.              "shape": {
12.                "type": "envelope",
13.                "coordinates": [
14.                  [
15.                    -122.35610961914062,
16.                    47.70514099299205
17.                  ],
18.                  [
19.                    -122.2283935546875,
20.                    47.01900141320191
21.                  ]
22.                ]
23.              },
24.              "relation": "disjoint"
25.            }
26.          }
27.        }
28.      }
29.    }
30.  }


预索引形状

该查询还支持使用已在另一个索引中编制索引的形状。 当你有一个预定义的形状列表并且你希望使用逻辑名称(例如 New Zealand)而不是每次都提供坐标来引用该列表时,这特别有用。 在这种情况下,只需提供:

  • id - 包含预索引形状的文档的 ID。
  • index - 预索引形状所在的索引的名称。 默认为 shapes。
  • path - 该字段被指定为包含预索引形状的路径。 默认为 shap。
  • routing - 形状文档的路由(如果需要)。


1.  PUT shapes
2.  {
3.    "mappings": {
4.      "properties": {
5.        "geometry": {
6.          "type": "geo_shape"
7.        }
8.      }
9.    }
10.  }




1.  PUT shapes/_doc/test
2.  {
3.    "location": {
4.      "type": "envelope",
5.      "coordinates": [
6.        [
7.          -122.35610961914062,
8.          47.70514099299205
9.        ],
10.        [
11.          -122.2283935546875,
12.          47.01900141320191
13.        ]
14.      ]
15.    }
16.  }


我们可以做如下的搜索:



1.  GET geo_cities_shapes/_search
2.  {
3.    "query": {
4.      "bool": {
5.        "filter": {
6.          "geo_shape": {
7.            "location": {
8.              "indexed_shape": {
9.                "index": "shapes",
10.                "id": "test",
11.                "path": "location"
12.              }
13.            }
14.          }
15.        }
16.      }
17.    }
18.  }


在上面,我们的 shape 信息是从 shapes 这个索引里得到的。

更多阅读: