【ElasticSearch】ES Java API 实现地理位置搜索


前言

最近,实习时涉及到了在地图上显示客户锚点的需求,想到 ES 有这么个功能可以用,便想来试试。但网上的教程太少了,我自己也是琢磨半点才看懂的,在此分享一下。


创建映射和文档

ES 的地理位置类型分为 geo_pointgeo_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 类型插入文档较复杂,因为它有很多子类型,如,pointcircleenvelopelinestringpolygonmultipointmultilinestringmultipolygon 等。

下面就介绍几种常用的类型:

"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();
        }
    }

}

因为半径搜索和多边形搜索更常用,所以就不介绍矩形搜索了,感兴趣自行搜索。

半径搜索

我们创建两个方法 geoPointCircleQuerygeoShapeCircleQuery 来代表两种类型的搜索:

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) 的构造方法来创建具有“洞”的多边形。

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值