Elasticsearch(十三) ElasticSearch搜索附近的人

需求: 通过指定点搜索附近的人 , 要求可以过滤年龄, 结果按照距离进行排序, 并且展示她/他距离你多远

本文参考:

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: 找出与指定点距离在给定最小距离和最大距离之间的点。

一般我们常用的是前两者: 

1. 地理坐标盒模型过滤器

这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。 你指定一个矩形的 顶部 , 底部 , 左边界 ,和 右边界 ,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间: 一般只要设定左上的坐标 和右下的坐标即可

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
            }
          }
        }
      }
    }
  }
}

2. 地理距离过滤器(这个就是我们想要的)

地理距离过滤器( 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就够了

3. 地理位置排序

检索结果可以按与指定点的距离排序:

注意: 当你 可以 按距离排序时, 按距离打分 通常是一个更好的解决方案。但是我们要计算当前距离,所以还是使用这个排序

搜索示例:

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对象)

  • 计算每个文档中 location 字段与指定的 lat/lon 点间的距离。
  • 将距离以 km 为单位写入到每个返回结果的 sort 键中。
  • 使用快速但精度略差的 plane 计算方式。

代码实操如下:

1. 环境准备

博主当前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
        

 

1. 设计数据库字段和ES的字段mapping

数据库字段设计: 添加两个字段经纬度

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

2. 准备海量mock数据

这里笔者以经度: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;
    }

Elasticsearch(十三) ElasticSearch搜索附近的人_第1张图片

数据导入成功.

3. 核心搜索

我们先设计返回给前台的对象, 和搜索条件类

/**
 * 功能描述: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);
    }

4. swagger测试

下图的搜索条件为, 以北纬30.30,东经120.24为坐标点,搜索附近100米内 ,年龄大于等18岁, 小于25岁的人

Elasticsearch(十三) ElasticSearch搜索附近的人_第2张图片

结果为展示: 

找到了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
}

至此, 结束

你可能感兴趣的:(ElasticSearch,elasticsearch)