ElasticSearch 实现地理位置搜索
最近,实习时涉及到了在地图上显示客户锚点的需求,想到 ES 有这么个功能可以用,便想来试试。但网上的教程太少了,我自己也是琢磨半点才看懂的,在此分享一下。
创建映射和文档
ES 的地理位置类型分为 geo_point
和 geo_shape
两种。前者表示一个地图上的点,即坐标;后者则表示多个点框出的一篇区域。功能上讲,后者的功能更强。
我们先创建一个带有这两种类型的索引映射:
{
"properties": {
"location": {
"type": "geo_point"
},
"area": {
"type": "geo_shape"
}
}
}
geo_point
对于 geo_point 类型插入文档很简单,有三种方法,如下:
"location": "34.247232,108.945872" // 第一种,直接插入该格式的字符串
"location": [108.945872, 34.247232] // 第二种,可以用数组 [lon, lat] 的形式表示
"location": { // 第三种,以对象形式插入
"lat": 34.247232,
"lon": 108.945872
}
geo_shape
对于 geo_shape 类型插入文档较复杂,因为它有很多子类型,如,point
,circle
,envelope
,linestring
,polygon
,multipoint
,multilinestring
,multipolygon
等。
下面就介绍几种常用的类型:
"area": {
"type": "point", // 点
"coordinates": [108.945872, 34.247232]
}
"area": {
"type": "circle", // 圆
"radius": "10km",
"coordinates": [-74.0059, 40.7128]
}
"area": {
"type": "envelope", // 矩形
"coordinates" : [
[108.945872, 34.247232],
[108.374854, 30.809156]
]
}
"area": {
"type": "linestring", // 线,至少两个点
"coordinates": [
[108.945872, 34.247232],
[108.374854, 30.809156],
[108.378368, 30.809938]
]
}
"area": {
"type": "polygon", // 封闭多边形,其首点和末点必须匹配,最少需要 4 个顶点
"coordinates": [
[ // 第一个多边形,作为主体
[-77.03653, 38.897676],
[-77.03653, 37.897676],
[-76.03653, 38.897676],
[-77.03653, 38.997676],
[-77.03653, 38.897676]
]
// 若存在第二个及以后的多边形,则作为主体中的“洞”,排除主体中不需要包含的面积
]
}
其余的 multi 类型就是在外围多加一个中括号即可。
地理位置搜索
geo_point 的查询方式与 geo_shape 不同,两者常用的查询方式有半径,矩形和多边形查询。但 geo_shape 查询可以兼容 geo_point 类型,而且 geo_shape 不仅可以搜索选定区域的点,还可以搜索区域,查询的空间关系如下:
- INTERSECTS -(默认)返回其 geo_shape 或 geo_point 字段与查询几何相交的所有文档。
- DISJOINT - 返回其 geo_shape 或 geo_point 字段与查询几何没有共同点的所有文档。
- WITHIN - 返回其 geo_shape 或 geo_point 字段在查询几何内的所有文档。 不支持线几何。
- CONTAINS - 返回其 geo_shape 或 geo_point 字段包含查询几何的所有文档。
半径搜索
geo_point 的半径搜索就是在地图上标定一个中心点,再标出半径,查询在这个圆内的坐标点。
{
"query": {
"geo_distance": {
"distance": "500km", // 半径,可以附带单位
"location": { // 中心点,此处使用的是第三种写法
"lat": "38.993443",
"lon": "117.158558"
}
}
}
}
geo_shape 也是类似,不过它跟插入文档时的格式一样。
{
"query": {
"geo_shape": {
"location": {
"shape": {
"type": "circle",
"radius": "10km",
"coordinates": [-74.0059, 40.7128]
}
}
}
}
}
矩形搜索
geo_point 的矩形搜索只要给出左上角和右下角两个坐标即可。
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 47.7328,
"lon": -122.448
},
"bottom_right": {
"lat": 47.468,
"lon": -122.0924
}
}
}
}
}
geo_shape 也是类似,不过它跟插入文档时的格式一样。
{
"query": {
"geo_shape": {
"location": {
"shape": {
"type": "envelope", // 矩形
"coordinates" : [
[108.945872, 34.247232],
[108.374854, 30.809156]
]
}
}
}
}
}
多边形搜索
geo_point 的多边形搜索需要给出组成多边形的所有边界点。
{
"query": {
"geo_polygon": {
"location": {
"points" : [
{"lat" : 40, "lon" : -70},
{"lat" : 30, "lon" : -80},
{"lat" : 20, "lon" : -90}
]
}
}
}
}
注意:geo_point 的多边形,其首点和末点是无需匹配的,而 geo_shape 的必须要匹配。
geo_shape 也是类似,不过它跟插入文档时的格式一样。
{
"query": {
"geo_shape": {
"location": {
"shape": {
"type": "polygon",
"coordinates": [
[
[-77.03653, 38.897676],
[-77.03653, 37.897676],
[-76.03653, 38.897676],
[-77.03653, 38.997676],
[-77.03653, 38.897676]
]
]
}
}
}
}
}
Java API 实现地理位置搜索
ElasticSearch 提供了一套 API 给 Java 用于操作,需要引入下面的依赖:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.15.2</version>
</dependency>
因为业务场景中,ES 中的数据大多是从其他数据源同步过来的,而非用 Java 手动创建,所以下面仅介绍如何实现地理位置搜索。
首先,搭建好框架,便于测试:
@SuppressWarnings("deprecation")
public class ESTest_Doc_Geo_Query {
public static final double[][][][] coordinates = {
{
{
{ 116.53, 39.67 },
{ 117.05, 39.67 },
{ 116.39, 39.42 },
{ 117.48, 39.16 },
{ 116.53, 39.67 }
}
},
{
{
{ 116.53, 39.67 },
{ 117.05, 39.67 },
{ 116.39, 39.42 },
{ 117.48, 39.16 },
{ 116.53, 39.67 }
}
}
};
public static void main(String[] args) {
RestClientBuilder builder = RestClient.builder(new HttpHost("127.0.0.1", 9200, "http"));
try (RestHighLevelClient client = new RestHighLevelClient(builder)) {
// 接下来,只需要调用不同的方法就行实现不同的搜索
QueryBuilder geoShapeQuery = geoShapePolygonQuery(coordinates);
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.matchAllQuery())
.filter(geoShapeQuery);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery);
SearchRequest request = new SearchRequest().indices("geo").source(searchSourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
System.out.println(response.getTook());
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
因为半径搜索和多边形搜索更常用,所以就不介绍矩形搜索了,感兴趣自行搜索。
半径搜索
我们创建两个方法 geoPointCircleQuery
,geoShapeCircleQuery
来代表两种类型的搜索:
public static GeoDistanceQueryBuilder geoPointCircleQuery(String name, double lon, double lat, String distance) {
return QueryBuilders.geoDistanceQuery(name).distance(distance).point(lat, lon);
}
public static GeoShapeQueryBuilder geoShapeCircleQuery(String name, double lon, double lat, double radius) throws IOException {
return QueryBuilders.geoIntersectionQuery(name, new Circle(lon, lat, radius * 1000));
}
geo_point 的每种搜索都会有一个专门的 Builder 类,而 geo_shape 只有一种。
geoIntersectionQuery
等价于 使用 builder.relation(ShapeRelation.INTERSECTS)
设置空间关系为相交的 geoShapeQuery
。同理,其余关系也有专门的查询类。当然,你也可以选择手动设置。
多边形搜索
同上,还是封装两个方法实现:
public static GeoPolygonQueryBuilder geoPointPolygonQuery(String name, double[][] points) {
List<GeoPoint> geoPoints = Arrays.stream(points).map(point -> new GeoPoint(point[1], point[0]))
.collect(Collectors.toList());
return QueryBuilders.geoPolygonQuery(name, geoPoints);
}
public static GeoShapeQueryBuilder geoShapePolygonQuery(String name, double[][] points) throws IOException {
double[] lat = Arrays.stream(points).mapToDouble(point -> point[1]).toArray();
double[] lon = Arrays.stream(points).mapToDouble(point -> point[0]).toArray();
return QueryBuilders.geoIntersectionQuery(name, new Polygon(new LinearRing(lon, lat)));
}
LinearRing
代表一个闭合的线,仅作为创建 Polygon
的边界,不能直接应用于搜索。
p.s. Polygon
还提供了 Polygon(LinearRing polygon, List<LinearRing> holes)
的构造方法来创建具有“洞”的多边形。