用Elasticsearch搜索匹配功能实现基于地理位置的查询

1.Redis,MongoDB,Elasticsearch实现地理位置查询比较

1.1 Redis:

优点:Redis提供了地理空间索引功能,可以通过Geo数据类型进行地理位置查询。这使得Redis在处理地理位置查询时非常高效。

缺点:

Redis的地理空间索引功能相对简单,只能支持二维平面坐标系(经纬度)的查询,对于三维坐标系或者不规则地理区域的查询支持不够好。

功能有限:Redis的地理位置查询功能相对简单,仅支持基本的距离计算、范围查询等操作,无法满足复杂的空间查询需求。

存储容量限制:由于Redis数据存储在内存中,其存储容量受限于物理内存大小,对于大规模地理位置数据,可能需要进行分片或其他优化策略。

扩展性受限:Redis对于数据的扩展能力有限,不如Elasticsearch那样容易水平扩展以适应规模的增长。

使用场景:适用于需要快速查询地理位置信息的场景,小型应用,并且对于快速插入和查询地理位置数据有较高的实时性要求,可以考虑使用Redis geo。

1.2 MongoDB :

优点:

灵活性好:MongoDB支持多种地理位置查询操作,包括点查询、范围查询和多边形查询等。

数据结构简单:MongoDB的文档型结构非常适合存储地理位置数据,容易理解和使用。

高可用性:MongoDB提供了复制集和分片等机制来确保数据的高可用性和扩展性。

然而,MongoDB + 2d索引实现地理位置查询也存在一些缺点:

性能相对较差:相比Elasticsearch,在处理大规模的地理位置查询时,MongoDB的性能可能会受到限制。

功能相对简单:MongoDB的地理位置查询功能较为基础,相比Elasticsearch可能缺乏某些高级查询功能。

不支持部分地理位置操作:例如,MongoDB不支持直接计算两个地理位置之间的距离。

1.3 Elasticsearch geo

使用Elasticsearch geo实现地理位置查询的优点:

高性能:Elasticsearch是一种搜索引擎,使用geo点的经纬度数据可以快速进行空间查询和过滤,具有较高的查询效率。

灵活性:Elasticsearch提供了丰富的地理位置查询功能,例如可以根据距离、范围及其他条件进行查询和排序。

可扩展性:Elasticsearch可以通过分片和副本来实现水平扩展,以应对大规模的地理位置数据查询需求。

使用Elasticsearch geo实现地理位置查询的缺点:

学习成本:学习和配置Elasticsearch需要花费一定的时间和精力。

依赖性:使用Elasticsearch需要安装和维护Elasticsearch服务,这可能增加系统依赖和部署复杂性。

数据存储限制:Elasticsearch适用于小到中等大小的数据集,对于大量地理位置数据,可能需要额外的硬件资源和优化工作。

Elasticsearch为用户提供了基于地理位置的搜索功能。它主要支持两种类型的地理查询:一种是地理点(geo_point),即经纬度查询,另一种是地理形状查询(geo_shape),即支持点、线、圆形和多边形查询等

2.Elasticsearch geo地理位置数据类型

在Elasticsearch中,存在两种地理位置数据类型:geo_point和geo_shape。

geo_point:这是最基本的地理位置类型,通常用于表示一个二维坐标点(经度和纬度)。可以计算落在某个矩形内的点、以某个点为半径(圆)的点、某个多边形内的点等。此外,geo_point还可以用于排序、聚合等操作。

geo_shape:这种数据类型表示一个复杂的图形,使用的是GeoJSON的格式。它可以表达一块地理区域,区域的形状可以是任意多边形,也可以是点、线、面、多点、多线、多面等几何类型。然而,这种数据类型不能进行排序操作。

2.基于geo_point类型实现查询加油站案例

elasticsearch 版本7.12.1

2.1 springboot集成elasticsearch

java 复制代码
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
            <version>2.5.11</version>
        </dependency>
        <dependency>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
            <version>1.18.1</version>
        </dependency>
        <dependency>
            <groupId>org.locationtech.spatial4j</groupId>
            <artifactId>spatial4j</artifactId>
            <version>0.8</version>
        </dependency>

配置文件

java 复制代码
# es 服务地址
elasticsearch.host=127.0.0.1
# es 服务端口
elasticsearch.port=9200
# 配置日志级别
logging.level.org.springframework.data.elasticsearch.core=debug
logging.level.org.springframework.data.elasticsearch.client.WIRE=trace

