Elasticsearch 提供了 两种表示地理位置的方式:

  • 用纬度-经度表示的坐标点使用 geo_point字段类型。
  • 以 GeoJSON 格式定义的复杂地理形状,使用 geo_shape 字段类型。
三种表示经纬度的坐标格式
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 ] 
}

地理坐标点用字符串形式表示时是纬度在前,经度在后( "latitude,longitude" ),而数组形式表示时是经度在前,纬度在后( [longitude,latitude] )—顺序刚好相反。==用对象表示时不存在这个问题==

lat:纬度 (Latitude[ˈlætɪtu:d])

lon:经度 (Longitude[ˈlɑ:ndʒətu:d])

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

  • geo_bounding_box(地理坐标盒模型) 找出落在指定矩形框中的点。
  • geo_distance 找出与指定位置在给定距离内的点。
  • geo_distance_range 找出与指定点距离在给定最小距离和最大距离之间的点。
  • geo_polygon 找出落在多边形中的点。 这个过滤器使用代价很大。
地理坐标盒模型

指定一个矩形的 顶部 , 底部 , 左边界 ,和 右边界 ,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间,以此来过滤文档。

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "location": { 
            "top_left": {
              "lat":  40.8,
              "lon": -74.0
            },
            "bottom_right": {
              "lat":  40.7,
              "lon": -73.0
            }
          }
        }
      }
    }
  }
}

盒坐标也可以用 bottom_left 和 top_right 来表示。

盒模型过滤器可以优化,先把get_point字段用lat和lon分别映射到索引中(lat_lon:true),再在搜索时使用已索引的lat和lon(type:indexed)。

PUT /attractions
{
  "mappings": {
    "restaurant": {
      "properties": {
        "location": {
          "type":    "geo_point",
          "lat_lon": true 
        }
      }
    }
  }
}
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.7,
              "lon":  -73.0
            }
          }
        }
      }
    }
  }
}
地理距离过滤器
GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance": {
          "distance": "1km", 
          "location": { 
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}

位置距离查看distance units

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

距离计算有三种计算方式:arc, plane, sloppy_arc(默认值)。可以在搜索时以distance_type来表示。

距离区间
GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance_range": {
          "gte":    "1km", 
          "lt":     "2km", 
          "location": {
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}
按距离排序

检索结果可以按与指定点的距离排序 ==按距离打分 通常是一个更好的解决方案==:

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": { 
          "lat":  40.715,
          "lon": -73.998
        },
        "order":         "asc",
        "unit":          "km", 
        "distance_type": "plane" 
      }
    }
  ]
}

具体的距离值会在sort字段中以指定距离单位的数量返回,例如下面表示与指定坐标点的距离为0.084km:

{
        "_index": "attractions",
        "_type": "restaurant",
        "_id": "2",
        "_score": null,
        "_source": {
           "name": "New Malaysia",
           "location": {
              "lat": 40.715,
              "lon": -73.997
           }
        },
        "sort": [
           0.08425653647614346 
        ]
     }

按距离打分 常见的情况是距离会和其它因素,比如全文检索匹配度、流行程度或者价格一起决定排序结果。按距离排序还有个缺点就是性能:需要对每一个匹配到的文档都进行距离计算。这时需要参考 功能评分查询 、 越近越好 、 rescore语句。 ++而 function_score 查询,在 rescore 语句 中可以限制只对前 n 个结果进行计算。++

Geohashes

https://www.elastic.co/guide/cn/elasticsearch/guide/current/geohashes.html

Geohashes 把整个世界分为 32 个单元的格子 —— 4 行 8 列 —— 每一个格子都用一个字母或者数字标识。比如 g 这个单元覆盖了半个格林兰,冰岛的全部和大不列颠的大部分。每一个单元还可以进一步被分解成新的 32 个单元,这些单元又可以继续被分解成 32 个更小的单元,不断重复下去。 gc 这个单元覆盖了爱尔兰和英格兰, gcp 覆盖了伦敦的大部分和部分南英格兰, gcpuuz94k 是白金汉宫的入口,精确到约 5 米。

