地理坐标点

地理坐标点 是指地球表面可以用经纬度描述的一个点。 地理坐标点可以用来计算两个坐标间的距离,还可以判断一个坐标是否在一个区域中,或在聚合中。

地理坐标点不能被动态映射(dynamic mapping)自动检测,而是需要显式声明对应字段类型为 geo-point

PUT /attractions
{
  "mappings": {
    "restaurant": {
      "properties": {
        "name": {
          "type": "string"
        },
        "location": {
          "type": "geo_point"
        }
      }
    }
  }
}

经纬度坐标格式

如上例,location 字段被声明为 geo_point 后,我们就可以索引包含了经纬度信息的文档了。经纬度信息的形式可以是字符串、数组或者对象:

PUT /attractions/restaurant/1
{
  "name":     "Chipotle Mexican Grill",
  "location": "40.715, -74.011" (1)
}

PUT /attractions/restaurant/2
{
  "name":     "Pala Pizza",
  "location": { (2)
    "lat":     40.722,
    "lon":    -73.989
  }
}

PUT /attractions/restaurant/3
{
  "name":     "Mini Munchies Pizza",
  "location": [ -73.983, 40.719 ] (3)
}
  1. 字符串形式以半角逗号分割,如 "lat,lon"

  2. 对象形式显式命名为 latlon

  3. 数组形式表示为 [lon,lat]

Caution

可能所有人都至少一次踩过这个坑:地理坐标点用字符串形式表示时是纬度在前,经度在后( "latitude,longitude" ),而数组形式表示时是经度在前,纬度在后( [longitude,latitude] )—顺序刚好相反。

其实,在 Elasticesearch 内部,不管字符串形式还是数组形式,都是经度在前,纬度在后。不过早期为了适配 GeoJSON 的格式规范,调整了数组形式的表示方式。

因此,在使用地理位置的路上就出现了这么一个“捕熊器”,专坑那些不了解这个陷阱的使用者。

通过地理坐标点过滤

有四种地理坐标点相关的过滤器可以用来选中或者排除文档:

geo_bounding_box

找出落在指定矩形框中的点。

geo_distance

找出与指定位置在给定距离内的点。

geo_distance_range

找出与指定点距离在给定最小距离和最大距离之间的点。

geo_polygon

找出落在多边形中的点。 这个过滤器使用代价很大 。当你觉得自己需要使用它,最好先看看 geo-shapes

这些过滤器判断点是否落在指定区域时的计算方法稍有不同,但过程类似。指定的区域被转换成一系列以quad/geohash为前缀的tokens,并被用来在倒排索引中搜索拥有相同tokens的文档。

Tip

地理坐标过滤器使用代价昂贵 — 所以最好在文档集合尽可能少的场景下使用。你可以先使用那些简单快捷的过滤器,比如 termrange ,来过滤掉尽可能多的文档,最后才交给地理坐标过滤器处理。

布尔型过滤器 bool filter 会自动帮你做这件事。它会优先让那些基于“bitset”的简单过滤器(见 关于缓存 )来过滤掉尽可能多的文档,然后依次才是更昂贵的地理坐标过滤器或者脚本类的过滤器。

地理坐标盒模型过滤器

这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。 你指定一个矩形的 顶部 , 底部 , 左边界 ,和 右边界 ,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "location": { (1)
            "top_left": {
              "lat":  40.8,
              "lon": -74.0
            },
            "bottom_right": {
              "lat":  40.7,
              "lon": -73.0
            }
          }
        }
      }
    }
  }
}
  1. 这些坐标也可以用 bottom_lefttop_right 来表示。

优化盒模型

地理坐标盒模型过滤器 不需要把所有坐标点都加载到内存里。 因为它要做的 只是简单判断 latlon 坐标数值是否在给定的范围内,可以用倒排索引做一个 range 过滤来实现目标。

要使用这种优化方式,需要把 geo_point 字段 用 latlon 的方式分别映射到索引中:

PUT /attractions
{
  "mappings": {
    "restaurant": {
      "properties": {
        "name": {
          "type": "string"
        },
        "location": {
          "type":    "geo_point",
          "lat_lon": true (1)
        }
      }
    }
  }
}
  1. location.latlocation.lon 字段将被分别索引。它们可以被用于检索,但是不会在检索结果中返回。

然后,查询时你需要告诉 Elasticesearch 使用已索引的 latlon

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "type":    "indexed", (1)
          "location": {
            "top_left": {
              "lat":  40.8,
              "lon": -74.0
            },
            "bottom_right": {
              "lat":  40.7,
              "lon":  -73.0
            }
          }
        }
      }
    }
  }
}
  1. 设置 type 参数为 indexed (替代默认值 memory )来明确告诉 Elasticsearch 对这个过滤器使用倒排索引。

Caution
geo_point 类型的字段可以包含多个地理坐标点,但是针对经度纬度分别索引的这种优化方式只对包含单个坐标点的字段有效。

地理距离过滤器

