地理位置聚合

虽然地理位置过滤或评分功能很有用,不过更有用得是将信息再地图上呈现给用户。 检索的结果集可能很多而不能将每个点都一一呈现,这时候就可以使用地理位置聚合来把这些位置点分布到更加可控的桶(buckets)里。

有三种聚合器可以作用于 geo_point 类型的字段:

  • geo_distance 将文档按以指定中心点为圆心的圆环分组
  • geohash_grid 将文档按 geohash 单元分组,以便在地图上呈现
  • geo_bounds 返回包含一系列矩形框的经纬坐标对,这些矩形框包含了所有的坐标点。 这种方式对于要在地图上选择一个合适的缩放等级(zoom level)时很实用。

按距离聚合

按距离聚合对于类似“找出距我1公里内的所有pizza店”这样的检索场景很适合。 检索结果需要确实地只返回距离用户1km内的文档,不过我们可以再加上一个“1-2km内的结果集”:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "query": {
        "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> 矩形框过滤器让结果集缩小到纽约区域。
  • <3> 距离聚合器计算距用户1km和1km-2km的结果数。
  • <4> 最后,后置过滤器(post_filter)再把结果缩小到距离用户1km的饭店。

上例请求的返回结果如下:

"hits": {
  "total":     1,
  "max_score": 0.15342641,
  "hits": [ <1>
     {
        "_index": "attractions",
        "_type":  "restaurant",
        "_id":    "3",
        "_score": 0.15342641,
        "_source": {
           "name": "Mini Munchies Pizza",
           "location": [
              -73.983,
              40.719
           ]
        }
     }
  ]
},
"aggregations": {
  "per_ring": { <2>
     "buckets": [
        {
           "key":       "*-1.0",
           "from":      0,
           "to":        1,
           "doc_count": 1
        },
        {
           "key":       "1.0-2.0",
           "from":      1,
           "to":        2,
           "doc_count": 1
        }
     ]
  }
}
  • <1> 后置过滤器(post_filter)已经结果集缩小到满足“距离用户1km”条件下的唯一一个pizza店。
  • <2> 聚合器包含了"距离用户2km"的pizza店的检索结果。

这个例子中,我们统计了落到各个环形区域中的饭店数。 当然,我们也可以使用子聚合器再在每个环形区域中进一步计算它们的平均价格,最流行,等等。

geohash单元聚合器

一个查询返回的结果集中可能包含很多的点,以至于不能在地图上全部单独显示。 geohash单元聚合器可以按照你指定的精度计算每个点的geohash并将相邻的点聚合到一起。

返回结果是一个个单元格,每个单元格对应一个可以在地图上展示的 geohash。 通过改变 geohash 的精度,你可以统计全球、某个国家,或者一个城市级别的综述信息。

聚合结果是稀疏(sparse )的,因为它只返回包含了文档集合的单元。 如果你的geohash精度太细,导致生成了太多的结果集,它默认只会返回包含结果最多的10000个单元 -- 它们包含了大部分文档集合。 然后,为了找出这排在前10000的单元,它还是需要先生成所有的结果集。 你可以通过如下方式控制生成的单元的数目:

  • 使用一个矩形过滤器来限制结果集。
  • 对应该矩形,选择一个合适的精度。
GET /attractions/restaurant/_search?search_type=count
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "location": { <1>
            "top_left": {
              "lat":  40,8,
              "lon": -74.1
            },
            "bottom_right": {
              "lat":  40.4,
              "lon": -73.7
            }
          }
        }
      }
    }
  },
  "aggs": {
    "new_york": {
      "geohash_grid": { <2>
        "field":     "location",
        "precision": 5
      }
    }
  }
}
  • <1> 矩形框将检索限制在纽约区域。
  • <2> 使用精度为 5 的geohash,精度大约是 5km x 5km.

每个精度为 5 的 geohash 覆盖约 25平方公里,那 10000 个单元就能覆盖 25万平方公里。 我们指定的矩形框覆盖面积约 44km * 33km,也就是大概 1452平方公里。 所以这肯定在一个安全的限度内,我们不会因此浪费大量内存来生成太多单元。

上例请求的返回如下:

...
"aggregations": {
  "new_york": {
     "buckets": [ <1>
        {
           "key": "dr5rs",
           "doc_count": 2
        },
        {
           "key": "dr5re",
           "doc_count": 1
        }
     ]
  }
}
...
  • <1> 每个单元以一个 geohash 作为 key

Again, we didn't specify any subaggregations, so all we got back was the document count. We could have asked for popular restaurant types, average price, or other details. 同样的,我们没有指定子聚合器,所以我们的返回结果是文档数目。 我们也可以(指定子聚合器来)得到流行的饭店类型,平均价格,或者其它详细信息。

提示

为了将这些单元放置在地图上展示,我们需要一个类库来将geohash解析为对于的矩形框或者中心点。 JavaScript和一些语言中有现成的类库,不过你也可以根据 geo-bounds-agg 的信息自己来实现。

范围(边界)聚合器

在geohash聚合器的例子中,我们使用了一个矩形框过滤器来将结果限制在纽约区域。 然而,我们的结果都分布在曼哈顿。 当在地图上呈现给用户时,合理的方式是可以缩放到有数据的区域;地图上有大量空白区域是没有任何点分布的。

范围过滤器是这么做得: 它计算出一个个小矩形框来覆盖到所有的坐标点。

GET /attractions/restaurant/_search?search_type=count
{
  "query": {
    "filtered": {
      "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
      }
    },
    "map_zoom": { <1>
      "geo_bounds": {
        "field":     "location"
      }
    }
  }
}
  • <1> 范围聚合器会计算出一个最小的矩形框来覆盖查询结果的所有文档。

返回结果包含了一个可以用来在地图上缩放的矩形框:

...
"aggregations": {
  "map_zoom": {
     "bounds": {
        "top_left": {
           "lat":  40.722,
           "lon": -74.011
        },
        "bottom_right": {
           "lat":  40.715,
           "lon": -73.983
        }
     }
  },
...

实际上,我们可以把矩形聚合器放到每一个 geohash 单元里,因为有坐标点的单元只占了所有单元的一部分:

GET /attractions/restaurant/_search?search_type=count
{
  "query": {
    "filtered": {
      "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": { <1>
          "geo_bounds": {
            "field": "location"
          }
        }
      }
    }
  }
}
  • <2> 子聚合器 cell_bounds 会作用于每个 geohash 单元。

现在落在每个geohash单元中的点都有了一个所在的矩形框区域:

...
"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
                 }
              }
           }
        },
...
下一节:Geohashes 是一种将 经纬度坐标对(lat/lon)编码成字符串的方式。 最开始这么做只是为了让地理位置在url上呈现的形式更加友好, 不过现在geohash已经变成一种在数据库中有效索引地理坐标点和地理形状的方式。