elasticsearch 地理信息处理(java)
移动对象数据库和地理信息数据库有不少,它们在处理地信方面能力强大。比如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);
