前言
最近项目中有个需求是完成离我最近的功能,经过讨论决定使用ElasticSearch根据用户所在位置经纬度来完成相关需求,这里使用一个小的Demo来做记录。
准备工作
1.构建索引
创建索引location_test,索引只包含两个字段,一个景点名字另一个是景点经纬度,保存经纬度使用geo_point类型存储。
PUT http://localhost:9200/location_test/_mapping
{
"properties":{
"locationName":{
"type":"text",
"analyzer":"ik_max_word"
},
"location":{
"type":"geo_point"
}
}
}
2.插入数据
因为是Demo所以这里只插入七条上海的数据来进行演示。
POST http://localhost:9200/location_test/_doc
{
"locationName": "召稼楼古镇",
"location": {
"lat": 31.081128,
"lon": 121.555511
}
}
{
"locationName": "上海奇思妙想减压馆",
"location": {
"lat": 31.24249,
"lon": 121.490283
}
}
{
"locationName": "上海JOYPOLIS世嘉都市乐园",
"location": {
"lat": 31.239446,
"lon": 121.419129
}
}
{
"locationName": "震旦博物馆",
"location": {
"lat": 31.240324,
"lon": 121.506166
}
}
{
"locationName": "江南三民文化村",
"location": {
"lat": 31.723805,
"lon": 121.504962
}
}
{
"locationName": "海湾3D错觉艺术馆",
"location": {
"lat": 30.828349,
"lon": 121.535577
}
}
{
"locationName": "上海迪士尼度假区",
"location": {
"lat": 31.148267,
"lon": 121.671964
}
}
完成需求
接下来就是完成需求的过程了,主要是完成两块需求:获取距离并排序、根据距离进行筛选。
1.根据经纬度进行距离排序并获取距离
假设你现在正在外滩和女朋友散步(外滩经纬度:31.243453,121.497204),这时你们要找离你们最近的哪些景点可以去看看,使用ES可以很轻松的完成这一功能,ES的请求如下:
POST http://localhost:9200/location_test/_search
{
"sort": [
{
"_geo_distance": {
"location": [
{
"lat": 31.243453,
"lon": 121.497204
}
],
"unit": "m",
"order": "asc"
}
}
]
}
这里使用 _geo_distance 关键字进行距离查询,存储文档经纬度的字段是localtion,location中的值就是你们现在位置的经纬度,unit指的是查询的单位,这里是用米,也可以使用千米(km)做单位,排序方式用升序。得到查询结果如下:
{
"total":{
"value":7,
"relation":"eq"
},
"max_score":null,
"hits":[
{
"_index":"location_test",
"_type":"_doc",
"_id":"Umg6oHQB29lS14JF6ljQ",
"_score":null,
"_source":{
"locationName":"上海奇思妙想减压馆",
"location":{
"lat":31.24249,
"lon":121.490283
}
},
"sort":[
666.6306788021509
]
},
{
"_index":"location_test",
"_type":"_doc",
"_id":"W2g7oHQB29lS14JFE1jZ",
"_score":null,
"_source":{
"locationName":"震旦博物馆",
"location":{
"lat":31.240324,
"lon":121.506166
}
},
"sort":[
920.3164557098329
]
},
{
"_index":"location_test",
"_type":"_doc",
"_id":"Wmg7oHQB29lS14JFAlgP",
"_score":null,
"_source":{
"locationName":"上海JOYPOLIS世嘉都市乐园",
"location":{
"lat":31.239446,
"lon":121.419129
}
},
"sort":[
7436.0041059579125
]
},
{
"_index":"location_test",
"_type":"_doc",
"_id":"8mg1oHQB29lS14JFUFBe",
"_score":null,
"_source":{
"locationName":"召稼楼古镇",
"location":{
"lat":31.081128,
"lon":121.555511
}
},
"sort":[
18883.13042012399
]
},
{
"_index":"location_test",
"_type":"_doc",
"_id":"RGg9oHQB29lS14JF5Vpq",
"_score":null,
"_source":{
"locationName":"上海迪士尼度假区",
"location":{
"lat":31.148267,
"lon":121.671964
}
},
"sort":[
19706.20435968952
]
},
{
"_index":"location_test",
"_type":"_doc",
"_id":"bWg7oHQB29lS14JFOlgq",
"_score":null,
"_source":{
"locationName":"海湾3D错觉艺术馆",
"location":{
"lat":30.828349,
"lon":121.535577
}
},
"sort":[
46302.09271440355
]
},
{
"_index":"location_test",
"_type":"_doc",
"_id":"XWg7oHQB29lS14JFJ1hP",
"_score":null,
"_source":{
"locationName":"江南三民文化村",
"location":{
"lat":31.723805,
"lon":121.504962
}
},
"sort":[
53417.844250374095
]
}
]
}
查询结果把文档数据根据location中的经纬度进行计算排序,结果中的sort就是根据经纬度计算出来的距离,单位是米。可以看到目前离你们最近的是上海奇思妙想减压馆距离666米左右。
Java代码如下:
public void searchNearest() throws IOException {
// 待查询的经纬度
double lat = 31.243453;
double lon = 121.497204;
SearchRequest searchRequest = new SearchRequest("location_test");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 使用GeoDistanceSortBuilder构建查询条件
GeoDistanceSortBuilder distanceSortBuilder = new GeoDistanceSortBuilder("location", lat, lon);
// 设置单位为米,升序排序
distanceSortBuilder.unit(DistanceUnit.METERS).order(SortOrder.ASC);
sourceBuilder.sort(distanceSortBuilder);
SearchRequest request = searchRequest.source(sourceBuilder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHit[] hitResult = response.getHits().getHits();
// 打印输出,距离在使用getSortValues()获取
for (int i = 0; i < hitResult.length; i++) {
BigDecimal distance = new BigDecimal(hitResult[i].getSortValues()[0].toString())
.setScale(2, BigDecimal.ROUND_HALF_UP);
System.out.println(hitResult[i].getSourceAsMap().get("locationName").toString()+"据你"+ distance +"米");
}
}
使用Java代码计算经纬度主要是使用GeoDistanceSortBuilder来进行组织筛选条件,并且将组织好的GeoDistanceSortBuilder对象传入sourceBuilder.sort()中得到排序结果。上面结果打印出来的结果如下:
2.获取距离并筛选距离
在得到了周边景点距离后,你和你女朋友发现有些很远有些很近,想再筛选一下距离,比如一公里内的景点有哪些,使用ES同样可以完成这样的功能。
这里主要是在上面查询的基础上使用post_filter做后置过滤,在post_filter中使用distance关键字来决定筛选的距离。
{
"sort":[
{
"_geo_distance":{
"location":[
{
"lat":31.243453,
"lon":121.497204
}
],
"unit":"m",
"order":"asc"
}
}
],
"post_filter":{
"geo_distance":{
"distance":"1km",
"location":{
"lat":31.243453,
"lon":121.497204
}
}
}
}
查询筛选结果如下:
{
"total":{
"value":2,
"relation":"eq"
},
"max_score":null,
"hits":[
{
"_index":"location_test",
"_type":"_doc",
"_id":"Umg6oHQB29lS14JF6ljQ",
"_score":null,
"_source":{
"locationName":"上海奇思妙想减压馆",
"location":{
"lat":31.24249,
"lon":121.490283
}
},
"sort":[
666.6306788021509
]
},
{
"_index":"location_test",
"_type":"_doc",
"_id":"W2g7oHQB29lS14JFE1jZ",
"_score":null,
"_source":{
"locationName":"震旦博物馆",
"location":{
"lat":31.240324,
"lon":121.506166
}
},
"sort":[
920.3164557098329
]
}
]
}
可以看到在你们一公里以内的有两个景点可以去参观,于是你和女朋友就可以愉快的去参观了。使用Java来查询查询部分和上面大致相同,筛选使用GeoDistanceQueryBuilder构建距离筛选条件,并传入sourceBuilder的postFilter()方法中使用,相关Java代码如下:
public void searchFilter() throws IOException {
double lat = 31.243453;
double lon = 121.497204;
SearchRequest request = new SearchRequest("location_test");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 使用GeoDistanceSortBuilder构建查询条件
GeoDistanceSortBuilder distanceSortBuilder = new GeoDistanceSortBuilder("location", lat, lon);
// 设置单位为米,升序排序
distanceSortBuilder.unit(DistanceUnit.METERS).order(SortOrder.ASC);
// 构建筛选条件
GeoDistanceQueryBuilder distanceQueryBuilder = new GeoDistanceQueryBuilder("location");
distanceQueryBuilder.distance(1,DistanceUnit.KILOMETERS).point(lat,lon);
sourceBuilder.sort(distanceSortBuilder).postFilter(distanceQueryBuilder);
request.source(sourceBuilder);
// 获取结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHit[] hitResult = response.getHits().getHits();
System.out.println("一公里范围内有以下景点:");
for (int i = 0; i < hitResult.length; i++) {
BigDecimal distance = new BigDecimal(hitResult[i].getSortValues()[0].toString())
.setScale(2, BigDecimal.ROUND_HALF_UP);
System.out.println(hitResult[i].getSourceAsMap().get("locationName").toString()+",据你"+ distance +"米");
}
}
上面代码打印的结果如下:
总结
使用ElasticSearch可以很好的完成经纬度的距离计算和查询筛选,但是使用ElasticSearch计算的距离是直线距离,如果要计算路线距离计算需要使用地图相关API进行计算了,并且使用ElasticSearch的话在数据增量同步的会稍微麻烦一点。