地理形状

地理形状( Geo-shapes )使用一种与地理坐标点完全不同的方法。我们在计算机屏幕上看到的圆形并不是由完美的连续的线组成的。而是用一个个连续的着色像素点画出的一个近似圆。地理形状的工作方式就与此相似。

复杂的形状——比如点集、线、多边形、多多边形、中空多边形——都是通过 geohash 单元 ``画出来'' 的,这些形状会转化为一个被它所覆盖到的 geohash 的集合。

Note

实际上,两种类型的网格可以被用于 geo-shapes:默认使用我们之前讨论过的 geohash ,另外还有一种是 四叉树 。四叉树与 geohash 类似,除了四叉树每个层级只有 4 个单元,而不是 32 。这种不同取决于编码方式的选择。

组成一个形状的 geohash 都作为一个单元被索引在一起。有了这些信息,通过查看是否有相同的 geohash 单元,就可以很轻易地检查两个形状是否有交集。

geo-shapes 有以下作用:判断查询的形状与索引的形状的关系;这些 关系 可能是以下之一:

intersects

查询的形状与索引的形状有重叠(默认)。

disjoint

查询的形状与索引的形状完全 重叠。

within

索引的形状完全被包含在查询的形状中。

Geo-shapes 不能用于计算距离、排序、打分以及聚合。

映射地理形状

geo_point 类型的字段相似, 地理形状也必须在使用前明确映射:

PUT /attractions
{
  "mappings": {
    "landmark": {
      "properties": {
        "name": {
          "type": "string"
        },
        "location": {
          "type": "geo_shape"
        }
      }
    }
  }
}

你需要考虑修改两个非常重要的设置: 精度距离误差

精度

精度precision )参数 用来控制生成的 geohash 的最大长度。默认精度为 9 ,等同于尺寸在 5m x 5m 的geohash 。这个精度可能比你需要的精确得多。

精度越低,需要索引的单元就越少,检索时也会更快。当然,精度越低,地理形状的准确性就越差。你需要考虑自己的地理形状所需要的精度——即使减少1-2个等级的精度也能带来明显的消耗缩减收益。

你可以使用距离来指定精度 —— 如 50m2km—不过这些距离最终也会转换成对应的Geohashes等级。

距离误差

当索引一个多边形时,中间连续区域很容易用一个短 geohash 来表示。麻烦的是边缘部分,这些地方需要使用更精细的 geohashes 才能表示。

当你在索引一个小地标时,你会希望它的边界比较精确。让这些纪念碑一个叠着一个可不好。当索引整个国家时,你就不需要这么高的精度了。误差个50米左右也不可能引发战争。

距离误差 指定地理形状可以接受的最大错误率。它的默认值是 0.025 , 即 2.5% 。也就是说,大的地理形状(比如国家)相比小的地理形状(比如纪念碑)来说,容许更加模糊的边界。

0.025 是一个不错的初始值。不过如果我们容许更大的错误率,对应地理形状需要索引的单元就越少。

索引地理形状

地理形状通过 GeoJSON 来表示,这是一种开放的使用 JSON 实现的二维形状编码方式。 每个形状都包含了形状类型— point, line, polygon, envelope —和一个或多个经纬度点集合的数组。

Caution
在 GeoJSON 里,经纬度表示方式通常是 纬度 在前, 经度 在后。

如,我们用一个多边形来索引阿姆斯特丹达姆广场:

PUT /attractions/landmark/dam_square
{
    "name" : "Dam Square, Amsterdam",
    "location" : {
        "type" : "polygon", (1)
        "coordinates" : [[ (2)
          [ 4.89218, 52.37356 ],
          [ 4.89205, 52.37276 ],
          [ 4.89301, 52.37274 ],
          [ 4.89392, 52.37250 ],
          [ 4.89431, 52.37287 ],
          [ 4.89331, 52.37346 ],
          [ 4.89305, 52.37326 ],
          [ 4.89218, 52.37356 ]
        ]]
    }
}
  1. type 参数指明了经纬度坐标集表示的形状类型。

  2. lon/lat 列表描述了多边形的形状。

