目录

ES-使用geo-point-查询离目标地址最近的数据

目录

ES 使用geo point 查询离目标地址最近的数据

需求描述:项目中需要通过经纬度坐标查询目标地所在的行政区。

解决思路大致有种,使用es和mysql分别查询。

1、使用es进行查询

将带有经纬度坐标的省市区数据存入es中,mappings字段使用geo point类型,索引及查询dsl如下。

geo point文档地址:

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等。

总结:没有最好的,只有最适合的,按需设计。