需求: 通过指定点搜索附近的人 , 要求可以过滤年龄, 结果按照距离进行排序, 并且展示她/他距离你多远
本文参考:
es官网文档: https://www.elastic.co/guide/cn/elasticsearch/guide/current/sorting-by-distance.html
Spring官网文档:https://docs.spring.io/spring-data/elasticsearch/docs/4.0.3.RELEASE/reference/html/#new-features
ES提供了很多地理位置的搜索方式 :
geo_bounding_box
: 找出落在指定矩形框中的点。geo_distance
: 找出与指定位置在给定距离内的点。geo_distance_range
: 找出与指定点距离在给定最小距离和最大距离之间的点。一般我们常用的是前两者:
这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。 你指定一个矩形的 顶部
, 底部
, 左边界
,和 右边界
,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间: 一般只要设定左上的坐标 和右下的坐标即可
GET /attractions/restaurant/_search
{
"query": {
"filtered": {
"filter": {
"geo_bounding_box": {
"type": "indexed",
"location": {
"top_left": {
"lat": 40.8,
"lon": -74.0
},
"bottom_right": {
"lat": 40.7,
"lon": -73.0
}
}
}
}
}
}
}
地理距离过滤器( geo_distance
)以给定位置为圆心画一个圆,来找出那些地理坐标落在其中的文档:
GET /attractions/restaurant/_search
{
"query": {
"filtered": {
"filter": {
"geo_distance": {
"distance": "1km",
"location": {
"lat": 40.715,
"lon": -73.988
}
}
}
}
}
}
距离单位es官方给我们提供了很多种: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#distance-units
常用的我们m, km就够了
检索结果可以按与指定点的距离排序:
注意: 当你 可以 按距离排序时, 按距离打分 通常是一个更好的解决方案。但是我们要计算当前距离,所以还是使用这个排序
搜索示例:
GET /attractions/restaurant/_search
{
"query": {
"filtered": {
"filter": {
"geo_bounding_box": {
"type": "indexed",
"location": {
"top_left": {
"lat": 40.8,
"lon": -74.0
},
"bottom_right": {
"lat": 40.4,
"lon": -73.0
}
}
}
}
}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 40.715,
"lon": -73.998
},
"order": "asc",
"unit": "km",
"distance_type": "plane"
}
}
]
}
解读以下: (注意看sort对象)
博主当前ElasticSearch版本为7.8.0;
springboot版本
org.springframework.boot
spring-boot-starter-parent
2.4.0-SNAPSHOT
starter依赖
org.springframework.boot
spring-boot-starter-data-elasticsearch
2.4.0-SNAPSHOT
数据库字段设计: 添加两个字段经纬度
CREATE TABLE `es_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL,
`age` int(5) DEFAULT NULL,
`tags` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '多标签用 ''|'' 分割',
`user_desc` varchar(255) COLLATE utf8mb4_bin DEFAULT '' COMMENT '用户简介',
`is_deleted` varchar(1) COLLATE utf8mb4_bin NOT NULL DEFAULT 'N',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`lat` decimal(10,6) DEFAULT '0.000000' COMMENT '维度',
`lon` decimal(10,6) DEFAULT '0.000000' COMMENT '经度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=657 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
Java代码:
/**
* (EsUser)实体类
*
* @author makejava
* @since 2020-08-10 14:23:59
*/
@Data
public class EsUserEntity implements Serializable {
private static final long serialVersionUID = 578800011612714754L;
/**
* 主键
*/
private Long id;
private String name;
private Integer age;
/**
* 多标签用 '|' 分割
*/
private String tags;
/**
* 用户简介
*/
private String userDesc;
private String isDeleted = "0";
private Date gmtCreate;
private Date gmtModified;
// 经度
private Double lat;
// 维度
private Double lon;
}
Java中对象映射: 我这边直接采用了GeoPoint字段, 根据spring官网, 也可以通过字符串表示, 但是我这里还是用es推荐的字段
@Data
@Document(indexName = "es_user")
public class ESUser {
@Id
private Long id;
@Field(type = FieldType.Text)
private String name;
@Field(type = FieldType.Integer)
private Integer age;
@Field(type = FieldType.Keyword)
private List tags;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String desc;
@GeoPointField
private GeoPoint location;
}
数据的导入的同步是通过rocketmq进行同步的, 代码不展示了, 参考文档https://blog.csdn.net/weixin_38399962/article/details/107918244
这里笔者以经度:120.24 维度:30.3 作为圆心, mock一些附近的点作为mock用户的坐标数据
@ApiOperation("录入测试")
@PostMapping("/content/test-insert")
public Long importEsUser(Long num) {
for (int i = 0; i < num; i++) {
ThreadPoolUtil.execute(() -> {
EsUserEntity esUser = generateRandomMockerUser();
esUserService.importEsUser(esUser);
});
}
return num;
}
// mock随机用户数据
private EsUserEntity generateRandomMockerUser() {
// 120.247589,30.306362
EsUserEntity esUserEntity = new EsUserEntity();
int age = new Random().nextInt(20) + 5;
esUserEntity.setAge(age);
boolean flag = age % 2 > 0;
esUserEntity.setName(flag ? RandomCodeUtil.getRandomChinese("0") : RandomCodeUtil.getRandomChinese("1"));
esUserEntity.setTags(flag ? "善良|Java|帅气" : "可爱|稳重|React");
esUserEntity.setUserDesc(flag ? "大闹天宫,南天门守卫, 擅长编程, 烹饪" : "天空守卫,擅长编程,睡觉");
String latRandNumber = RandomCodeUtil.getRandNumberCode(4);
String lonRandNumber = RandomCodeUtil.getRandNumberCode(4);
esUserEntity.setLon(Double.valueOf("120.24" + latRandNumber));
esUserEntity.setLat(Double.valueOf("30.30" + lonRandNumber));
return esUserEntity;
}
数据导入成功.
我们先设计返回给前台的对象, 和搜索条件类
/**
* 功能描述:ES的用户搜索结果
*
* @Author: zhouzhou
* @Date: 2020/7/30$ 9:57$
*/
@Data
public class PeopleNearByVo {
private ESUserVo esUserVo;
private Double distance;
}
/**
* 功能描述:ES的用户搜索结果
*
* @Author: zhouzhou
* @Date: 2020/7/30$ 9:57$
*/
@Data
public class ESUserVo {
private Long id;
private String name;
private Integer age;
private List tags;
// 高亮部分
private List highLightTags;
private String desc;
// 高亮部分
private List highLightDesc;
// 坐标
private GeoPoint location;
}
搜索类
/**
* 功能描述: 搜索附近的人
*
* @Author: zhouzhou
* @Date: 2020/8/14$ 11:13$
*/
@Data
public class ESUserLocationSearch {
// 纬度 [3.86, 53.55]
private Double lat;
// 经度 [73.66, 135.05]
private Double lon;
// 搜索范围(单位米)
private Integer distance;
// 年龄大于等于
private Integer ageGte;
// 年龄小于
private Integer ageLt;
}
核心搜索方法:
/**
* 搜索附近的人
* @param locationSearch
* @return
*/
public Page queryNearBy(ESUserLocationSearch locationSearch) {
Integer distance = locationSearch.getDistance();
Double lat = locationSearch.getLat();
Double lon = locationSearch.getLon();
Integer ageGte = locationSearch.getAgeGte();
Integer ageLt = locationSearch.getAgeLt();
// 先构建查询条件
BoolQueryBuilder defaultQueryBuilder = QueryBuilders.boolQuery();
// 距离搜索条件
if (distance != null && lat != null && lon != null) {
defaultQueryBuilder.filter(QueryBuilders.geoDistanceQuery("location")
.distance(distance, DistanceUnit.METERS)
.point(lat, lon)
);
}
// 过滤年龄条件
if (ageGte != null && ageLt != null) {
defaultQueryBuilder.filter(QueryBuilders.rangeQuery("age").gte(ageGte).lt(ageLt));
}
// 分页条件
PageRequest pageRequest = PageRequest.of(0, 10);
// 地理位置排序
GeoDistanceSortBuilder sortBuilder = SortBuilders.geoDistanceSort("location", lat, lon);
//组装条件
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(defaultQueryBuilder)
.withPageable(pageRequest)
.withSort(sortBuilder)
.build();
SearchHits searchHits = elasticsearchRestTemplate.search(searchQuery, ESUser.class);
List peopleNearByVos = Lists.newArrayList();
for (SearchHit searchHit : searchHits) {
ESUser content = searchHit.getContent();
ESUserVo esUserVo = new ESUserVo();
BeanUtils.copyProperties(content, esUserVo);
PeopleNearByVo peopleNearByVo = new PeopleNearByVo();
peopleNearByVo.setEsUserVo(esUserVo);
peopleNearByVo.setDistance((Double) searchHit.getSortValues().get(0));
peopleNearByVos.add(peopleNearByVo);
}
// 组装分页对象
Page peopleNearByVoPage = new PageImpl<>(peopleNearByVos, pageRequest, searchHits.getTotalHits());
return peopleNearByVoPage;
}
controller层
@RequestMapping(value = "/query-doc/nearBy", method = RequestMethod.POST)
@ApiOperation("根据坐标点搜索附近的人")
public Page queryNearBy(@RequestBody ESUserLocationSearch locationSearch) {
return esUserService.queryNearBy(locationSearch);
}
下图的搜索条件为, 以北纬30.30,东经120.24为坐标点,搜索附近100米内 ,年龄大于等18岁, 小于25岁的人
结果为展示:
找到了2个, 排序按照距离排序, 年龄区间正确, 第一个兄弟距离39米, 第二个仁兄距离85米, 结果正确
{
"content": [
{
"esUserVo": {
"id": 601,
"name": "季福林",
"age": 22,
"tags": [
"可爱",
"稳重",
"React"
],
"highLightTags": null,
"desc": "天空守卫,擅长编程,睡觉",
"highLightDesc": null,
"location": {
"lat": 30.300214,
"lon": 120.240329,
"geohash": "wtms25urd9r8",
"fragment": true
}
},
"distance": 39.53764107382481
},
{
"esUserVo": {
"id": 338,
"name": "逄军",
"age": 20,
"tags": [
"可爱",
"稳重",
"React"
],
"highLightTags": null,
"desc": "天空守卫,擅长编程,睡觉",
"highLightDesc": null,
"location": {
"lat": 30.300242,
"lon": 120.240846,
"geohash": "wtms25uxwy3p",
"fragment": true
}
},
"distance": 85.56052789780142
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageNumber": 0,
"pageSize": 10,
"paged": true,
"unpaged": false
},
"last": true,
"totalPages": 1,
"totalElements": 2,
"size": 10,
"number": 0,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 2,
"first": true,
"empty": false
}
至此, 结束