学习spring boot

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, '龙域西二路');
  1. 把数据导入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;

}
  1. 准备好数据后就可以写业务逻辑测试了
    地图拖动时传入的主要参数为:地图缩放等级、地图的边界即左上角点和右下角点的坐标
    所以创建一个地图检索的请求参数实体类:
@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将在以后的项目中经常会用到,学好这个中间件可以丰富自己的技术栈。

上一篇 下一篇

猜你喜欢

热点阅读