深圳网站设计张兵,西安工程建设信息网,什么网站可以做软件有哪些内容吗,公众号制作素材一、前言
随着互联网的热门#xff0c;越来越多的传统行业将全部或者部分业务转移到互联网上#xff0c;其中不乏一些和地理位置强相关的行业。基于地理位置的搜索功能#xff0c;大大提升了人们的生活和工作效率。例如#xff0c;外出旅行时#xff0c;只需要用手机打开…一、前言
随着互联网的热门越来越多的传统行业将全部或者部分业务转移到互联网上其中不乏一些和地理位置强相关的行业。基于地理位置的搜索功能大大提升了人们的生活和工作效率。例如外出旅行时只需要用手机打开订酒店的应用软件查找附近心仪的酒店下单即可又或者打车行业人们不用在寒冷的户外拦截出租车只需要在室内打开打车APP定位到当前位置然后确定目的地系统就可以为附近的车辆派发订单。 幸运的是ES为用户提供了基于地理位置的搜索功能。它主要支持两种类型的地理查询一种是地理点(geo_point),即经纬度查询另一种是地理形状查询(geo_shape),即支持点、线、圆形和多边形查询等。 从实用性来说地理点即geo_point数据类型的使用的更多一些本节也只对地理点类型进行介绍。 对应于geo_point字段类型的查询方式有3种分别为geo_distance查询、geo_bounding_box查询和geo_polygon。 为了更方便专一的学习地理搜索我们在hotel索引批量增加多条文档内容如下
POST /hotel_location/_doc/_bulk
{index:{_id:51}}
{title:连锁酒店1,location:{lat:40.17836693398477,lon:116.64002551005981}}
{index:{_id:52}}
{title:连锁酒店2,location:{lat:40.19103839805197,lon:116.5624013764374}}
{index:{_id:53}}
{title:连锁酒店3,location:{lat:40.13933715136454,lon:116.63441990026217}}
{index:{_id:54}}
{title:连锁酒店4,location:{lat:40.14901664712196,lon:116.53067995860928}}
{index:{_id:55}}
{title:连锁酒店5,location:{lat:40.125057718315716,lon:116.62963567059545}}
{index:{_id:56}}
{title:连锁酒店6,location:{lat:40.19216257806647,lon:116.64025980109571}}
{index:{_id:57}}
{title:连锁酒店7,location:{lat:40.16371689899584,lon:116.63095084701624}}
{index:{_id:58}}
{title:连锁酒店8,location:{lat:40.146045218040605,lon:116.5696251832195}}
{index:{_id:59}}
{title:连锁酒店9,location:{lat:40.144735806234166,lon:116.60712460957835}}通过上面的步骤我们完成了9条经纬度数据的插入可以通过search语句查询一下结果
二、geo_bounding_box
geo_bounding_box语法又称为地理坐标盒模型在当前语法中只需选择一个矩阵范围(输入矩阵的左上角的顶点地理坐标和矩阵的右上角的顶点地理坐标构建成为一个矩阵)即可计算出当前矩阵中符合条件的元素
简单来说呢就是给定两个坐标通过这两个坐标形成对角线平行于地球经纬度从而得到的一个矩阵。采用geo_bounding_box语法可以得到坐落于当前矩阵中的元素的信息 假设如上图我这边给定两个坐标分别是A(116.498353,40.187328) 和 B(116.610461,40.084509)这样我们就得到了一个矩阵。 ES的geo_bounding_box语法有很多种查询方式但是需要注意的是我们要确定好哪个是左上角的坐标哪个是右下角的坐标并且这两个坐标不能互换。 那上面的例子通过A\B两点去找矩阵范围内的酒店DSL如下
GET /hotel/_search
{query: {geo_bounding_box: {location: {top_left: {lat: 40.187328,lon: 116.498353},bottom_right: {lat: 40.084509,lon: 116.610461}}}}
}输出如下可以看到有3家酒店位于这个矩阵范围中
{...hits : {total : {value : 3,relation : eq},max_score : 1.0,hits : [{_index : hotel,_type : _doc,_id : 54,_score : 1.0,_source : {title : 连锁酒店4,location : {lon : 116.53067995860928,lat : 40.14901664712196}}},{_index : hotel,_type : _doc,_id : 58,_score : 1.0,_source : {title : 连锁酒店8,location : {lon : 116.5696251832195,lat : 40.146045218040605}}},{_index : hotel,_type : _doc,_id : 59,_score : 1.0,_source : {title : 连锁酒店9,location : {lon : 116.60712460957835,lat : 40.144735806234166}}}]}
}
那么除了上面的查询的DSL语法之外还有如下语法获取的结果和上面DSL均相同
基于经纬度数组的DSL语法
需要注意的是数组形式的经纬度顺序需调换一下 这样就不需要输入lat和lon了直接通过数组表示经纬度
GET /hotel/_search
{query: {geo_bounding_box: {location: {top_left: [116.498353,40.187328],bottom_right: [116.610461,40.084509]}}}
}基于经纬度字符串的DSL语法
字符串不同于数组经纬度顺序不需要调换
GET /hotel/_search
{query: {geo_bounding_box: {location: {top_left: 40.187328,116.498353,bottom_right: 40.084509,116.610461}}}
}基于经纬度边界框WKT的DSL语法
可以看到先是A的经度再是B的经度然后是A的纬度再是B的纬度通过BBOX封装
GET /hotel/_search
{query: {geo_bounding_box: {location: {wkt: BBOX (116.498353,116.610461,40.187328,40.084509)}}}
}基于经纬度GeoHash的DSL语法
//关于GeoHash可以参考两个网址
// 全球GeoHash地图 http://geohash.gofreerange.com/
// GeoHash坐标在线转换 http://geohash.co/
GET /hotel/_search
{query: {geo_bounding_box: {location: {top_left: wx4udgz,bottom_right: wx4uj91}}}
}基于经纬度顶点属性的DSL语法 相当于把top_left和bottom_right分成了四个
GET /hotel/_search
{query: {geo_bounding_box: {location: {top: 40.187328,left: 116.498353,bottom: 40.084509,right: 116.610461}}}
}至此采用上述6种方式计算的矩阵坐落元素所执行结果均一致且逐个在地图上核实所召回的建筑均真实的在上图的矩阵中 计算某个矩阵或者是多边形中的元素在Redis中目前是不支持的在这方面ES表现的更为强大通过上述的三种语法可以看到ES可以很好的支持 矩阵、圆、多边形的空间地理检索通过查看Redis的语法可以看到Redis目前只支持圆的空间地理检索
在java客户端使用new GeoBoundingBoxQueryBuilder()构造geo_bounding_box请求我们可以看到它有很多附加的方法 但是我们这次使用的主要是setCorners()设置两点距离可以看到它支持geoHash,两点坐标经纬度顶点等查询方法。 我们使用两点坐标来进行查询 Service如下
public ListHotel geoBoundingBoxQuery(HotelDocRequest hotelDocRequest) throws IOException {//新建搜索请求String indexName getNotNullIndexName(hotelDocRequest);SearchRequest searchRequest new SearchRequest(indexName);SearchSourceBuilder searchSourceBuilder new SearchSourceBuilder();// 构造左上点坐标GeoPoint topLeft new GeoPoint(40.187328D, 116.498353D);// 构造右下点坐标GeoPoint bottomRight new GeoPoint(40.084509D, 116.610461D);GeoBoundingBoxQueryBuilder geoBoundingBoxQueryBuilder new GeoBoundingBoxQueryBuilder(location).setCorners(topLeft,bottomRight);searchSourceBuilder.query(geoBoundingBoxQueryBuilder);searchRequest.source(searchSourceBuilder);return getQueryResult(searchRequest);}这次getQueryResult()我们需要将结果能够正常返回location这个属性 首先我们建立Location这个类,lat代表纬度lon代表经度
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;Data
AllArgsConstructor
NoArgsConstructor
public class Location {private String lat;private String lon;
}然后在hotel索引类增加location属性这个属性名和你建造索引用的是一样的 然后对getQueryResult进行改造因为我们获取到location的json对象其实是类似于hashMap结构的对象我们可以使用JSONUtil.toJsonStr(location)将其先转化为json字符串然后通过JSONUtil.toBean(String jsonString,T)转化成我们的目标对象Location。 完整代码如下
private ListHotel getQueryResult(SearchRequest searchRequest) throws IOException {ArrayListHotel resultList new ArrayList();SearchResponse searchResponse client.search(searchRequest, RequestOptions.DEFAULT);RestStatus status searchResponse.status();if (status ! RestStatus.OK) {return Collections.emptyList();}SearchHits searchHits searchResponse.getHits();for (SearchHit searchHit : searchHits) {Hotel hotelResult new Hotel();hotelResult.setId(searchHit.getId()); //文档_idhotelResult.setIndex(searchHit.getIndex()); //索引名称hotelResult.setScore(searchHit.getScore()); //文档得分//转换为MapMapString, Object dataMap searchHit.getSourceAsMap();hotelResult.setTitle((String) dataMap.get(title));hotelResult.setCity((String) dataMap.get(city));Object price dataMap.get(price);if (price ! null) {hotelResult.setPrice(Double.valueOf((String) price));}//获取locationObject location dataMap.get(location);if (location ! null) {hotelResult.setLocation(JSONUtil.toBean(JSONUtil.toJsonStr(location), Location.class));}resultList.add(hotelResult);}return resultList;}然后回到controller调用 PostMapping(/query/bounding-box)public FoundationResponseListHotel geoBoundingBoxQuery(RequestBody HotelDocRequest hotelDocRequest) {try {ListHotel hotelList esQueryService.geoBoundingBoxQuery(hotelDocRequest);if (CollUtil.isNotEmpty(hotelList)) {return FoundationResponse.success(hotelList);} else {return FoundationResponse.error(100,no data);}} catch (IOException e) {log.warn(搜索发生异常原因为:{}, e.getMessage());return FoundationResponse.error(100, e.getMessage());} catch (Exception e) {log.error(服务发生异常原因为:{}, e.getMessage());return FoundationResponse.error(100, e.getMessage());}}postman调用截图
三、geo_distance
ES中的geo_distance语法与Redis中的georadius语法类似通过给定一个坐标和半径圈出圆内的点。在ES可以定义一些排序规则返回召回结果集数据与当前坐标的距离Redis中默认返回了距离 与geo_bounding_box语法类似geo_distance语法也有多种查询方式如 经纬度属性、经纬度数组、经纬度字符串、GeoHash等下面就简单的以 经纬度字符串为例进行演示重新选定坐标以纬度(116.5864,40.174697)为例查询3km范围内的酒店
GET /hotel/_search
{query: {geo_distance:{distance:3km,location:40.174697,116.5864}}
}查询结果如下
{...hits : {total : {value : 1,relation : eq},max_score : 1.0,hits : [{_index : hotel,_type : _doc,_id : 52,_score : 1.0,_source : {title : 连锁酒店2,location : 40.19103839805197,116.5624013764374}}]}
}而geo_distance和后面我们的sort排序用的很紧密例如微信附近的人就可以通过该功能实现其中结合sort可以返回当前位置与目标位置之间的距离。这个我们后面会介绍。 在Java客户端可以使用new GeoDistanceQueryBuilder()构造geo_distance查询通过distance()设置以指定坐标点为中心的半径大小以及距离的单位point()设置指定坐标点service如下 public ListHotel geoDistanceQuery(HotelDocRequest hotelDocRequest) throws IOException {//新建搜索请求String indexName getNotNullIndexName(hotelDocRequest);SearchRequest searchRequest new SearchRequest(indexName);SearchSourceBuilder searchSourceBuilder new SearchSourceBuilder();// 假设目标距离坐标GeoPoint sourcePoint new GeoPoint(40.174697D, 116.5864D);GeoDistanceQueryBuilder geoDistanceQueryBuilder new GeoDistanceQueryBuilder(location).distance(3, DistanceUnit.KILOMETERS).point(sourcePoint);searchSourceBuilder.query(geoDistanceQueryBuilder);searchRequest.source(searchSourceBuilder);return getQueryResult(searchRequest);}controller如下 PostMapping(/query/geo-distance)public FoundationResponseListHotel geoDistanceQuery(RequestBody HotelDocRequest hotelDocRequest) {try {ListHotel hotelList esQueryService.geoDistanceQuery(hotelDocRequest);if (CollUtil.isNotEmpty(hotelList)) {return FoundationResponse.success(hotelList);} else {return FoundationResponse.error(100, no data);}} catch (IOException e) {log.warn(搜索发生异常原因为:{}, e.getMessage());return FoundationResponse.error(100, e.getMessage());} catch (Exception e) {log.error(服务发生异常原因为:{}, e.getMessage());return FoundationResponse.error(100, e.getMessage());}}postman截图如下
四、geo_polygon
ES的geo_polygon语法可以通过指定多个坐标点从而构成一个多边形然后从当前多边形中召回坐落其中的元素进行召回在当前语法中最少需要3个坐标从而构成一个多边形 例如我在ES增加一个我公司的坐标121.530533,31.085692
POST /hotel/_doc/031
{title:上海闵行浦江智谷,location:{lat:31.085692,lon:121.530533}
}然后可以指定3个坐标将公司位置坐落于这三个坐标中看看公司位置是否可以检索出来3个坐标在地图上的展示如下
坐标点A(121.531257,31.085262) 坐标点B(121.529694,31.085494) 坐标点C(121.530632,31.086252)
ES的geo_polygon语法也支持多种语法如 经纬度数组、经纬度字符串、GeoHash值等这里就采用字符串演示了另外两种语法不再赘述其执行DSL如下
GET /hotel/_search
{query: {geo_polygon: {location: {points: [31.085262,121.531257,31.085494, 121.529694,31.086252, 121.530632]}}}
}结果如下
{...hits : {total : {value : 1,relation : eq},max_score : 1.0,hits : [{_index : hotel,_type : _doc,_id : 031,_score : 1.0,_source : {title : 上海闵行浦江智谷,location : {lat : 31.085692,lon : 121.530533}}}]}
}可以看到通过三个点构建成一个三角形当目标元素坐落于所构建的形状中即可很好的将其召回
到这里关于ES的geo_point语法已经接近尾声了简单的了解了一下ES的空间地理支持下面再新增一种相对复杂一点的地形看看geo_polygon语法可以很好的支持不。
通过刚才的3个坐标我们新增一个坐标构建一个凹形的多边形将目标节点剔除在多边形外看看ES在这方面的支持如何最终构建的多边形如下:
GET /hotel/_search
{query: {geo_polygon: {location: {points: [31.085262,121.531257,31.086252, 121.530632,31.085494, 121.529694,31.085854,121.530524]}}}
}结果没有找到 需要注意的是查询语句输入的点的顺序是需要注意的它会按照你输入的点的顺序构成不同的多边形从而出现不同的结果就像我上图那样的使用箭头标注顺序如果我改变某个点的顺序有可能就会把目标囊括进去从而和之前结果不一样。 那么在java客户端可以使用new GeoPolygonQueryBuilder ()构造geo_polygon查询构造方法包含需要查询的字段以及可以接收一个GeoPoint数组数组就和我们刚才DSL中输入的那些点是一样的记住要按照顺序放service如下
public ListHotel geoPolygonQuery(HotelDocRequest hotelDocRequest) throws IOException {//新建搜索请求String indexName getNotNullIndexName(hotelDocRequest);SearchRequest searchRequest new SearchRequest(indexName);SearchSourceBuilder searchSourceBuilder new SearchSourceBuilder();// 假设目标距离坐标ArrayListGeoPoint geoPoints new ArrayListGeoPoint();GeoPoint sourcePoint1 new GeoPoint(31.085262D, 121.531257D);GeoPoint sourcePoint2 new GeoPoint(31.085494D, 121.529694D);GeoPoint sourcePoint3 new GeoPoint(31.086252D, 121.530632D);geoPoints.add(sourcePoint1);geoPoints.add(sourcePoint2);geoPoints.add(sourcePoint3);GeoPolygonQueryBuilder geoPolygonQueryBuilder new GeoPolygonQueryBuilder(location, geoPoints);searchSourceBuilder.query(geoPolygonQueryBuilder);searchRequest.source(searchSourceBuilder);return getQueryResult(searchRequest);}controller如下
PostMapping(/query/geo-polygon)public FoundationResponseListHotel geoPolygonQuery(RequestBody HotelDocRequest hotelDocRequest) {try {ListHotel hotelList esQueryService.geoPolygonQuery(hotelDocRequest);if (CollUtil.isNotEmpty(hotelList)) {return FoundationResponse.success(hotelList);} else {return FoundationResponse.error(100, no data);}} catch (IOException e) {log.warn(搜索发生异常原因为:{}, e.getMessage());return FoundationResponse.error(100, e.getMessage());} catch (Exception e) {log.error(服务发生异常原因为:{}, e.getMessage());return FoundationResponse.error(100, e.getMessage());}}postman执行如下