• ES应用之搜索附近的人


    需求:

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

    设计:

    ES提供了很多地理位置的搜索方式 :
    一般常用的是前两者。

    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 计算方式。

    环境准备

    使用ElasticSearch 7.8.0版本,引入依赖。
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        <version>2.4.0-SNAPSHOT</version>
    </dependency> 

    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;
    实体类:
    /**
     * (EsUser)实体类
     */
    @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中对象映射: 
    @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<String> tags;
        @Field(type = FieldType.Text, analyzer = "ik_max_word")
        private String desc;
        @GeoPointField
        private GeoPoint location;
        
    } 

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

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

    /**
     * 功能描述:ES的用户搜索结果
     */
    @Data
    public class PeopleNearByVo {
     
         private ESUserVo esUserVo;
     
         private Double distance;
    }
    /**
     * 功能描述:ES的用户搜索结果
     */
    @Data
    public class ESUserVo {
     
        private Long id;
        private String name;
        private Integer age;
        private List<String> tags;
        // 高亮部分
        private List<String> highLightTags;
        private String desc;
        // 高亮部分
        private List<String> highLightDesc;
        // 坐标
        private GeoPoint location;
     
    } 
    搜索类
    /**
     * 功能描述: 搜索附近的人
     */
    @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<PeopleNearByVo> 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<ESUser> searchHits = elasticsearchRestTemplate.search(searchQuery, ESUser.class);
        List<PeopleNearByVo> peopleNearByVos = Lists.newArrayList();
        for (SearchHit<ESUser> 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<PeopleNearByVo> peopleNearByVoPage = new PageImpl<>(peopleNearByVos, pageRequest, searchHits.getTotalHits());
    
        return peopleNearByVoPage;
    } 
    controller层
    @RequestMapping(value = "/query-doc/nearBy", method = RequestMethod.POST)
    @ApiOperation("根据坐标点搜索附近的人")
    public Page<PeopleNearByVo> queryNearBy(@RequestBody ESUserLocationSearch locationSearch) {
        return esUserService.queryNearBy(locationSearch);
    } 

    4. swagger测试

    下图的搜索条件为, 以北纬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
    }
  • 相关阅读:
    STM32 -- 硬件知识
    PCIe相关的操作命令
    [转载]PCI/PCIe基础——配置空间
    [转载]网络虚拟化中的 offload 技术:LSO/LRO、GSO/GRO、TSO/UFO、VXLAN
    [转载]TSO、UFO、GSO、LRO、GRO和RSS介绍
    Linux应用函数 -- 字符串
    初级PLC
    中断方式下进行串口通讯的正确方法
    [altium] Altium Designer2013 13.3.4 (10.1881.28608) 完美版
    32个最热CPLD-FPGA论坛
  • 原文地址:https://www.cnblogs.com/johnvwan/p/15644841.html
Copyright © 2020-2023  润新知