需求描述:项目中需要通过经纬度坐标查询目标地所在的行政区。
解决思路大致有种,使用es和mysql分别查询。
1、使用es进行查询
将带有经纬度坐标的省市区数据存入es中,mappings字段使用geo point类型,索引及查询dsl如下。
geo point文档地址:
Geo-distance query | Elasticsearch Guide [8.6] | Elastic
Sort search results | Elasticsearch Guide [8.6] | Elastic
mappings结构:
PUT /sys_district
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 1
}
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"parent_id": {
"type": "long"
},
"name": {
"type": "keyword"
},
"zipcode": {
"type": "integer"
},
"pinyin": {
"type": "keyword"
},
"location": {
"type": "geo_point" // 如果用于地理坐标,可以考虑使用 geo_point 类型
},
"level": {
"type": "byte"
},
"sort": {
"type": "byte"
}
}
}
}
dsl语句:
# 搜索坐标点附近的数据
GET sys_district/_search
{
"from": 0,
"size": 3,
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": [
{
"geo_distance": {
# 半径内距离限制
"distance": "100km",
"location": {
# 目的地坐标
"lat": 34.4328,
"lon": 115.88
}
}
},
{
"term": {
"level": "3"
}
}
]
}
},
# 排序
"sort" : [
{
"_geo_distance" : {
"location" : {
"lat" : 34.4328,
"lon" :115.88
},
"order" : "asc",
"unit" : "km"
}
}
]
}
获取举例最近的排序不能漏了
2、使用mysql进行查询
将带有经纬度坐标的省市区数据存入mysql中,使用mysql直接计算,表结构及查询sql如下。
表结构:
CREATE TABLE `sys_district` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`parent_id` INT(10) UNSIGNED NOT NULL COMMENT '父栏目',
`name` VARCHAR(50) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
`zipcode` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`pinyin` VARCHAR(100) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
`lng` VARCHAR(20) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
`lat` VARCHAR(20) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
`level` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
`sort` TINYINT(3) UNSIGNED NOT NULL DEFAULT '50' COMMENT '排序',
`location` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`) USING BTREE
)
COMMENT='(公共)区域数据'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
查询sql:
SELECT * FROM sys_district WHERE ABS(lat - 34.4328) + ABS(lng - 115.88) = (SELECT MIN(ABS(lng - 115.88) + ABS(lat - 34.4328)) FROM sys_district ) LIMIT 1;
使用mysql计算可优化的地方在于,新版本mysql提供了空间几何字段类型POINT,优化后新表结构如下。
CREATE TABLE `sys_district` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`parent_id` INT(10) UNSIGNED NOT NULL COMMENT '父栏目',
`name` VARCHAR(50) NOT NULL DEFAULT '' COLLATE 'utf8mb3_general_ci',
`zipcode` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`pinyin` VARCHAR(100) NOT NULL DEFAULT '' COLLATE 'utf8mb3_general_ci',
`lng` VARCHAR(20) NOT NULL DEFAULT '' COLLATE 'utf8mb3_general_ci',
`lat` VARCHAR(20) NOT NULL DEFAULT '' COLLATE 'utf8mb3_general_ci',
`geom` POINT NOT NULL COMMENT 'geo',
`level` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
`sort` TINYINT(3) UNSIGNED NOT NULL DEFAULT '50' COMMENT '排序',
`location` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8mb3_general_ci',
PRIMARY KEY (`id`) USING BTREE,
SPATIAL INDEX `geom` (`geom`)
)
COMMENT='(公共)区域数据'
COLLATE='utf8mb3_general_ci'
ENGINE=InnoDB
;
字段设置:
ALTER TABLE `sys_district`
ADD COLUMN `geom` POINT NULL AFTER `lat`;
UPDATE sys_district SET geom = ST_PointFromText(CONCAT('POINT(', lng, ' ', lat, ')')) ;
ALTER TABLE sys_district ADD SPATIAL INDEX(geom);
查询sql如下:
ST_PointFromText(CONCAT('POINT(', lng, ' ', lat, ')'))
将表中的经度和纬度转换为几何点。
ST_Distance_Sphere(geom, ST_PointFromText(CONCAT('POINT(', 120.15, ' ', 30.28, ')')))
计算每个点与目标点之间的距离(单位为米)。
ORDER BY distance
按距离从小到大排序
SELECT id, name, lng, lat,
ST_Distance_Sphere(geom, ST_PointFromText(CONCAT('POINT(', 120.15, ' ', 30.28, ')'))) AS distance
FROM sys_district
ORDER BY distance
LIMIT 3;
3、其他方式
如果带查询的数据项不变化,类似于行政区划的坐标,还可以把这些数据加载到内存中进行计算。
3.1 Java-使用 Haversine 公式来计算(不依赖三方库)
创建表示位置的类
public class Location {
private double lon;
private double lat;
public Location(double lon, double double lat) {
this.lon = lon;
this.lat = lat;
}
// Getter 和 Setter 方法
}
使用 Haversine 公式计算两点间的距离
public class DistanceCalculator {
private static final int EARTH_RADIUS = 6371; // 地球半径,单位为公里
/**
* 计算两个经纬度点之间的距离
*/
public static double calculateDistance(Location loc1, Location loc2) {
double lat1 = Math.toRadians(loc1.getLat());
double lon1 = Math.toRadians(loc1.getLon());
double lat2 = Math.toRadians(loc2.getLat());
double lon2 = Math.toRadians(loc2.getLon());
double dlat = lat2 - lat1;
double dlon = lon2 - lon1;
double a = Math.sin(dlat / 2) * Math.sin(dlat / 2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(dlon / 2) * Math.sin(dlon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return EARTH_RADIUS * c; // 返回单位为公里
}
}
查找最近的数据点
public class NearestLocationFinder {
public static LocationData findNearestLocation(List<LocationData> locations, Location targetLocation) {
LocationData nearest = null;
double minDistance = Double.MAX_VALUE;
for (LocationData location : locations) {
Location currentLocation = new Location(location.getLocation().getLon(), location.getLocation().getLat());
double distance = DistanceCalculator.calculateDistance(currentLocation, targetLocation);
if (distance < minDistance) {
minDistance = distance;
nearest = location;
}
}
return nearest;
}
}
调用方法
public class Main {
public static void main(String[] args) {
// 已加载所有的位置数据
List<LocationData> locations = loadData();
// 输入的经纬度
Location targetLocation = new Location(115.65, 34.43);
// 查找最近的位置
LocationData nearest = NearestLocationFinder.findNearestLocation(locations, targetLocation);
System.out.println("最近的位置是: " + nearest.getName());
}
// 加载数据
private static List<LocationData> loadData() {
return new ArrayList<>();
}
}
4、Java-使用JTS STRtree(依赖三方库)
maven依赖
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.18.2</version>
</dependency>
调用方法
public class NearestPointFinder {
public static void main(String[] args) {
// 创建一个包含所有位置信息的列表
List<LocationData> locations = loadData();
// 输入的经纬度
double lon = 115.65, lat = 34.43;
// 使用JTS的STRtree加速查询
STRtree tree = new STRtree();
GeometryFactory geometryFactory = new GeometryFactory();
for (LocationData location : locations) {
Point point = geometryFactory.createPoint(new Coordinate(location.getLocation().getLon(), location.getLocation().getLat()));
tree.insert(point.getEnvelopeInternal(), location);
}
Point targetPoint = geometryFactory.createPoint(new Coordinate(lon, lat));
LocationData nearest = (LocationData) tree.nearestNeighbour(targetPoint.getEnvelopeInternal(), null);
System.out.println("最近的位置是: " + nearest.getName());
}
private static List<LocationData> loadData() {
// 加载位置数据
return new ArrayList<>();
}
}
还有其他的一些三方库:H3 by Uber、GeoTools、Spatial4j等。
总结:没有最好的,只有最适合的,按需设计。