GeoHash是一种地理坐标编码系统,可以将地理位置按照一定的规则转换为字符串,以方便对地理位置信息建立空间索引。首先要明确的是,GeoHash代表的不是一个点而是一个区域。GeoHash具有两个显著的特点:一是通过改变 GeoHash的长度,我们可以表示任意精度的位置:GeoHash越短,其表示的区域越大,位置精度越低;相反,GeoHash越长,其表示的区域越小,位置精度越高。Elasticsearch 支持GeoHash字符串长度是12,这个精度已经达到了厘米级别;二是如果不同位置的GeoHash字符串前缀相同,那他们一定在同一个区域中。
GeoHash将地理坐标编码为字符串的方法与二分查找有几分相识。它将经纬度范围一分为二,分成左右两个区间;坐标落入左区间为0,落入右区间为1。按照此方法,分别对精度和维度不停地地柜逼近实际坐标值,就会得到两组由0,1组成的数字字符串。然后按照偶数位放经度,奇数位放纬度的方法,将两组数据串联组合起来。最后将组合的数字串转换成十进制,并用Base32对数字进行编码就得到GeoHash最终编码了。
根据经纬度的定义,经度整体范围为: [ -180 , 180 ] ,纬度整体范围为:[ -90 , 90 ]。所以第一次递归分割坐标后,经度的左右区间为 **[ -180 , 0 ) **和
[ 0 , 180 ],即西半球和东半球 纬度的左右区间为 **[ -90 , 0 ) 和 [ 0 , 90 ],即南半球和北半球。以山东济南某地坐标( 117.15838 , 36.74038 )**为例,经纬度第一次递归后都落入到右区间,则他们第一位数字都是1。由于落入了右区间,所以经纬第二次递归就是针对右区间分割:经度为 **[ 0 , 90 ) **和 [ 90 , 180 ],纬度为
[ 0 , 45 ) 和 [ 45 , 90 ] 。这次经度落入到了右区间,二纬度落入了左区间,所以他们的第二位数字分别为1和0。按照此方法不停的递归下去,就能无线的接近于实际坐标。
可以看出,GeoHash时间上是将地球假设为一个平面,然后在这个平面上划分网格的过程。在这个递归的过程中,每一次都是将整个区域一分为二,区域面积也就约分越小,而且在相同区域的位置他们的前缀数字一定是相同的。表1-1列出了经度 117.15838经过十次递归运算过程及结果。
表1-1 GeoHash经度 117.15838
次数 | 经度范围 | 左区间 - 0 | 右区间 - 1 | 117.15838 |
---|---|---|---|---|
1 | [ -180 , 180 ] | [ -180 , 0 ) | [ 0 , 180 ] | 1 |
2 | [ 0 , 180 ] | [ 0 , 90 ) | [ 90 , 180 ] | 1 |
3 | [ 90 , 180 ] | [90 , 135 ) | [ 135 , 180 ] | 0 |
4 | [ 90 , 135 ) | [ 90, 112.5 ) | [ 112.5 , 135 ) | 1 |
5 | [ 112.5 , 135 ) | [ 112.5 , 123.75 ) | [ 123.75 , 135 ) | 0 |
6 | [ 112.5 , 123.75 ) | [ 112.5 , 118.125 ) | [ 118.125 , 123.75) | 0 |
7 | [ 112.5 , 118.125 ) | [ 112.5 , 115.3125 ) | [ 115.3125 , 118.125) | 1 |
8 | [ 115.3125 , 118.125) | [ 115.3125 , 116.71875 ) | [ 116.71875 , 118.125 ) | 1 |
9 | [ 116.71875 , 118.125 ) | [ 116.71875 , 117.421875 ) | [ 117.421875 , 118.125 ) | 0 |
10 | [ 116.71875 , 117.421875 ) | [ 116.71875 ,117.0703125 ) | [ 117.0703125, 117.421875 ) | 1 |
经过10次递归,经度为117.15838得到的数字字符串为1101001101,以同样的方式对纬度做GeoHash运算,表1-2列出了纬度 36.74038经过十次递归运算过程及结果。
表1-2 GeoHash纬度 36.74038
次数 | 经度范围 | 左区间 - 0 | 右区间 - 1 | 36.74038 |
---|---|---|---|---|
1 | [ -90, 90] | [ -90 , 0 ) | [ 0 , 90] | 1 |
2 | [ 0 , 90] | [ 0 , 45) | [ 45, 90] | 0 |
3 | [ 0 , 45) | [0, 22.5) | [ 22.5, 45 ) | 1 |
4 | [ 22.5, 45) | [ 22.5, 33.75 ) | [ 33.75 , 45) | 1 |
5 | [ 33.75 , 45) | [ 33.75, 39.375 ) | [ 39.375 , 45 ) | 0 |
6 | [ 33.75, 39.375 ) | [ 33.75, 36.5625 ) | [ 36.5625 , 39.375) | 1 |
7 | [ 36.5625 , 39.375) | [ 36.5625 , 37.96875) | [ 37.96875 , 39.375) | 0 |
8 | [ 36.5625 , 37.96875) | [ 36.5625 , 37.265625 ) | [ 37.265625 , 37.96875 ) | 0 |
9 | [ 36.5625 , 37.265625 ) | [ 36.5625, 36.9140625 ) | [ 36.9140625 , 37.265625 ) | 0 |
10 | [ 36.5625, 36.9140625 ) | [ 36.5625 ,36.7385625 ) | [ 36.7385625 , 36.9140625 ) | 1 |
同样经过十次递归,纬度得到的数字串为1011010001 ,下面按偶数位经度,奇数位纬度的方法合并两个数字串,注意位数是从0开始,或者理解为先经度后纬度,各区一位交叉合并。
经度:1101001101
纬度:1011010001
合并:11100 11100 01101 00011
合并后结果按照每五位一组,一次转换成十进制数 28、28、13、3,并使用表1-3对应的Base32编码转为字符串。
最终结果为wwe34,可以到 http://geohash.org/ 网站上输入"36.74038,117.15838" 验证结果为:wwe3402hhp5z 这个结果为12位,说明做了30次递归。
表1-4列出了GeoHash字符串长度与实际位置误差关系。
表 1- 4 GeoHash字符串长度与实际位置关系
GeoHash长度 | 纬度位数 | 经度位数 | 纬度误差 | 经度误差 | 区域面积 |
---|---|---|---|---|---|
1 | 2 | 3 | ±23 | ±23 | 5009.4km*4992.6km |
2 | 5 | 5 | ±2.8 | ±5.6 | 1252.3km*624.1km |
3 | 7 | 8 | ±0.70 | ±0.70 | 156.5km*156km |
4 | 10 | 10 | ±0.087 | ±0.18 | 39.1km*19.5km |
5 | 12 | 13 | ±0.022 | ±0.022 | 4.9km*4.9km |
6 | 15 | 15 | ±0.0027 | ±0.0055 | 1.2km*609.4m |
7 | 17 | 18 | ±0.00068 | ±0.00068 | 152.9m*152.4m |
8 | 20 | 20 | ±0.000085 | ±0.00017 | 38.2m*19m |
9 | 22 | 23 | ±0.000021 | ±0.000021 | 4.8m*4.8m |
10 | 25 | 25 | ±0.00000268 | ±0.00000536 | 1.2m*59.5cm |
11 | 27 | 28 | ±0.00000067 | ±0.00000067 | 4.9cm*14.9cm |
12 | 30 | 30 | ±0.00000008 | ±0.00000017 | 3.7cm*1.9cm |
根据图1-4表示,当GeoHash字符串长度达到12位时,位置经度误差可以控制在不到8cm²的范围内。
Elasticsearch 提供了两种地理相关的数据类型geo_point和 geo_shape,前者用于保存地理位置,即具体的某一个坐标;而后者用于保存地理形状,如矩形和多边形。
地理位置由经度和维度共同定义,所以geo_point定义地理位置坐标最基本的形式也是通过提供经度和纬度来实现的。
如下:存储了数据的日期、主键、地理位置信息
PUT /obd
{
"mapping":{
"properties":{
"datadate": { //数据日期
"type": "date"
},
"id": { //数据主键
"type": "long"
},
"location": { //地理位置信息
"type": "geo_point"
},
"name": { //名称(编号)
"type": "text"
}
}
}
}
如上,location的数据类型为geo_point,这种类型有四种表示形式
例如:
# 格式1 对象
PUT /obd/_doc/1
{
"location": {
"lat": 41.12,
"lon": -71.34
}
}
# 格式2 数组
PUT /obd/_doc/2
{
"location": [ -71.34, 41.12 ]
}
# 格式3 字符串
PUT /obd/_doc/3
{
"location": "41.12,-71.34"
}
# 格式4 geohash
PUT /obd/_doc/4
{
"location": "wwe3402hhp5z"
}
按距离搜索
按距离排序
按时间范围
maven依赖
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.3version>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.microsoft.sqlservergroupId>
<artifactId>mssql-jdbcartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.5version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>dynamic-datasource-spring-boot-starterartifactId>
<version>2.5.4version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.16version>
dependency>
配置文件yml
spring:
elasticsearch:
rest:
uris: 192.168.254.190:9200,192.168.254.191:9200,192.168.254.192:9200
datasource:
druid:
stat-view-servlet:
# 默认true 内置监控页面首页/druid/index.html
enabled: false
initial-size: 20
max-active: 60
min-idle: 20
max-wait: 30000
dynamic:
primary: master
strict: false
datasource:
master: ***
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: ***
url:
jdc:
url: ***
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
username: sa
password: ***
数据库实体
@Data
@EqualsAndHashCode()
@TableName("carPosition")
public class CarPosition {
@TableId(value = "id")
private Long id;
@TableField("vehicleIdentificationNumber")
private String vehicleIdentificationNumber;
@TableField("collectionTime")
private LocalDateTime collectionTime;
/**
* 经度
*/
private String longitude;
/**
* 纬度
*/
private String latitude;
}
es实体
@Data
@FieldNameConstants
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "#{@dynamicIndex.getIndex()}", createIndex = false)
public class ObdModel implements Serializable {
@Id
private Long id;
@Field(type = FieldType.Text)
private String name;
@GeoPointField
private GeoPoint location;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern ="yyyy-MM-dd HH:mm:ss:SSS")
private LocalDateTime datadate;
}
将数据库数据存储到es
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sunfeng.elasticsearch.entity.CarPosition;
import com.sunfeng.elasticsearch.es.model.ObdModel;
import com.sunfeng.elasticsearch.es.repository.ObdModelRepository;
import com.sunfeng.elasticsearch.es.utils.DynamicIndex;
import com.sunfeng.elasticsearch.service.CarPositionService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.geo.GeoPoint;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.sunfeng.elasticsearch.es.utils.DateTimeFormatterUtil.yyyy_MM_dd_HH_mm_ss;
@Slf4j
@SpringBootTest
class ElasticsearchSpringdataApplicationTests {
@Resource
private ObdModelRepository obdModelRepository;
@Resource
private DynamicIndex dynamicIndex;
@Resource
private ElasticsearchRestTemplate restTemplate;
@Resource
private CarPositionService carPositionService;
@Test
public void createIndex() {
//生成索引名称
String obdIndex = "obd2";
Document mapping = restTemplate.indexOps(IndexCoordinates.of(obdIndex)).createMapping(ObdModel.class);
//判断索引库中是否存在该索引
if (!restTemplate.indexOps(IndexCoordinates.of(obdIndex)).exists()) {
restTemplate.indexOps(IndexCoordinates.of(obdIndex)).create();
//创建映射
restTemplate.indexOps(IndexCoordinates.of(obdIndex)).putMapping(mapping);
}
}
@Test
public void contextLoads() {
dynamicIndex.setIndex("obd");
Iterable<ObdModel> all = obdModelRepository.findAll();
all.forEach(System.out::println);
dynamicIndex.setIndex("");
}
//纬度正则
private static String latReg = "^[\\-\\+]?((0|([1-8]\\d?))(\\.\\d{1,6})?|90(\\.0{1,6})?)$";
//经度正则
private static String lonReg = "^[\\-\\+]?(0(\\.\\d{1,6})?|([1-9](\\d)?)(\\.\\d{1,6})?|1[0-7]\\d{1}(\\.\\d{1,6})?|180(\\.0{1,6})?)$";
public static void main(String[] args) {
System.out.println(LocalDateTime.parse("2023-08-04 19:00:00", yyyy_MM_dd_HH_mm_ss).toInstant(ZoneOffset.of("+8")).toEpochMilli());
}
@Test
public void dbToEs() {
log.info("开始统计!!");
LambdaQueryWrapper<CarPosition> lambdaQueryWrapper = new LambdaQueryWrapper<CarPosition>();
dynamicIndex.setIndex("obd202308");
List<CarPosition> carPositionList = carPositionService.list(lambdaQueryWrapper);
/**
* 获取分页数据
*/
log.info("需要处理的数据量为:{}", carPositionList.size());
carPositionList = carPositionList.parallelStream()
.filter(carPosition -> ObjectUtil.isNotEmpty(carPosition.getLatitude()) &&
ObjectUtil.isNotEmpty(carPosition.getLongitude()) &&
ReUtil.isMatch(latReg, carPosition.getLatitude()) &&
ReUtil.isMatch(lonReg, carPosition.getLongitude())).collect(Collectors.toList());
log.info("排除部分脏数据后为:{}", carPositionList.size());
ArrayList<ObdModel> obdModels = new ArrayList<>();
//批量保存提高效率
for (int i = 0; i < carPositionList.size(); i++) {
CarPosition carPosition = carPositionList.get(i);
ObdModel obdModel = new ObdModel();
obdModel.setId(carPosition.getId());
obdModel.setName(carPosition.getVehicleIdentificationNumber());
obdModel.setDatadate(carPosition.getCollectionTime());
GeoPoint geoPoint = new GeoPoint(Double.parseDouble(carPosition.getLatitude()), Double.parseDouble(carPosition.getLongitude()));
obdModel.setLocation(geoPoint);
obdModels.add(obdModel);
if (obdModels.size() % 10000 == 0) {
log.info("开始保存");
obdModelRepository.saveAll(obdModels);
log.info("结束保存");
obdModels.clear();
}
}
obdModelRepository.saveAll(obdModels);
}
}
es数据查询
package com.sunfeng.elasticsearch;
import com.sunfeng.elasticsearch.es.utils.DateTimeFormatterUtil;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.GeoDistanceQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Map;
import static com.sunfeng.elasticsearch.es.utils.DateTimeFormatterUtil.getDateTimeOfTimestamp;
import static com.sunfeng.elasticsearch.es.utils.DateTimeFormatterUtil.yyyy_MM_dd_HH_mm_ss;
@Slf4j
@SpringBootTest
public class TestSearchDocument {
@Resource
private RestHighLevelClient restHighLevelClient;
@Test
public void contextLoads() throws IOException {
//创建查询请求
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("obd202308");
GeoPoint geoPoint = new GeoPoint(36.508270D, 117.848530D);
//条件1、设置搜索半径
GeoDistanceQueryBuilder distanceQueryBuilder = QueryBuilders.geoDistanceQuery("location")
.point(geoPoint)
.distance(10000, DistanceUnit.METERS)
.geoDistance(GeoDistance.ARC); //设置查询精度
//条件3、设置搜索区间 时间区间
//注意: 时间格式要用 yyyy-MM-dd HH:mm:ss:SSS
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("datadate")
.gt("2023-08-04 19:00:00:000")
.lte("2023-08-04 20:00:00:000");
// 组合查询条件
BoolQueryBuilder must = QueryBuilders.boolQuery()
.must(rangeQueryBuilder)
.must(distanceQueryBuilder);
//条件2:按照距离排序
//构建GeoDistanceSortBuilder设置按距离排序参数
GeoDistanceSortBuilder sort = SortBuilders.geoDistanceSort("location", geoPoint);
//升序排序
sort.order(SortOrder.ASC);
//构建检索
SearchSourceBuilder sourceBuilder = SearchSourceBuilder
.searchSource()
.from(0)
.size(10000)
.query(must)
.sort(sort);
// 设置SearchRequest搜索参数
searchRequest.source(sourceBuilder);
log.info("开始搜索");
// 执行ES请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
log.info("结束搜索");
SearchHits hits = searchResponse.getHits();
log.info("检索结果数量为:{}", "" + hits.getHits().length);
//结果打印
hits.forEach(hit -> {
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
// 获取坐标
Object location = sourceAsMap.get("location");
String datadate = sourceAsMap.get("datadate").toString();
Long id = Long.valueOf(sourceAsMap.get("id").toString());
//获取距离值,并保留两位小数点
BigDecimal geoDis = BigDecimal.valueOf((Double) hit.getSortValues()[0]);
System.out.println("获取坐标:" + location + ",时间:" + datadate + ",距离:" + geoDis + ",id:" + id);
});
}
}
2023-08-20 23:16:51.745 INFO 8624 --- [ main] c.s.elasticsearch.TestSearchDocument : 开始搜索
2023-08-20 23:16:52.705 INFO 8624 --- [ main] c.s.elasticsearch.TestSearchDocument : 结束搜索
2023-08-20 23:16:52.706 INFO 8624 --- [ main] c.s.elasticsearch.TestSearchDocument : 检索结果数量为:1401