配置类

java 复制代码
@ConfigurationProperties(prefix = "elasticsearch")
@Configuration
@Data
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
    private String host ;
    private Integer port ;
    //重写父类方法
    @Override
    public RestHighLevelClient elasticsearchClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
        RestHighLevelClient restHighLevelClient = new
                RestHighLevelClient(builder);

        return restHighLevelClient;
    }
}

测试实体类

java 复制代码
@ApiModel
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Document(indexName = "stationcc", shards = 3, replicas = 1)
public class ChargingStationDTO {
    //必须有 id,这里的 id 是全局唯一的标识,等同于 es 中的"_id"
    @Id
    @ApiModelProperty(value = "id", example = "111111111111")
    private Long baseId;

    /**
     * type : 字段数据类型
     * analyzer : 分词器类型
     * index : 是否索引(默认:true)
     * Keyword : 短语,不进行分词
     */
    @ApiModelProperty(value = "加油站ID", example = "111111111111")
    @Field(type = FieldType.Keyword)
    private String stationId;

    @ApiModelProperty(value = "运营商ID", example = "395815801")
    @Field(type = FieldType.Keyword)
    private String operatorId;

    @ApiModelProperty(value = "加油站名称", example = "测试加油站")
    @Field(type = FieldType.Keyword)
    private String stationName;

    @ApiModelProperty(value = "运营商名称", example = "测试")
    @Field(type = FieldType.Keyword)
    private String operatorName;

    @GeoPointField
    @ApiModelProperty(value = "经纬度")
    private GeoPoint location; 

    @Field(type = FieldType.Keyword)
    @ApiModelProperty(value = "详细地址", example = "山东路154号")
    private String address;


    @ApiModelProperty(value = "距离", example = "1.0")
    private  double distance;
}

初始化数据

java 复制代码
 @Test
    public void saveAll() {
        //起点 111.000,31.000
        //终点 121.000,31.000
        //( 121 , 31 )    -    ( 111 , 31 )    之间的距离为    952.8062737420901 km

        //96-121,23-40
        List<ChargingStationDTO> chargingStationDTOList = new ArrayList<>();
        List<String> stringList = CollUtil.newArrayList("招式", "王五", "基于", "好好", "电动", "反复", "第三十", "十三点", "但是");
        for (int i = 2000; i < 450000; i++) {
            ChargingStationDTO chargingStationDTO = new ChargingStationDTO();
            chargingStationDTO.setBaseId(Long.valueOf(i));
            chargingStationDTO.setStationId(Long.valueOf(i).toString());
            chargingStationDTO.setOperatorId(Long.valueOf(i).toString());
            chargingStationDTO.setStationName(RandomUtil.randomEleList(stringList, 1).get(0));
            chargingStationDTO.setAddress("地址" + i);
            //经度范围是0-180°,纬度范围是0-90°
            //纬度
            double lat = RandomUtil.randomDouble(23.000, 40.000, 3, RoundingMode.DOWN);
            //经度
            double lon = RandomUtil.randomDouble(96.000, 121.000, 3, RoundingMode.DOWN);

            chargingStationDTO.setLocation(new GeoPoint(lat, lon));
            chargingStationDTOList.add(chargingStationDTO);
            if (chargingStationDTOList.size() == 1000) {
                chargingStationDao.saveAll(chargingStationDTOList);
                chargingStationDTOList.clear();
                System.out.println("插入1000,i"+i);
            }

        }

    }

2.2 查询附近加油站(圆形查询)

请求参数