两个刚好相邻的位置,可能会有完全不同的 geohash 。比如,伦敦 Millenium Dome 的 geohash 是 u10hbp ,因为它落在了 u 这个单元里,而紧挨着它东边的最大的单元是 g 。

geohashes映射

PUT /attractions
{
  "mappings": {
    "restaurant": {
      "properties": {
        "name": {
          "type": "string"
        },
        "location": {
          "type":               "geo_point",
          "geohash_prefix":     true, 
          "geohash_precision":  "1km" 
        }
      }
    }
  }
}

精度(geohash_precision)可以是一个具体的数字,代表的 geohash 的长度,也可以是一个距离。 1km 的精度对应的 geohash 的长度是 7。

==这句话不是很懂:== 通过如上设置, geohash 前缀中 1 到 7 的部分将被索引,所能提供的精度大约在 150 米。

geohash查询

GET /attractions/restaurant/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "geohash_cell": {
          "location": {
            "lat":  40.718,
            "lon": -73.983
          },
          "neighbors": true, 
          "precision": "2km"
        }
      }
    }
  }
}
  • precision 字段设置的精度不能高于映射时 geohash_precision 字段指定的值。
  • 如果不加neighbors可能不会返回 2km 内所有的餐馆。因为geohash 实际上仅是个矩形,而指定的点可能位于这个矩形中的任何位置。有可能这个点刚好落在了 geohash 单元的边缘附近。

明显的, 2km 精度的 geohash 加上周围的单元,最终导致一个范围极大的搜索区域。此查询不是为精度而生,但是它非常有效率,而且可以作为更高精度的地理位置过滤器的前置过滤器

2km 的 precision 会被转换成长度为 6 的 geohash 。实际上它的尺寸是约 1.2km x 0.6km。

此查询的另一个优点是,相比 geo_bounding_box 查询,它支持一个字段中有多个坐标位置的情况

地理位置聚合
  • 地理位置距离 将文档按照距离围绕一个中心点来分组。
  • geohash 网格 将文档按照 geohash 范围来分组,用来显示在地图上。
  • 地理位置边界 返回一个包含所有地理位置坐标点的边界的经纬度坐标,这对显示地图时缩放比例的选择非常有用。

地理位置距离

GET /attractions/restaurant/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {              //1
          "name": "pizza"
        }
      },
      "filter": {
        "geo_bounding_box": {
          "location": {         //2
            "top_left": {
              "lat":  40.8,
              "lon": -74.1
            },
            "bottom_right": {
              "lat":  40.4,
              "lon": -73.7
            }
          }
        }
      }
    }
  },
  "aggs": {
    "per_ring": {
      "geo_distance": {         //3
        "field":    "location",
        "unit":     "km",
        "origin": {
          "lat":    40.712,
          "lon":   -73.988
        },
        "ranges": [
          { "from": 0, "to": 1 },
          { "from": 1, "to": 2 }
        ]
      }
    }
  },
  "post_filter": {          //4
    "geo_distance": {
      "distance":   "1km",
      "location": {
        "lat":      40.712,
        "lon":     -73.988
      }
    }
  }
}
  1. 主查询查找名称中含有 pizza 的饭店。
  2. geo_bounding_box 筛选那些只在纽约区域的结果。(++个人认为是作为更高精度的地理位置过滤器的前置过滤器++)
  3. geo_distance 聚合统计距离用户 1km 以内,1km 到 2km 的结果的数量。
  4. 最后,post_filter 将结果缩小至那些在用户 1km 范围内的饭店。
Geohash网格聚合

geohash_grid 按照你定义的精度计算每一个点的geohash值而将附近的位置聚合在一起。默认返回那些包含了大量文档、最密集的10000个单元。

结果是一个网格—一个单元格表示一个可以显示在地图上的 geohash 。通过改变 geohash 的精度,你可以按国家或者城市街区来概括全世界。

可以通过以下方式来控制 buckets 的产生数量:

  • 使用 geo_bounding_box 来限制结果。
  • 为你的边界大小选择一个适当的 precision (精度)
