es的空间检索提供了通过空间距离和位置关系进行检索的能力。将es整合到springboot中,可以轻松实现空间检索模块的编写与使用。
Springboot整合es的具体流程此处不再赘述,若有需要。可以参考如下文档:
从零开始:SpringBoot整合ElasticSearch实现简单增删改查
一. 使用 postman 新增实例
具体使用中,可以自己编写新增接口来向es中批量添加实例。由于上一篇文档中已经详细说明了如何编写增删改查的接口,此处不再重复编写新增接口,而是直接用postman向es中增加了若干条演示实例。
1. 新建 es 映射
本文档使用 city 对象来演示空间检索功能。city 对象具有 id,name 和坐标 locationPoint 的基本属性。如何使用postman在es中新建映射请查看上面提到的文档。
city映射:
{
"mappings": {
"city": {
"properties": {
"id": {
"type": "text",
"fields": {
"keyword": {
"type":"keyword",
"ignore_above":256
}
}
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type":"keyword",
"ignore_above":256
}
}
},
"locationPoint": {
"type": "geo_point",
"fields": {
"keyword": {
"type":"keyword",
"ignore_above":256
}
}
}
}
}
}
}
2. 向es中添加数据
实例如下:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "place",
"_type": "city",
"_id": "111",
"_score": 1.0,
"_source": {
"id": "111",
"name": "A1",
"locationPoint": {
"lat": 41.12,
"lon": -71.34
}
}
},
{
"_index": "place",
"_type": "city",
"_id": "222",
"_score": 1.0,
"_source": {
"id": "222",
"name": "A2",
"locationPoint": {
"lat": 40.12,
"lon": -70.34
}
}
},
{
"_index": "place",
"_type": "city",
"_id": "333",
"_score": 1.0,
"_source": {
"id": "333",
"name": "A3",
"locationPoint": {
"lat": 38.12,
"lon": -76.34
}
}
},
{
"_index": "place",
"_type": "city",
"_id": "444",
"_score": 1.0,
"_source": {
"id": "555",
"name": "B2",
"locationPoint": {
"lat": 40,
"lon": -74
}
}
}
]
}
}
二. 在 springboot 中编写相关接口
1. 新建 city 类
City:
package com.example.elasticsearchdemo.bean;
import org.springframework.data.annotation.Id;
public class City {
@Id
private String id;
//城市名
private String name;
//经纬度
private EsGeoPoint locationPoint;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public EsGeoPoint getLocationPoint() {
return locationPoint;
}
public void setLocationPoint(EsGeoPoint locationPoint) {
this.locationPoint = locationPoint;
}
@Override
public String toString() {
return "City{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", locationPoint=" + locationPoint +
'}';
}
}
EsGeoPoint:
package com.example.elasticsearchdemo.bean;
public class EsGeoPoint {
//纬度
private double lat;
//经度
private double lon;
public double getLat() {
return lat;
}
public void setLat(double lat) {
this.lat = lat;
}
public double getLon() {
return lon;
}
public void setLon(double lon) {
this.lon = lon;
}
@Override
public String toString() {
return "EsGeoPoint{" +
"lat=" + lat +
", lon=" + lon +
'}';
}
}
2. 新建 GeoSearch 查询类
GeoSearch:
package com.example.elasticsearchdemo.bean;
import org.elasticsearch.common.geo.GeoPoint;
import java.util.List;
public class GeoSearch {
private String type;
// 类型{Polygon: 多边形; Circle: 圆; bounding: 矩形}
private List<GeoPoint> points;
// 点集
private String radius;
// 半径
private GeoPoint origin;
// 原点
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<GeoPoint> getPoints() {
return points;
}
public void setPoints(List<GeoPoint> points) {
this.points = points;
}
public String getRadius() {
return radius;
}
public void setRadius(String radius) {
this.radius = radius;
}
public GeoPoint getOrigin() {
return origin;
}
public void setOrigin(GeoPoint origin) {
this.origin = origin;
}
@Override
public String toString() {
return "GeoSearch{" +
"type='" + type + '\'' +
", points=" + points +
", radius='" + radius + '\'' +
", origin=" + origin +
'}';
}
}
3. Controller 层
GeoSearchController:
package com.example.elasticsearchdemo.controller;
import com.alibaba.fastjson.JSONObject;
import com.example.elasticsearchdemo.bean.GeoSearch;
import com.example.elasticsearchdemo.service.GeoSearchService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/geoSearch")
@CrossOrigin(value = "*", maxAge = 3600)
public class GeoSearchController {
@Resource
private GeoSearchService service;
@RequestMapping(value = "/search",method = RequestMethod.POST)
public JSONObject geoSearch(@RequestBody GeoSearch search) throws Exception{
JSONObject output = new JSONObject();
JSONObject result = service.geoSearch(search);
if (result.get("msg") == "succeed") {
output.put("msg" ,"succeed");
output.put("data", result.get("data"));
} else {
output.put("msg", "failed");
output.put("data", result.get("data"));
}
return output;
}
}
2. Service 层
GeoSearchService:
package com.example.elasticsearchdemo.service;
import com.alibaba.fastjson.JSONObject;
import com.example.elasticsearchdemo.bean.City;
import com.example.elasticsearchdemo.bean.GeoSearch;
import com.example.elasticsearchdemo.dao.common.GeoSearchDao;
import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Service
public class GeoSearchService {
@Resource
private GeoSearchDao geoSearchDao;
public JSONObject geoSearch(GeoSearch search) throws Exception{
JSONObject out = new JSONObject();
String type = search.getType();
List result = new ArrayList<>();
//type为空则返回所有数据
if ((type == null) || (type == "")) {
List list = geoSearchDao.getAll();
out.put("msg", "succeed");
out.put("data", list);
return out;
}
//根据type判断采用何种查询
else if (type.equals("Polygon") || type == "Polygon") {
List<GeoPoint> list = search.getPoints();
result = geoSearchDao.searchGeoPolygon(list);
}
else if (type.equals("Circle") || type == "Circle") {
String distance = search.getRadius();
GeoPoint point = search.getOrigin();
result = geoSearchDao.searchGeoDistance(distance, point);
}
else if (type.equals("bounding") ||type == "bounding") {
if (search.getPoints().size() >= 2) {
GeoPoint point1 = search.getPoints().get(0);
GeoPoint point2 = search.getPoints().get(1);
result = geoSearchDao.searchGeoBoundingBox(point1, point2);
} else {
out.put("msg", "failed");
out.put("data", "Points error");
return out;
}
} else {
out.put("msg", "failed");
out.put("data", "Type error");
return out;
}
out.put("msg", "succeed");
out.put("data", result);
return out;
}
}
3. Dao 层
GeoSearchDao:
package com.example.elasticsearchdemo.dao.common;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Repository
public class GeoSearchDao {
@Resource
private RestHighLevelClient client;
@Value("${elasticsearch.place.index}")
private String cityIndex;
@Value("${elasticsearch.city.type}")
private String cityType;
@Resource
private Factory factory;
/**查询所有*/
public List getAll() throws IOException {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
boolQueryBuilder.should(matchAllQueryBuilder);
searchSourceBuilder.query(boolQueryBuilder);
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices(cityIndex);
searchRequest.types(cityType);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
List list = new ArrayList();
for (SearchHit hit: searchResponse.getHits()) {
String s = JSON.toJSONString(hit);
JSONObject o = JSON.parseObject(s);
list.add(o.getJSONObject("sourceAsMap"));
}
return list;
}
/**
* 查询多边形中的位置
* @param points
* @return
* @throws Exception
*/
public List searchGeoPolygon(List<GeoPoint> points) throws Exception {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = factory.builtPolygonQuery("locationPoint",points);
return search(searchSourceBuilder, boolQueryBuilder);
}
/**
* 查询矩形范围内位置
* @param point1
* @param point2
* @return
* @throws Exception
*/
public List searchGeoBoundingBox(GeoPoint point1, GeoPoint point2) throws Exception {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = factory.builderBoundingBox("locationPoint", point1, point2);
return search(searchSourceBuilder, boolQueryBuilder);
}
/**
* 查询圆内位置
* @param distance
* @param point
* @return
* @throws Exception
*/
public List searchGeoDistance(String distance, GeoPoint point) throws Exception {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = factory.builderDistance("locationPoint", distance, point);
return search(searchSourceBuilder, boolQueryBuilder);
}
public List search(SearchSourceBuilder searchSourceBuilder, BoolQueryBuilder boolQueryBuilder) throws Exception {
searchSourceBuilder.query(boolQueryBuilder);
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices(cityIndex);
searchRequest.types(cityType);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
List list = new ArrayList();
for (SearchHit hit: searchResponse.getHits()) {
String s = JSON.toJSONString(hit);
System.out.println(s);
JSONObject o = JSON.parseObject(s);
list.add(o.getJSONObject("sourceAsMap"));
}
return list;
}
}
Factory:
package com.example.elasticsearchdemo.dao.common;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class Factory {
/**
* 查询多边形内数据
* @param field
* @param points
* @return
* @throws Exception
*/
public BoolQueryBuilder builtPolygonQuery(String field, List<GeoPoint> points) throws Exception {
if (points == null ||points.size() <=0) {
throw new Exception("bad args of geo points");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
QueryBuilder matchQueryBuilder = QueryBuilders.geoPolygonQuery(field, points);
boolQueryBuilder.should(matchQueryBuilder);
return boolQueryBuilder;
}
/**
* 查询矩形框内数据
* @param field
* @param point1
* @param point2
* @return
* @throws Exception
*/
public BoolQueryBuilder builderBoundingBox(String field, GeoPoint point1, GeoPoint point2) throws Exception {
if (point1 == null ||point2 == null) {
throw new Exception("bad args of geo points");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
QueryBuilder matchQueryBuilder = QueryBuilders.geoBoundingBoxQuery(field).setCorners(point1, point2);
boolQueryBuilder.should(matchQueryBuilder);
return boolQueryBuilder;
}
/**
* 查询圆内数据
* @param field
* @param distance
* @param point
* @return
* @throws Exception
*/
public BoolQueryBuilder builderDistance(String field, String distance, GeoPoint point) throws Exception {
if (point == null) {
throw new Exception("bad args of geo points");
}
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
QueryBuilder matchQueryBuilder = QueryBuilders.geoDistanceQuery(field).point(point).distance(distance, DistanceUnit.METERS);
boolQueryBuilder.should(matchQueryBuilder);
return boolQueryBuilder;
}
}
三. 使用 postman 测试空间检索接口
以 springboot 端口为 9650 为例,实际以自己配置中端口为准。
1. 返回所有
当 type 传入为空时,默认返回所有数据。
POST http://localhost:9650/geoSearch/search
传参:
{
"type": ""
}
返回值:
{
"msg": "succeed",
"data": [
{
"name": "A1",
"id": "111",
"locationPoint": {
"lon": -71.34,
"lat": 41.12
}
},
{
"name": "A2",
"id": "222",
"locationPoint": {
"lon": -70.34,
"lat": 40.12
}
},
{
"name": "A3",
"id": "333",
"locationPoint": {
"lon": -76.34,
"lat": 38.12
}
},
{
"name": "B2",
"id": "555",
"locationPoint": {
"lon": -74,
"lat": 40
}
}
]
}
2. 返回处于指定圆形范围中的数据
传入参数中 radius 为半径(不添加单位默认为m,可指定为km),origin 为圆心坐标。
POST http://localhost:9650/geoSearch/search
传参:
{
"type": "Circle",
"radius": "200km",
"origin": {
"lat": 41,
"lon": -71
}
}
返回值:
{
"msg": "succeed",
"data": [
{
"name": "A1",
"id": "111",
"locationPoint": {
"lon": -71.34,
"lat": 41.12
}
},
{
"name": "A2",
"id": "222",
"locationPoint": {
"lon": -70.34,
"lat": 40.12
}
}
]
}
3. 返回处于指定矩形范围中的数据
传入参数中 points 为矩形左上角和右下角的坐标集。
POST http://localhost:9650/geoSearch/search
传参:
{
"type": "Bounding",
"points": [
{
"lat": "42",
"lon": "-72"
},
{
"lat": "40",
"lon": "-71"
}
]
}
返回值:
{
"msg": "succeed",
"data": [
{
"name": "A1",
"id": "111",
"locationPoint": {
"lon": -71.34,
"lat": 41.12
}
}
]
}
3. 返回处于指定多边形范围中的数据
传入参数中 points 为构成多边形点的坐标集。
POST http://localhost:9650/geoSearch/search
传参(以三角形为例):
{
"type": "Polygon",
"points": [
{
"lat": "42",
"lon": "-72"
},
{
"lat": "35",
"lon": "-72"
},
{
"lat": "40",
"lon": "-60"
}
]
}
返回值:
{
"msg": "succeed",
"data": [
{
"name": "A1",
"id": "111",
"locationPoint": {
"lon": -71.34,
"lat": 41.12
}
},
{
"name": "A2",
"id": "222",
"locationPoint": {
"lon": -70.34,
"lat": 40.12
}
}
]
}
以上即为 springboot 整合 es 实现空间检索的简单实现方法。