java 复制代码
@ApiModel
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChargingStationNearbySearchDTO {

    @ApiModelProperty(value = "id", example = "1111111111")
    private Long baseId;

    @ApiModelProperty(value = "加油站名称", example = "测试加油站")
    private String stationName;

    @ApiModelProperty(value = "经度")
    @NotNull(message = "经度不能为空")
    private Double lon;

    @ApiModelProperty(value = "纬度")
    @NotNull(message = "纬度不能为空")
    private Double lat;



    @ApiModelProperty(value = "查找半径")
    private int radius;

    @ApiModelProperty(value = "page", example = "1")
    private Integer page;

    @ApiModelProperty(value = "pageSize", example = "100")
    private Integer pageSize;
}
java 复制代码
 @PostMapping("/nearby")
    @ApiOperation(value = "查询附近加油站")
    public Response<ChargingStationVO> nearbySearch(@RequestBody @Valid @Validated ChargingStationNearbySearchDTO searchDTO) {
        String fieldName = "location";
        // NativeSearchQuery实现了SearchQuery接口
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        // 分页
        PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getPageSize());
        nativeSearchQueryBuilder.withPageable(pageRequest);
        // 定义bool查询
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        //https://blog.csdn.net/icanlove/article/details/126425788?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&utm_relevant_index=1
        //使用 minimum_should_match 选项,至少匹配一项should子句。
        if (StringUtils.isNotBlank(searchDTO.getStationName()) || ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {

            if (StringUtils.isNotBlank(searchDTO.getStationName())) {
                // //左右模糊查询,其中fuzziness的参数作用是在查询时,es动态的将查询关键词前后增加或者删除一个词,然后进行匹配
                QueryBuilder queryBuilder = QueryBuilders.fuzzyQuery("stationName", searchDTO.getStationName()).fuzziness(Fuzziness.ONE);
                boolQueryBuilder.must(queryBuilder);
            }
            if (ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {
                // //关键字不支持分词
                QueryBuilder queryBuilder = QueryBuilders.termQuery("baseId", searchDTO.getBaseId());
                boolQueryBuilder.must(queryBuilder);
            }
        }
        // geo查询,定义中心点,指定查询范围
        GeoDistanceQueryBuilder geoDistanceQueryBuilder = new GeoDistanceQueryBuilder(fieldName);
        geoDistanceQueryBuilder.point(searchDTO.getLat(), searchDTO.getLon());
        geoDistanceQueryBuilder.distance(searchDTO.getRadius(), DistanceUnit.METERS);
        boolQueryBuilder.must(geoDistanceQueryBuilder);

        //     外部 bool 过滤器
        BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
        queryBuilder.filter(boolQueryBuilder);
        nativeSearchQueryBuilder.withQuery(queryBuilder);


        // 按照距离升序
        GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder(fieldName, searchDTO.getLat(), searchDTO.getLon());
        geoDistanceSortBuilder.unit(DistanceUnit.METERS); //距离单位
        geoDistanceSortBuilder.order(SortOrder.ASC); //升序

        nativeSearchQueryBuilder.withSort(geoDistanceSortBuilder);
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        DslLogUtil.log(elasticsearchOperations, nativeSearchQuery);
        SearchHits<ChargingStationDTO> searchHits = elasticsearchOperations.search(nativeSearchQuery, ChargingStationDTO.class);
        log.info("响应数据:{}", LogUtil.getLogJson(searchHits));
        List<ChargingStationDTO> chargingStationDTOList = null;
        if (CollectionUtil.isNotEmpty(searchHits.getSearchHits())) {
            chargingStationDTOList = searchHits.getSearchHits().stream().map(o -> {
                // 计算两点距离
                //关于GeoDistance.ARC和GeoDistance.PLANE,前者比后者计算起来要慢,但精确度要比后者高,具体区别可以看。
                double distance = GeoDistance.ARC.calculate(o.getContent().getLocation().getLat(), o.getContent().getLocation().getLon(), searchDTO.getLat(), searchDTO.getLon(), DistanceUnit.KILOMETERS);
                ChargingStationDTO chargingStationDTO = o.getContent();
                chargingStationDTO.setDistance(distance);
                return chargingStationDTO;
            }).collect(Collectors.toList());
        }
        int count = CollectionUtils.isEmpty(chargingStationDTOList) ? 0 : chargingStationDTOList.size();
        return Response.success(ChargingStationVO.builder().
                positions(chargingStationDTOList).
                count(count).
                build());
    }

2.3 查询附近加油站( geo_bounding_box 矩形查询)

geo_bounding_box语法又称为地理坐标盒模型,在当前语法中,只需选择一个矩阵范围(输入矩阵的左上角的顶点地理坐标和矩阵的右上角的顶点地理坐标,构建成为一个矩阵),即可计算出当前矩阵中符合条件的元素;

java 复制代码
/**
     * 给定两个坐标,通过这两个坐标形成对角线,
     * 平行于地球经纬度从而得到的一个矩阵。
     * 采用geo_bounding_box语法可以得到坐落于当前矩阵中的元素的信息;
     *
     * @param searchDTO
     * @return
     */
    @PostMapping("/box/query")
    @ApiOperation(value = "矩形查询附近加油站")
    public Response<ChargingStationVO> boxQuery(@RequestBody @Valid @Validated ChargingStationSearchDTO searchDTO) {
        // NativeSearchQuery实现了SearchQuery接口
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        // 分页
        PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getPageSize());
        nativeSearchQueryBuilder.withPageable(pageRequest);
        // 定义bool查询
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        //https://blog.csdn.net/icanlove/article/details/126425788?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&utm_relevant_index=1
        //使用 minimum_should_match 选项,至少匹配一项should子句。
        if (StringUtils.isNotBlank(searchDTO.getStationName()) || ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {

            if (StringUtils.isNotBlank(searchDTO.getStationName())) {
                // //左右模糊查询,其中fuzziness的参数作用是在查询时,es动态的将查询关键词前后增加或者删除一个词,然后进行匹配
                QueryBuilder queryBuilder = QueryBuilders.fuzzyQuery("stationName", searchDTO.getStationName()).fuzziness(Fuzziness.ONE);
                boolQueryBuilder.must(queryBuilder);
            }
            if (ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {
                // //关键字不支持分词
                QueryBuilder queryBuilder = QueryBuilders.termQuery("baseId", searchDTO.getBaseId());
                boolQueryBuilder.must(queryBuilder);
            }
        }

        //给定两个坐标,通过这两个坐标形成对角线,
        // 平行于地球经纬度从而得到的一个矩阵。
        // 采用geo_bounding_box语法可以得到坐落于当前矩阵中的元素的信息;
        // 构造左上点坐标
        GeoPoint topLeft = new GeoPoint(searchDTO.getPositions().get(0).getLat(), searchDTO.getPositions().get(0).getLon());
        // 构造右下点坐标
        GeoPoint bottomRight = new GeoPoint(searchDTO.getPositions().get(1).getLat(), searchDTO.getPositions().get(1).getLon());
        GeoBoundingBoxQueryBuilder geoBoundingBoxQueryBuilder = new GeoBoundingBoxQueryBuilder("location")
                .setCorners(topLeft, bottomRight);

        boolQueryBuilder.must(geoBoundingBoxQueryBuilder);

        //     外部 bool 过滤器
        BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
        queryBuilder.filter(boolQueryBuilder);
        nativeSearchQueryBuilder.withQuery(queryBuilder);


        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        DslLogUtil.log(elasticsearchOperations, nativeSearchQuery);
        SearchHits<ChargingStationDTO> searchHits = elasticsearchOperations.search(nativeSearchQuery, ChargingStationDTO.class);
        log.info("响应数据:{}", LogUtil.getLogJson(searchHits));
        List<ChargingStationDTO> chargingStationDTOList = null;
        if (CollectionUtil.isNotEmpty(searchHits.getSearchHits())) {
            chargingStationDTOList = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
        }
        int count = CollectionUtils.isEmpty(chargingStationDTOList) ? 0 : chargingStationDTOList.size();
        return Response.success(ChargingStationVO.builder().
                positions(chargingStationDTOList).
                count(count).
                build());
    }

DSL

请求体:

java 复制代码
{
	"from": 0,
	"size": 100,
	"query": {
		"bool": {
			"filter": [{
				"bool": {
					"must": [{
						"fuzzy": {
							"stationName": {
								"value": "第三十",
								"fuzziness": "1",
								"prefix_length": 0,
								"max_expansions": 50,
								"transpositions": true,
								"boost": 1.0
							}
						}
					}, {
						"geo_bounding_box": {
							"location": {
								"top_left": [120.91224, 30.84623],
								"bottom_right": [120.93743, 30.8245]
							},
							"validation_method": "STRICT",
							"type": "MEMORY",
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}],
					"adjust_pure_negative": true,
					"boost": 1.0
				}
			}],
			"adjust_pure_negative": true,
			"boost": 1.0
		}
	},
	"version": true,
	"explain": false
}

响应体:

java 复制代码
{
  "code": 200,
  "message": "成功",
  "data": {
    "count": 2,
    "positions": [
      {
        "baseId": 431843,
        "stationId": "431843",
        "operatorId": "431843",
        "stationName": "好好",
        "operatorName": null,
        "location": {
          "lat": 30.833,
          "lon": 120.934,
          "geohash": "wtmzruvrnry1",
          "fragment": true
        },
        "address": "地址431843",
        "distance": 0
      },
      {
        "baseId": 114960,
        "stationId": "114960",
        "operatorId": "114960",
        "stationName": "第三十",
        "operatorName": null,
        "location": {
          "lat": 30.84,
          "lon": 120.919,
          "geohash": "wtmzrw680btm",
          "fragment": true
        },
        "address": "地址114960",
        "distance": 0
      }
    ]
  },
  "extraData": {}
}

2.4 多边形查询附近加油站(geo-polygon-多边形查询)

ES的geo_polygon语法,可以通过指定多个坐标点,从而构成一个多边形,然后从当前多边形中召回坐落其中的元素进行召回;在当前语法中,最少需要3个坐标,从而构成一个多边形;

java 复制代码
  @PostMapping("/polygon/query")
    @ApiOperation(value = "多边形查询附近加油站")
    public Response<ChargingStationVO> polygonQuery(@RequestBody @Valid @Validated ChargingStationSearchDTO searchDTO) throws IOException {
        // NativeSearchQuery实现了SearchQuery接口
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        // 分页
        PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getPageSize());
        nativeSearchQueryBuilder.withPageable(pageRequest);
        // 定义bool查询
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        //https://blog.csdn.net/icanlove/article/details/126425788?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&utm_relevant_index=1
        //使用 minimum_should_match 选项,至少匹配一项should子句。
        if (StringUtils.isNotBlank(searchDTO.getStationName()) || ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {

            if (StringUtils.isNotBlank(searchDTO.getStationName())) {
                // //左右模糊查询,其中fuzziness的参数作用是在查询时,es动态的将查询关键词前后增加或者删除一个词,然后进行匹配
                QueryBuilder queryBuilder = QueryBuilders.fuzzyQuery("stationName", searchDTO.getStationName()).fuzziness(Fuzziness.ONE);
                boolQueryBuilder.must(queryBuilder);
            }
            if (ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {
                // //关键字不支持分词
                QueryBuilder queryBuilder = QueryBuilders.termQuery("baseId", searchDTO.getBaseId());
                boolQueryBuilder.must(queryBuilder);
            }
        }

        //可以通过指定多个坐标点,从而构成一个多边形,
        //然后从当前多边形中召回坐落其中的元素进行召回;
        //在当前语法中,最少需要3个坐标,从而构成一个多边形;

        // 创建多边形几何对象
        CoordinatesBuilder coordinatesBuilder = new CoordinatesBuilder();
        for (GpsListDTO gpsListDTO : searchDTO.getPositions()) {
            coordinatesBuilder.coordinate(gpsListDTO.getLon(), gpsListDTO.getLat());
        }

        PolygonBuilder pb = new PolygonBuilder(coordinatesBuilder);
        GeoShapeQueryBuilder geoShapeQueryBuilder = QueryBuilders.geoShapeQuery("location", pb.buildGeometry());
        // intersects - 查询的形状与索引的形状有重叠(默认), 即图形有交集则匹配。
        //disjoint - 查询的形状与索引的形状完全不重叠。
        //within - 查询的形状包含索引的形状。
        //CONTAINS将返回其geo_shape字段包含查询中指定的几何形状的所有文档。
        //within与CONTAINS的区别
        // 它们是反比关系:A包含B,B在A内.
        // `A`是查询中的形状,而`B`是文档中的形状。
        //`WITHIN`表示`A包含B`   A.contains(B) True
        // `CONTAINS`表示`B包含A`  B.within(A)  True
        geoShapeQueryBuilder.relation(ShapeRelation.INTERSECTS);
        boolQueryBuilder.must(geoShapeQueryBuilder);

        //     外部 bool 过滤器
        BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
        queryBuilder.filter(boolQueryBuilder);
        nativeSearchQueryBuilder.withQuery(queryBuilder);


        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        DslLogUtil.log(elasticsearchOperations, nativeSearchQuery);
        SearchHits<ChargingStationDTO> searchHits = elasticsearchOperations.search(nativeSearchQuery, ChargingStationDTO.class);
        log.info("响应数据:{}", LogUtil.getLogJson(searchHits));
        List<ChargingStationDTO> chargingStationDTOList = null;
        if (CollectionUtil.isNotEmpty(searchHits.getSearchHits())) {
            chargingStationDTOList = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
        }
        int count = CollectionUtils.isEmpty(chargingStationDTOList) ? 0 : chargingStationDTOList.size();
        return Response.success(ChargingStationVO.builder().
                positions(chargingStationDTOList).
                count(count).
                build());
    }

DSL

请求体:

java 复制代码
{
	"from": 0,
	"size": 100,
	"query": {
		"bool": {
			"filter": [{
				"bool": {
					"must": [{
						"fuzzy": {
							"stationName": {
								"value": "好好",
								"fuzziness": "1",
								"prefix_length": 0,
								"max_expansions": 50,
								"transpositions": true,
								"boost": 1.0
							}
						}
					}, {
						"geo_shape": {
							"location": {
								"shape": {
									"type": "Polygon",
									"coordinates": [
										[
											[120.92696, 30.83932],
											[120.91964, 30.82868],
											[120.95907, 30.81838],
											[120.96842, 30.83525],
											[120.94369, 30.84345],
											[120.92696, 30.83932]
										]
									]
								},
								"relation": "intersects"
							},
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}],
					"adjust_pure_negative": true,
					"boost": 1.0
				}
			}],
			"adjust_pure_negative": true,
			"boost": 1.0
		}
	},
	"version": true,
	"explain": false
}

响应体:

java 复制代码
{
	"empty": false,
	"maxScore": 0.0,
	"searchHits": [{
		"content": {
			"address": "地址431843",
			"baseId": 431843,
			"distance": 0.0,
			"location": {
				"fragment": true,
				"geohash": "wtmzruvrnry1",
				"lat": 30.833,
				"lon": 120.934
			},
			"operatorId": "431843",
			"stationId": "431843",
			"stationName": "好好"
		},
		"highlightFields": {},
		"id": "431843",
		"index": "stationcc",
		"innerHits": {},
		"matchedQueries": [],
		"score": 0.0,
		"sortValues": []
	}],
	"totalHits": 1,
	"totalHitsRelation": "EQUAL_TO"
}

2.5 查询沿途加油站(一次查询多个圆点)

java 复制代码
   @PostMapping("/route")
    @ApiOperation(value = "查询沿途加油站")
    public Response<ChargingStationVO> routeSearch(@RequestBody @Valid @Validated ChargingStationSearchDTO searchDTO) {
        String fieldName = "location";
        // NativeSearchQuery实现了SearchQuery接口
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        // 分页
        PageRequest pageRequest = PageRequest.of(searchDTO.getPage() - 1, searchDTO.getPageSize());
        nativeSearchQueryBuilder.withPageable(pageRequest);
        // 定义bool查询
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        //https://blog.csdn.net/icanlove/article/details/126425788?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-126425788-blog-120678401.235%5Ev38%5Epc_relevant_default_base&utm_relevant_index=1
        //使用 minimum_should_match 选项,至少匹配一项should子句。
        boolQueryBuilder.minimumShouldMatch(1);
        if (StringUtils.isNotBlank(searchDTO.getStationName()) || ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {
            if (StringUtils.isNotBlank(searchDTO.getStationName())) {
                // //左右模糊查询,其中fuzziness的参数作用是在查询时,es动态的将查询关键词前后增加或者删除一个词,然后进行匹配
                QueryBuilder queryBuilder = QueryBuilders.fuzzyQuery("stationName", searchDTO.getStationName()).fuzziness(Fuzziness.ONE);
                boolQueryBuilder.must(queryBuilder);
            }
            if (ObjectUtils.isNotEmpty(searchDTO.getBaseId())) {
                // //关键字不支持分词
                QueryBuilder queryBuilder = QueryBuilders.termQuery("baseId", searchDTO.getBaseId());
                boolQueryBuilder.must(queryBuilder);
            }
        }
        if (CollectionUtil.isNotEmpty(searchDTO.getPositions())) {
            boolQueryBuilder.minimumShouldMatch(1);
            for (GpsListDTO position : searchDTO.getPositions()) {
                // geo查询,定义中心点,指定查询范围
                GeoDistanceQueryBuilder geoDistanceQueryBuilder = new GeoDistanceQueryBuilder(fieldName);
                geoDistanceQueryBuilder.point(position.getLat(), position.getLon());
                geoDistanceQueryBuilder.distance(searchDTO.getRadius(), DistanceUnit.METERS);
                boolQueryBuilder.should(geoDistanceQueryBuilder);
            }
        }
        //     外部 bool 过滤器
//        Elasticsearch 查询条件和过滤条件的区别?
//        Elasticsearch中的查询条件和过滤条件都是用于搜索和过滤文档的条件,但它们之间有一些区别。
//        查询条件是用于计算文档相关度得分的条件,它会将所有符合条件的文档按照相关度得分从高到低排序,并返回前N个文档。查询条件可以使用各种类型的查询,如match、term、range、bool等。查询条件会计算每个文档的相关度得分,因此查询条件可以用于搜索和排序。
//        过滤条件是用于过滤文档的条件,它会将所有符合条件的文档返回,但不会计算相关度得分。过滤条件可以使用各种类型的过滤器,如term、range、bool、geo_distance等。过滤条件不会计算相关度得分,因此过滤条件可以用于过滤和聚合。
//        查询条件和过滤条件的区别在于,查询条件会计算每个文档的相关度得分,而过滤条件不会计算得分。因此,如果只需要过滤文档而不需要计算得分,应该使用过滤条件。另外,过滤条件可以缓存结果,提高查询性能,而查询条件不能缓存结果。
//        需要注意的是,查询条件和过滤条件都可以使用bool查询和bool过滤器来组合多个条件。bool查询和bool过滤器都是用于组合多个查询或过滤器的逻辑运算符,可以使用must、should、must_not三个子句来组合多个查询或过滤器。
        BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
        queryBuilder.filter(boolQueryBuilder);

        nativeSearchQueryBuilder.withQuery(queryBuilder);

        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
        DslLogUtil.log(elasticsearchOperations, nativeSearchQuery);
        SearchHits<ChargingStationDTO> searchHits = elasticsearchOperations.search(nativeSearchQuery, ChargingStationDTO.class);
        log.info("响应数据:{}", LogUtil.getLogJson(searchHits));
        List<ChargingStationDTO> chargingStationDTOList = null;
        if (CollectionUtil.isNotEmpty(searchHits.getSearchHits())) {
            chargingStationDTOList = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
        }
        int count = CollectionUtils.isEmpty(chargingStationDTOList) ? 0 : chargingStationDTOList.size();
        return Response.success(ChargingStationVO.builder().
                positions(chargingStationDTOList).
                count(count).
                build());
    }

请求DSL语句:

java 复制代码
{
	"from": 0,
	"size": 10000,
	"query": {
		"bool": {
			"filter": [{
				"bool": {
					"must": [{
						"fuzzy": {
							"stationName": {
								"value": "王五",
								"fuzziness": "1",
								"prefix_length": 0,
								"max_expansions": 50,
								"transpositions": true,
								"boost": 1.0
							}
						}
					}],
					"should": [{
						"geo_distance": {
							"location": [114.7, 31.0],
							"distance": 10000.0,
							"distance_type": "arc",
							"validation_method": "STRICT",
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}, {
						"geo_distance": {
							"location": [116.935, 31.0],
							"distance": 10000.0,
							"distance_type": "arc",
							"validation_method": "STRICT",
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}, {
						"geo_distance": {
							"location": [117.261, 31.0],
							"distance": 10000.0,
							"distance_type": "arc",
							"validation_method": "STRICT",
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}, {
						"geo_distance": {
							"location": [116.569, 31.0],
							"distance": 10000.0,
							"distance_type": "arc",
							"validation_method": "STRICT",
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}, {
						"geo_distance": {
							"location": [117.639, 31.0],
							"distance": 10000.0,
							"distance_type": "arc",
							"validation_method": "STRICT",
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}, {
						"geo_distance": {
							"location": [119.236, 31.0],
							"distance": 10000.0,
							"distance_type": "arc",
							"validation_method": "STRICT",
							"ignore_unmapped": false,
							"boost": 1.0
						}
					}],
					"adjust_pure_negative": true,
					"minimum_should_match": "1",
					"boost": 1.0
				}
			}],
			"adjust_pure_negative": true,
			"boost": 1.0
		}
	},
	"version": true,
	"explain": false
}

响应数据:

java 复制代码
{
	"empty": false,
	"maxScore": 0.0,
	"searchHits": [{
		"content": {
			"address": "地址4031",
			"baseId": 4031,
			"distance": 0.0,
			"location": {
				"fragment": true,
				"geohash": "wtkzbygzuwxz",
				"lat": 30.932,
				"lon": 119.218
			},
			"operatorId": "4031",
			"stationId": "4031",
			"stationName": "王五"
		},
		"highlightFields": {},
		"id": "4031",
		"index": "stationcc",
		"innerHits": {},
		"matchedQueries": [],
		"score": 0.0,
		"sortValues": []
	}, {
		"content": {
			"address": "地址26708",
			"baseId": 26708,
			"distance": 0.0,
			"location": {
				"fragment": true,
				"geohash": "wte2df6z32vx",
				"lat": 31.039,
				"lon": 117.195
			},
			"operatorId": "26708",
			"stationId": "26708",
			"stationName": "王五"
		},
		"highlightFields": {},
		"id": "26708",
		"index": "stationcc",
		"innerHits": {},
		"matchedQueries": [],
		"score": 0.0,
		"sortValues": []
	}, {
		"content": {
			"address": "地址156487",
			"baseId": 156487,
			"distance": 0.0,
			"location": {
				"fragment": true,
				"geohash": "wt988d3zmbcx",
				"lat": 31.039,
				"lon": 114.634
			},
			"operatorId": "156487",
			"stationId": "156487",
			"stationName": "王五"
		},
		"highlightFields": {},
		"id": "156487",
		"index": "stationcc",
		"innerHits": {},
		"matchedQueries": [],
		"score": 0.0,
		"sortValues": []
	}, {
		"content": {
			"address": "地址131631",
			"baseId": 131631,
			"distance": 0.0,
			"location": {
				"fragment": true,
				"geohash": "wtdb78u6echc",
				"lat": 30.986,
				"lon": 116.527
			},
			"operatorId": "131631",
			"stationId": "131631",
			"stationName": "王五"
		},
		"highlightFields": {},
		"id": "131631",
		"index": "stationcc",
		"innerHits": {},
		"matchedQueries": [],
		"score": 0.0,
		"sortValues": []
	}, {
		"content": {
			"address": "地址265815",
			"baseId": 265815,
			"distance": 0.0,
			"location": {
				"fragment": true,
				"geohash": "wte8ks47qs3x",
				"lat": 31.004,
				"lon": 117.623
			},
			"operatorId": "265815",
			"stationId": "265815",
			"stationName": "王五"
		},
		"highlightFields": {},
		"id": "265815",
		"index": "stationcc",
		"innerHits": {},
		"matchedQueries": [],
		"score": 0.0,
		"sortValues": []
	}],
	"totalHits": 16,
	"totalHitsRelation": "EQUAL_TO"
}

打印完整DSL语句工具类

java 复制代码
@Slf4j
public class DslLogUtil {

    public static void log(ElasticsearchOperations elasticsearchOperations, NativeSearchQuery nativeSearchQuery) {
        if (elasticsearchOperations instanceof ElasticsearchRestTemplate) {
            try {
                ElasticsearchRestTemplate elasticsearchRestTemplate = (ElasticsearchRestTemplate) elasticsearchOperations;
                Method searchRequest = ReflectionUtils.findMethod(Class.forName("org.springframework.data.elasticsearch.core.RequestFactory"), "searchRequest", Query.class, Class.class, IndexCoordinates.class);
                searchRequest.setAccessible(true);
                Object o = ReflectionUtils.invokeMethod(searchRequest, elasticsearchRestTemplate.getRequestFactory(), nativeSearchQuery, ChargingStationDTO.class, elasticsearchRestTemplate.getIndexCoordinatesFor(ChargingStationDTO.class));
                Field source =ReflectionUtils.findField(Class.forName("org.elasticsearch.action.search.SearchRequest"), "source");
                source.setAccessible(true);
                Object s = ReflectionUtils.getField(source, o);
                log.info("请求DSL语句:{}", s);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

    }


}

参考:
https://www.kancloud.cn/yiyanan/elasticsearch_7_6/1670492

https://www.kancloud.cn/apachecn/elasticsearch-doc-zh/1945207

https://learnku.com/docs/elasticsearch73/7.3/5210-geo-distance-aggregation/8043

相关推荐
Data 3171 小时前
Hive数仓操作(十一)
大数据·数据库·数据仓库·hive·hadoop
qtj-0012 小时前
普通人在刚开始做副业时要注意哪些细节?
大数据·微信·新媒体运营·创业创新
知识分享小能手3 小时前
mysql学习教程,从入门到精通,SQL 修改表(ALTER TABLE 语句)(29)
大数据·开发语言·数据库·sql·学习·mysql·数据分析
a6953188_3 小时前
如何评估一个副业项目的可行性?
大数据·微信·创业创新
州周3 小时前
Flink一点整理
大数据·flink
柚乐果果3 小时前
数据分析实战简例
java·大数据·python
Data 3174 小时前
Hive数仓操作(九)
大数据·数据仓库·hive·hadoop
丶21364 小时前
【大数据】Elasticsearch 实战应用总结
大数据·elasticsearch·搜索引擎
闲人编程5 小时前
elasticsearch实战应用
大数据·python·elasticsearch·实战应用
Data 3178 小时前
Hive数仓操作(三)
大数据·数据库·数据仓库·hive·hadoop