elasticsearch 地理信息处理(java)

发表于2019-08-02,长度3850, 271个单词, 11分钟读完
Flag Counter

移动对象数据库和地理信息数据库有不少,它们在处理地信方面能力强大。比如Oracle和阿里云库。公司用的MySQL没有这种能力,需要通过业务代码来实现。不过公司使用的中间件,比如es和hbase都有地信索引和计算能力。这里简单说一下es的地理信息处理方式。

索引

不同于其他普通索引,地信索引的创建需要明确告知es。

坐标点 geo_point

其中一种地信是坐标位置,只有一个点,索引比较简单。

假设我们一个对象有经纬度信息:

    /**
     * 经度
     */
    private String longitude;

    /**
     * 维度
     */
    private String latitude;

存入对象的map已经存在:

Map<String, Object> map = JSON.parseObject(JSON.toJSONString(this), new TypeReference<Map<String, Object>>() {});

坐标字段要单独加入map:

Map<String, Object> locationMap = new HashMap<>(2, 1);
locationMap.put("lat", this.getLatitude());
locationMap.put("lon", this.getLongitude());
map.put("location", locationMap);

坐标必须有两个字段构成,而且分别是lat和lon。

当然有其他构造方式,可以去官网了解。我感觉上面这种比较简单直观

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

这里有一个一定要做的事情很容易忽略:不能依赖es自动映射,必须在保存数据前创建索引结构,且指明上面的location字段是geo_point类型。否则es不会讲其识别为地信点,也不会做特殊处理:

XContentBuilder builder = jsonBuilder()
        .startObject()
        .startObject("your_type").field("dynamic", true)
        .startObject("properties")
        .startObject("xxxx").field("type", "xx").endObject()
        .startObject("location").field("type", "geo_point").endObject()
        .endObject()
        .endObject()
        .endObject();

这样插入索引中的location就会被做为地信点索引了。

地信区域 geo_shape

相比坐标点,索引一块区域可能用得没那么多。一般的场景是有一块区域,有一堆点:判断哪些点在这块区域里。

索引区域的方式和点有点类似。先创建索引映射:

XContentBuilder builder = jsonBuilder()
        .startObject()
        .startObject("your_type").field("dynamic", true)
        .startObject("properties")
        .startObject("location").field("type", "geo_point").endObject()
        .startObject("range").field("type", "geo_shape").endObject()
        .endObject()
        .endObject()
        .endObject();

坐标点使用的是geo_point而区域使用的是形状geo_shape。

假设区域的每个拐点定义如下:

public class GeographyPoint {
    private double lng;// 经度
    private double lat;// 纬度
}

要创建一个多区域的索引(就是说某个对象有多块区域,当判断点是否在区域的时候要判断每块区域都不包含该点才失败),也是创建一个map:

Map<String, Object> polygonMap = new HashMap<>(2, 1);
polygonMap.put("type", "multilinestring");// GeoShapeType.MULTILINESTRING
polygonMap.put("coordinates", polygonList);
map.put("ranges", polygonMap);

注意到这个map也有两个元素,分别是type和coordinates:type必须是multilinestring,coordinates是坐标集合。 坐标集合是根据我们对象的区域构造的,假定多区域集合是

List<List<GeographyPoint>> lists;

则坐标集合的大小也是区域的数量:

List<List<List<BigDecimal>>> polygonList = new ArrayList<>(lists.size());
for (List<GeographyPoint> list : lists) {
    List<List<BigDecimal>> nl = new ArrayList<>(list.size());
    polygonList.add(nl);
    for (GeographyPoint point : list) {
        List<BigDecimal> nnl = new ArrayList<>(2);
        nl.add(nnl);
        nnl.add(new BigDecimal(point.getLng()));
        nnl.add(new BigDecimal(point.getLat()));
    }

}

查询

数据索引好以后,接下来查询。

假设要查询坐标点是lng,lat,查询范围1000米:

BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder()
        .must(QueryBuilders
            .geoDistanceQuery(”location“)
            .distance("1000m")
            .point(lat, lng)
        );
SearchRequestBuilder searchRequestBuilder = transportClient.prepareSearch(your_index)
        .setTypes(your_type)
        .setQuery(boolQueryBuilder);

如果需要对数据排序,可以加上addSort:

SearchRequestBuilder searchRequestBuilder = transportClient.prepareSearch(your_index)
        .setTypes(your_type)
        .setQuery(boolQueryBuilder)
        .addSort(SortBuilders.geoDistanceSort("location", lat, lng)
                .order(SortOrder.ASC)
                .point(lat, lng)
                .unit(DistanceUnit.METERS));

如果只想取排序后的top10,可以setSize:

SearchRequestBuilder searchRequestBuilder = transportClient.prepareSearch(your_index)
        .setTypes(your_type)
        .setQuery(boolQueryBuilder)
        .addSort(SortBuilders.geoDistanceSort("location", lat, lng)
                .order(SortOrder.ASC)
                .point(lat, lng)
                .unit(DistanceUnit.METERS))
                .setSize(10);
Written on August 2, 2019
分类: dev, 标签: java elasticsearch
如果你喜欢,请赞赏! davelet