优点:Redis提供了地理空间索引功能,可以通过Geo数据类型进行地理位置查询。这使得Redis在处理地理位置查询时非常高效。
缺点:
Redis的地理空间索引功能相对简单,只能支持二维平面坐标系(经纬度)的查询,对于三维坐标系或者不规则地理区域的查询支持不够好。
功能有限:Redis的地理位置查询功能相对简单,仅支持基本的距离计算、范围查询等操作,无法满足复杂的空间查询需求。
存储容量限制:由于Redis数据存储在内存中,其存储容量受限于物理内存大小,对于大规模地理位置数据,可能需要进行分片或其他优化策略。
扩展性受限:Redis对于数据的扩展能力有限,不如Elasticsearch那样容易水平扩展以适应规模的增长。
使用场景:适用于需要快速查询地理位置信息的场景,小型应用,并且对于快速插入和查询地理位置数据有较高的实时性要求,可以考虑使用Redis geo。
优点:
灵活性好:MongoDB支持多种地理位置查询操作,包括点查询、范围查询和多边形查询等。
数据结构简单:MongoDB的文档型结构非常适合存储地理位置数据,容易理解和使用。
高可用性:MongoDB提供了复制集和分片等机制来确保数据的高可用性和扩展性。
然而,MongoDB + 2d索引实现地理位置查询也存在一些缺点:
性能相对较差:相比Elasticsearch,在处理大规模的地理位置查询时,MongoDB的性能可能会受到限制。
功能相对简单:MongoDB的地理位置查询功能较为基础,相比Elasticsearch可能缺乏某些高级查询功能。
不支持部分地理位置操作:例如,MongoDB不支持直接计算两个地理位置之间的距离。
使用Elasticsearch geo实现地理位置查询的优点:
高性能:Elasticsearch是一种搜索引擎,使用geo点的经纬度数据可以快速进行空间查询和过滤,具有较高的查询效率。
灵活性:Elasticsearch提供了丰富的地理位置查询功能,例如可以根据距离、范围及其他条件进行查询和排序。
可扩展性:Elasticsearch可以通过分片和副本来实现水平扩展,以应对大规模的地理位置数据查询需求。
使用Elasticsearch geo实现地理位置查询的缺点:
学习成本:学习和配置Elasticsearch需要花费一定的时间和精力。
依赖性:使用Elasticsearch需要安装和维护Elasticsearch服务,这可能增加系统依赖和部署复杂性。
数据存储限制:Elasticsearch适用于小到中等大小的数据集,对于大量地理位置数据,可能需要额外的硬件资源和优化工作。
Elasticsearch为用户提供了基于地理位置的搜索功能。它主要支持两种类型的地理查询:一种是地理点(geo_point),即经纬度查询,另一种是地理形状查询(geo_shape),即支持点、线、圆形和多边形查询等
在Elasticsearch中,存在两种地理位置数据类型:geo_point和geo_shape。
geo_point:这是最基本的地理位置类型,通常用于表示一个二维坐标点(经度和纬度)。可以计算落在某个矩形内的点、以某个点为半径(圆)的点、某个多边形内的点等。此外,geo_point还可以用于排序、聚合等操作。
geo_shape:这种数据类型表示一个复杂的图形,使用的是GeoJSON的格式。它可以表达一块地理区域,区域的形状可以是任意多边形,也可以是点、线、面、多点、多线、多面等几何类型。然而,这种数据类型不能进行排序操作。
elasticsearch 版本7.12.1
<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>
配置文件
# 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
配置类
@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;
}
}
测试实体类
@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;
}
初始化数据
@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);
}
}
}
请求参数
@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;
}
@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());
}
geo_bounding_box语法又称为地理坐标盒模型,在当前语法中,只需选择一个矩阵范围(输入矩阵的左上角的顶点地理坐标和矩阵的右上角的顶点地理坐标,构建成为一个矩阵),即可计算出当前矩阵中符合条件的元素;
/**
* 给定两个坐标,通过这两个坐标形成对角线,
* 平行于地球经纬度从而得到的一个矩阵。
* 采用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
请求体:
{
"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
}
响应体:
{
"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": {}
}
ES的geo_polygon语法,可以通过指定多个坐标点,从而构成一个多边形,然后从当前多边形中召回坐落其中的元素进行召回;在当前语法中,最少需要3个坐标,从而构成一个多边形;
@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
请求体:
{
"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
}
响应体:
{
"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"
}
@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语句:
{
"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
}
响应数据:
{
"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语句工具类
@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