地理距离过滤器( geo_distance )以给定位置为圆心画一个圆,来找出那些地理坐标落在其中的文档:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance": {
          "distance": "1km", (1)
          "location": { (2)
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}
  1. 找出所有与指定点距离在 1km 内的 location 字段。访问 Distance Units 查看所支持的距离表示单位。

  2. 中心点可以表示为字符串,数组或者(如示例中的)对象。详见 经纬度坐标格式

地理距离过滤器计算代价昂贵。为了优化性能,Elasticsearch 先画一个矩形框来围住整个圆形,这样就可以先用消耗较少的盒模型计算方式来排除掉尽可能多的文档。 然后只对落在盒模型内的这部分点用地理距离计算方式处理。

Tip
你需要判断你的用户,是否需要如此精确的使用圆模型来做距离过滤?通常使用矩形模型 bounding box 是比地理距离更高效的方式,并且往往也能满足应用需求。

更快的地理距离计算

两点间的距离计算,有多种牺牲性能换取精度的算法:

arc

最慢但最精确的是 arc 计算方式,这种方式把世界当作球体来处理。不过这种方式的精度有限,因为这个世界并不是完全的球体。

plane

plane 计算方式把地球当成是平坦的,这种方式快一些但是精度略逊。在赤道附近的位置精度最好,而靠近两极则变差。

sloppy_arc

如此命名,是因为它使用了 Lucene 的 SloppyMath 类。这是一种用精度换取速度的计算方式, 它使用 Haversine formula 来计算距离。它比 arc 计算方式快 4 到 5 倍,并且距离精度达 99.9%。这也是默认的计算方式。

你可以参考下例来指定不同的计算方式:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance": {
          "distance":      "1km",
          "distance_type": "plane", (1)
          "location": {
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}
  1. 使用更快但精度稍差的 plane 计算方法。

Tip
你的用户真的会在意一个餐馆落在指定圆形区域数米之外吗?一些地理位置相关的应用会有较高的精度要求;但大部分实际应用场景中,使用精度较低但响应更快的计算方式可能更好。

地理距离区间过滤器

geo_distancegeo_distance_range 过滤器的唯一差别在于后者是一个环状的,它会排除掉落在内圈中的那部分文档。

指定到中心点的距离也可以换一种表示方式:指定一个最小距离(使用 gt 或者 gte )和最大距离(使用 ltlte ),就像使用 range 过滤器一样:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance_range": {
          "gte":    "1km", (1)
          "lt":     "2km", (1)
          "location": {
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}
  1. 匹配那些距离中心点大于等于 1km 而小于 2km 的位置。

按距离排序

检索结果可以按与指定点的距离排序:

Tip
当你 可以 按距离排序时, 按距离打分 通常是一个更好的解决方案。
GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "type":       "indexed",
          "location": {
            "top_left": {
              "lat":  40.8,
              "lon": -74.0
            },
            "bottom_right": {
              "lat":  40.4,
              "lon": -73.0
            }
          }
        }
      }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { (1)
          "lat":  40.715,
          "lon": -73.998
        },
        "order":         "asc",
        "unit":          "km", (2)
        "distance_type": "plane" (3)
      }
    }
  ]
}
  1. 计算每个文档中 location 字段与指定的 lat/lon 点间的距离。

  2. 将距离以 km 为单位写入到每个返回结果的 sort 键中。

  3. 使用快速但精度略差的 plane 计算方式。

你可能想问:为什么要制定距离的 单位 呢?用于排序的话,我们并不关心比较距离的尺度是英里、公里还是光年。 原因是,这个用于排序的值会设置在每个返回结果的 sort 元素中。

...
  "hits": [
     {
        "_index": "attractions",
        "_type": "restaurant",
        "_id": "2",
        "_score": null,
        "_source": {
           "name": "New Malaysia",
           "location": {
              "lat": 40.715,
              "lon": -73.997
           }
        },
        "sort": [
           0.08425653647614346 (1)
        ]
     },
...
  1. 餐厅到我们指定的位置距离是 0.084km。

你可以通过设置 单位unit )来让返回值的形式,匹配你应用中需要的。

Tip

地理距离排序可以对多个坐标点来使用,不管(这些坐标点)是在文档中还是排序参数中。使用 sort_mode 来指定是否需要使用位置集合的 最小min最大max )或者 平均avg )距离。 如此就可以返回 ``离我的工作地和家最近的朋友'' 这样的结果了。

按距离打分

有可能距离是决定返回结果排序的唯一重要因素,不过更常见的情况是距离会和其它因素,比如全文检索匹配度、流行程度或者价格一起决定排序结果。

遇到这种场景你需要在 功能评分查询 中指定方式让我们把这些因子处理后得到一个综合分。 越近越好 中有个一个例子就是介绍地理距离影响排序得分的。

另外按距离排序还有个缺点就是性能:需要对每一个匹配到的文档都进行距离计算。而 function_score 查询,在 rescore 语句 中可以限制只对前 n 个结果进行计算。


书籍推荐