上例中大量的方括号可能看起来让人困惑,不过实际上 GeoJSON 的语法非常简单:

  1. 用一个数组表示 经纬度 坐标点:

    [lon,lat]
  2. 一组坐标点放到一个数组来表示一个多边形:

    [[lon,lat],[lon,lat], ... ]
  3. 一个多边形( polygon )形状可以包含多个多边形;第一个表示多边形的外轮廓,后续的多边形表示第一个多边形内部的空洞:

    [
      [[lon,lat],[lon,lat], ... ],  # main polygon
      [[lon,lat],[lon,lat], ... ],  # hole in main polygon
      ...
    ]

参见 Geo-shape mapping documentation 了解更多支持的形状。

查询地理形状

geo_shape 查询不寻常的地方在于,它允许我们使用形状来做查询,而不仅仅是坐标点。

举个例子,当我们的用户刚刚迈出阿姆斯特丹中央火车站时,我们可以用如下方式,查询出方圆 1km 内的所有地标:

GET /attractions/landmark/_search
{
  "query": {
    "geo_shape": {
      "location": { (1)
        "shape": { (2)
          "type":   "circle", (3)
          "radius": "1km",
          "coordinates": [ (4)
            4.89994,
            52.37815
          ]
        }
      }
    }
  }
}
  1. 查询使用 location 字段中的地理形状。

  2. 查询中的形状是由 shape 键对应的内容表示。

  3. 形状是一个半径为 1km 的圆形。

  4. 安姆斯特丹中央火车站入口的坐标点。

默认的,查询(或者过滤器 —— 工作方式相同)会从已索引的形状中寻找与指定形状有交集的部分。此外,可以把 relation 字段设置为 disjoint 来查找与指定形状不相交的部分,或者设置为 within 来查找完全落在查询形状中的。

举个例子,我们可以查找阿姆斯特丹中心区域所有的地标:

GET /attractions/landmark/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "relation": "within", (1)
        "shape": {
          "type": "polygon",
          "coordinates": [[ (2)
              [4.88330,52.38617],
              [4.87463,52.37254],
              [4.87875,52.36369],
              [4.88939,52.35850],
              [4.89840,52.35755],
              [4.91909,52.36217],
              [4.92656,52.36594],
              [4.93368,52.36615],
              [4.93342,52.37275],
              [4.92690,52.37632],
              [4.88330,52.38617]
            ]]
        }
      }
    }
  }
}
  1. 只匹配完全落在查询形状中的已索引的形状。

  2. 这个多边形表示安姆斯特丹中心区域。

在查询中使用已索引的形状

对于那些经常会在查询中使用的形状,可以把它们索引起来以便在查询中可以方便地直接引用名字。以之前的阿姆斯特丹中部为例,我们可以把它存储成一个类型为 neighborhood 的文档。

首先,我们仿照之前设置 landmark 时的方式建立映射:

PUT /attractions/_mapping/neighborhood
{
  "properties": {
    "name": {
      "type": "string"
    },
    "location": {
      "type": "geo_shape"
    }
  }
}

然后我们索引阿姆斯特丹中部对应的形状:

PUT /attractions/neighborhood/central_amsterdam
{
  "name" : "Central Amsterdam",
  "location" : {
      "type" : "polygon",
      "coordinates" : [[
        [4.88330,52.38617],
        [4.87463,52.37254],
        [4.87875,52.36369],
        [4.88939,52.35850],
        [4.89840,52.35755],
        [4.91909,52.36217],
        [4.92656,52.36594],
        [4.93368,52.36615],
        [4.93342,52.37275],
        [4.92690,52.37632],
        [4.88330,52.38617]
      ]]
  }
}

形状索引好之后,我们就可以在查询中通过 indextypeid 来引用它了:

GET /attractions/landmark/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "relation": "within",
        "indexed_shape": { (1)
          "index": "attractions",
          "type":  "neighborhood",
          "id":    "central_amsterdam",
          "path":  "location"
        }
      }
    }
  }
}
  1. 指定 indexed_shape 而不是 shape ,Elasticesearch 就知道需要从指定的文档和 path 检索出对应的形状了。

阿姆斯特丹中部这个形状没有什么特别的。同样地,我们也可以在查询中使用已经索引好的达姆广场。这个查询可以找出与达姆广场有交集的临近点:

GET /attractions/neighborhood/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "indexed_shape": {
          "index": "attractions",
          "type":  "landmark",
          "id":    "dam_square",
          "path":  "location"
        }
      }
    }
  }
}

书籍推荐