GET /attractions/restaurant/_search
{
  "size" : 0,
  "query": {
    "constant_score": {
      "filter": {
        "geo_bounding_box": {
          "location": { 
            "top_left": {
              "lat":  40.8,
              "lon": -74.1
            },
            "bottom_right": {
              "lat":  40.4,
              "lon": -73.7
            }
          }
        }
      }
    }
  },
  "aggs": {
    "new_york": {
      "geohash_grid": { 
        "field":     "location",
        "precision": 5
      }
    }
  }
}

Geohashes 精度为 5 ,每个约25平方公里,所以10000个单元按这个精度将覆盖250000平方公里。我们指定的边界范围,约44km x 33km,或约1452平方公里,所以我们的边界在安全范围内;我们绝对不会在内存中创建了太多的 buckets。

返回结果如下

"aggregations": {
  "new_york": {
     "buckets": [ 
        {
           "key": "dr5rs",
           "doc_count": 2
        },
        {
           "key": "dr5re",
           "doc_count": 1
        }
     ]
  }
}
地理边界聚合

geo_bounds 用来计算封装所有地理位置点需要的最小边界框。

GET /attractions/restaurant/_search
{
  "size" : 0,
  "query": {
    "constant_score": {
      "filter": {
        "geo_bounding_box": {
          "location": {
            "top_left": {
              "lat":  40,8,
              "lon": -74.1
            },
            "bottom_right": {
              "lat":  40.4,
              "lon": -73.9
            }
          }
        }
      }
    }
  },
  "aggs": {
    "new_york": {
      "geohash_grid": {
        "field":     "location",
        "precision": 5
      },
      "aggs": {
        "cell": { 
          "geo_bounds": {
            "field": "location"
          }
        }
      }
    }
  }
}

返回结果中,每一个单元里的点有一个边界框

"aggregations": {
  "new_york": {
     "buckets": [
        {
           "key": "dr5rs",
           "doc_count": 2,
           "cell": {
              "bounds": {
                 "top_left": {
                    "lat":  40.722,
                    "lon": -73.989
                 },
                 "bottom_right": {
                    "lat":  40.719,
                    "lon": -73.983
                 }
              }
           }
        },
    ...
地理形状

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

  • intersects 查询的形状与索引的形状有重叠(默认)。
  • disjoint 查询的形状与索引的形状完全 不 重叠。
  • within 索引的形状完全被包含在查询的形状中。(索引的形状可以是多个)

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

地理形状映射

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

需要考虑修改两个非常重要的设置:精度(precision) 和 距离误差。

精度默认为9, 等同于尺寸在 5m x 5m 的geohash 。

距离误差 指定地理形状可以接受的最大错误率。它的默认值是 0.025 , 即 2.5% 。

索引地理位置

PUT /attractions/landmark/dam_square
{
    "name" : "Dam Square, Amsterdam",
    "location" : {
        "type" : "polygon",     //指明了经纬度坐标集表示的形状类型
        "coordinates" : [[ 
          [ 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 ]
        ]]
    }
}

表示多边形的语法:

  • 用一个数组表示 经纬度 坐标点:[lon,lat]
  • 一组坐标点放到一个数组来表示一个多边形:[[lon,lat],[lon,lat], ... ]
  • 一个多边形( polygon)形状可以包含多个多边形;++第一个表示多边形的外轮廓++,后续的多边形表示第一个多边形内部的空洞:
[
  [[lon,lat],[lon,lat], ... ],  # main polygon
  [[lon,lat],[lon,lat], ... ],  # hole in main polygon
  ...
]

地理形状的最新的详细文档查看这里

地理形状查询两个例子

  1. 查询出方圆 1km 内的所有地标:
GET /attractions/landmark/_search
{
  "query": {
    "geo_shape": {
      "location": {         //location 字段中是地理形状
        "shape": {          //查询中的形状是由 shape 键对应的内容表示
          "type":   "circle", //形状是一个半径为 1km 的圆形
          "radius": "1km",
          "coordinates": [ 
            4.89994,
            52.37815
          ]
        }
      }
    }
  }
}
  1. 查找阿姆斯特丹中心区域所有的地标:
GET /attractions/landmark/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "relation": "within", //只匹配完全落在查询形状中的已索引的形状
        "shape": {
          "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]
            ]]
        }
      }
    }
  }
}

可以在查询中使用已索引的形状https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl-geo-shape-query.html#_pre_indexed_shape