SpringBoot中使用Elasticsearch的GEO地理
2021-01-12 本文已影响0人
singleZhang2010
概述
地图找房功能是各类搜房源APP提升用户体验的功能,在链接、安居客等网站上都有此类功能。如果直接使用数据库进行搜索,频繁的拖动和请求,数据库压力肯定是吃不消的,所以引入Elasticsearch,ES里正好有GEO地理位置检索的功能。
开始
准备一个house表
CREATE TABLE `house` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'house唯一标识',
`title` varchar(32) NOT NULL,
`price` int(11) unsigned NOT NULL COMMENT '价格',
`area` int(11) unsigned NOT NULL COMMENT '面积',
`room` int(11) unsigned NOT NULL COMMENT '卧室数量',
`floor` int(11) unsigned NOT NULL COMMENT '楼层',
`total_floor` int(11) unsigned NOT NULL COMMENT '总楼层',
`watch_times` int(11) unsigned DEFAULT '0' COMMENT '被看次数',
`build_year` int(4) NOT NULL COMMENT '建立年限',
`status` int(4) unsigned NOT NULL DEFAULT '0' COMMENT '房屋状态 0-未审核 1-审核通过 2-已出租 3-逻辑删除',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`last_update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近数据更新时间',
`city_en_name` varchar(32) NOT NULL COMMENT '城市标记缩写 如 北京bj',
`region_en_name` varchar(255) NOT NULL COMMENT '地区英文简写 如昌平区 cpq',
`cover` varchar(32) DEFAULT NULL COMMENT '封面',
`direction` int(11) NOT NULL COMMENT '房屋朝向',
`distance_to_subway` int(11) NOT NULL DEFAULT '-1' COMMENT '距地铁距离 默认-1 附近无地铁',
`parlour` int(11) NOT NULL DEFAULT '0' COMMENT '客厅数量',
`district` varchar(32) NOT NULL COMMENT '所在小区',
`admin_id` int(11) NOT NULL COMMENT '所属管理员id',
`bathroom` int(11) NOT NULL DEFAULT '0',
`street` varchar(32) NOT NULL COMMENT '街道',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COMMENT='房屋信息表';
加入测试数据
INSERT INTO `house` VALUES (16, '富力城 国贸CBD 时尚休闲 商务办公', 6300, 70, 2, 10, 20, 0, 2012, 1, '2021-01-12 18:56:14', '2021-01-12 16:34:03', 'bj', 'hdq', 'FvkO1FFyGbrxCP_1O9tA94u2qvbP', 1, -1, 1, '融泽嘉园', 2, 0, '龙域西二路');
INSERT INTO `house` VALUES (18, '华贸城 东向一居挑空loft 干净温馨 随时可以签约', 5700, 52, 1, 7, 20, 0, 2012, 1, '2021-01-12 18:56:14', '2021-01-12 16:34:05', 'bj', 'hdq', 'Fl1lNikhmMIecbTn-JTsurxugtFU', 2, 1085, 1, '融泽嘉园', 2, 0, '龙域西二路');
INSERT INTO `house` VALUES (19, '望春园板楼三居室 自住精装 南北通透 采光好视野棒!', 9200, 132, 3, 6, 14, 0, 2005, 1, '2021-01-12 18:56:14', '2021-01-12 16:34:07', 'bj', 'hdq', 'Fp1xPKVYtPsCeVHVQVW0Hif2FXk7', 2, 1108, 2, '融泽嘉园', 2, 0, '龙域西二路');
INSERT INTO `house` VALUES (20, '高大上的整租两居室 业主诚意出租', 5400, 56, 2, 12, 20, 0, 2012, 1, '2021-01-12 18:56:14', '2021-01-12 16:34:08', 'bj', 'hdq', 'FvVqU8LneZZ5xaLBAOM1KXR2Pz1X', 2, -1, 1, '融泽嘉园', 2, 0, '龙域西二路');
-
把数据导入ElasticSearch
地理位置信息可以通过百度地图api获取,组装后一并提交给ES,在浏览器上查看数据可以看到location字段的值
image.png
获取location值的方法如下:
/**
通过城市、地址查询地图坐标
*/
public BaiduMapLocation getBaiduMapLocation(String city, String address) {
String encodeAddress;
String encodeCity;
final BAIDU_MAP_GEOCONV_API = "http://api.map.baidu.com/geocoder/v2/?";
final BAIDU_MAP_KEY = "6QtSF673D1pYl3eQkEXfwp8ZgsQpB77U";
try {
encodeAddress = URLEncoder.encode(address, "UTF-8");
encodeCity = URLEncoder.encode(city, "UTF-8");
} catch (UnsupportedEncodingException e) {
log.error("Error to encode house address", e);
return new ServiceResult<>(false, "Error to encode hosue address");
}
HttpClient httpClient = HttpClients.createDefault();
StringBuilder sb = new StringBuilder(BAIDU_MAP_GEOCONV_API);
sb.append("address=").append(encodeAddress).append("&")
.append("city=").append(encodeCity).append("&")
.append("output=json&")
.append("ak=").append(BAIDU_MAP_KEY);
HttpGet get = new HttpGet(sb.toString());
try {
HttpResponse response = httpClient.execute(get);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
return null;
}
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
JsonNode jsonNode = objectMapper.readTree(result);
int status = jsonNode.get("status").asInt();
if (status != 0) {
return null;
} {
BaiduMapLocation location = new BaiduMapLocation();
JsonNode jsonLocation = jsonNode.get("result").get("location");
location.setLongitude(jsonLocation.get("lng").asDouble());
location.setLatitude(jsonLocation.get("lat").asDouble());
return BaiduMapLocation ;
}
} catch (IOException e) {
log.error("Error to fetch baidumap api", e);
return null;
}
}
BaiduMapLocation实体类如下:
@Data
public class BaiduMapLocation {
// 经度
@JsonProperty("lon")
private double longitude;
// 纬度
@JsonProperty("lat")
private double latitude;
}
- 准备好数据后就可以写业务逻辑测试了
地图拖动时传入的主要参数为:地图缩放等级、地图的边界即左上角点和右下角点的坐标
所以创建一个地图检索的请求参数实体类:
@Data
public class MapSearch {
private String cityEnName;
/**
* 地图缩放级别
*/
private int level = 12;
private String orderBy = "lastUpdateTime";
private String orderDirection = "desc";
/**
* 左上角
*/
private Double leftLongitude;
private Double leftLatitude;
/**
* 右下角
*/
private Double rightLongitude;
private Double rightLatitude;
private int start = 0;
private int size = 5;
}
4.创建一个检索服务类SearchService和返回结果实体类ServiceMultiResult
ServiceMultiResult
public class ServiceMultiResult<T> {
private long total;
private List<T> result;
public ServiceMultiResult(long total, List<T> result) {
this.total = total;
this.result = result;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public List<T> getResult() {
return result;
}
public void setResult(List<T> result) {
this.result = result;
}
public int getResultSize() {
if (this.result == null) {
return 0;
}
return this.result.size();
}
}
SearchService
@Slf4j
@Service
public class SearchService{
private static final String INDEX_NAME = "xunwu";
private static final String INDEX_TYPE = "house";
private static final String INDEX_TOPIC = "house_build";
public ServiceMultiResult<Long> mapQuery(MapSearch mapSearch) {
//创建查询条件
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//地图检索参数中传入的城市
boolQuery.filter(QueryBuilders.termQuery(HouseIndexKey.CITY_EN_NAME, mapSearch.getCityEnName()));
//通过geoBoundingBoxQuery方法来进行地理位置搜索
boolQuery.filter(
QueryBuilders.geoBoundingBoxQuery("location")
.setCorners(
new GeoPoint(mapSearch.getLeftLatitude(), mapSearch.getLeftLongitude()),
new GeoPoint(mapSearch.getRightLatitude(), mapSearch.getRightLongitude())
));
//检索条件设置
SearchRequestBuilder builder = this.esClient.prepareSearch(INDEX_NAME)
.setTypes(INDEX_TYPE)
.setQuery(boolQuery)
.addSort(HouseSort.getSortKey(mapSearch.getOrderBy()),
SortOrder.fromString(mapSearch.getOrderDirection()))
.setFrom(mapSearch.getStart())
.setSize(mapSearch.getSize());
List<Long> houseIds = new ArrayList<>();
//查询结果
SearchResponse response = builder.get();
if (RestStatus.OK != response.status()) {
log.warn("Search status is not ok for " + builder);
return new ServiceMultiResult<>(0, houseIds);
}
for (SearchHit hit : response.getHits()) {
houseIds.add(Longs.tryParse(String.valueOf(hit.getSourceAsMap().get(HouseIndexKey.HOUSE_ID))));
}
return new ServiceMultiResult<>(response.getHits().getTotalHits(), houseIds);
}
}
这里主要看QueryBuilders,是es中提供的一个查询接口,其中的geoBoundingBoxQuery方法用来进行地理位置查询。
5.最后加个测试控制器来测试一下
@Controller
public class HouseController {
@Autowired
SearchService searchService
@GetMapping("/test")
@ResponseBody
public ServiceMultiResult<Long> test(@ModelAttribute MapSearch mapSearch) {
ServiceMultiResult<Long> serviceMultiResult;
serviceMultiResult = searchService.mapQuery(mapSearch);
return serviceMultiResult ;
}
}
好了,基于Elasticsearch的GEO地理位置搜索功能就这样完成了,demo需要优化的地方还有很多,只提供大概的思路,也可以按不同的思路做,比如location获取那段,可以在房源编辑的时候通过地图拾取坐标,直接加到数据库里。
总结
Elasticsearch将在以后的项目中经常会用到,学好这个中间件可以丰富自己的技术栈。