LBS位置服务 距离计算的几种方式
背景
互联网兴起,出现了很多基于地理位置服务的产品,如地图、外卖、陌陌等等,都会提供查找附近的人、餐厅等功能。
『附近的』这个就是基于位置服务,比如查找用户附近的门店场景。
实现上是,后台门店系统中维护好各个门店自己的经纬度位置,一次查询时App端上提供用户当前经纬度位置,服务端查询距离这个位置较近的门店列表,给出一个排序。
问题是如何检索如何计算这个距离?早期使用MySQL存储的,也可以实现,但性能较差;后期也有很多数据库组件支持GEO查询检索,比如MongoDB、ElasticSearch、Redis等。
这里以门店列表shop_list 检索举例,列举下这些组件是如何使用的,想了解GeoHash的原理网上也有很多文章。
MySQL
一张门店表shop_list,表结构字段包括shop_id,shop_name,lng,lat (门店id,门店名称,以及门店的经纬度等)。
当前用户的所处位置的经纬度是:lng 116.30759,lat 40.05748。获取距离用户1000m以内的100家门店,按照距离从近到远排序。
SELECT shop_id, shop_name, lng, lat
, ROUND(6378.138 * 2 * ASIN(SQRT(POW(SIN((40.05748 * PI() / 180 - lat * PI() / 180) / 2), 2) + COS(40.05748 * PI() / 180) * COS(lat * PI() / 180) * POW(SIN((116.30759 * PI() / 180 - lng * PI() / 180) / 2), 2))) * 1000) AS distance
FROM shop_list
HAVING distance < 1000
ORDER BY distance
LIMIT 100;
下面的这个计算方法更快一些,效果和上面的几乎差不多,只是距离distance并不真实。如果只想按照距离排序查出结果是没问题的。
SELECT shop_id, shop_name, lng, lat
, POWER(lat - 40.05748, 2) + POWER(lng - 116.30759, 2) * POWER(COS((lat + 40.05748) / 2), 2) AS distance
FROM shop_list
HAVING distance < 1000
ORDER BY distance
LIMIT 100;
MongoDB
- 要明确MongoDB在使用距离查询时,存储的经纬度结构要类似这样才可以
'point' : [
116.299,
40.053
]
或者:
'point' : {
'lng' : 116.299,
'lat' : 40.053
}
- 给经纬度的point做一个2dSphere索引
db.shop_list.createIndex({"point":"2dsphere"})
- 检索
- 用户所处这个点的附近门店
db.shop_list.find({'point':{$nearSphere: [116.30759, 40.05748]}})
- 用户所处这个点的附近1000米门店
db.shop_list.find({point: { $geoWithin: { $centerSphere: [ [ 116.30759, 40.05748 ], 1000/6378137 ] } } })
- 用户所处这个点的附近1000米的100个门店,并且有距离计算值
db.runCommand({ geoNear : "shop_list" , near : [ 116.30759, 40.05748], num : 100 , spherical:true, distanceMultiplier: 6378137, maxDistance:1000/6378137})
Redis
Redis在3.2 版新增了对GEO功能的支持,类似的空间索引也是采用GeoHash 原理,配合zset集合有序存储,查询效率接近log(N)。
命令如:
# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]
# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]
# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]
# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
- 添加两个门店位置
GEOADD shop_list_pos 116.50759 40.55748 shop_id1 116.90759 41.05748 shop_id2
- 用户所处个点的附近1000米的100个门店,并且有距离计算值
GEORADIUS shop_list_pos 116.30759 40.05748 1000 m WITHDIST COUNT 100 DESC
ElasticSearch
ES计算距离同样也是利用GeoHash原理。
地理坐标点不能被动态映射 (dynamic mapping)自动检测,而是需要显式声明对应字段类型为 geo-point。
- 建立mapping
{
"mappings": {
"shop_list": {
"properties": {
"location": {
"type": "geo_point"
}
}
}
}
}
- 添加两个门店数据
db/shop_list/1
{
"shop_id": "1",
"location": {
"lat": 40.12,
"lon": -71.34
}
}
db/shop_list/2
{
"shop_id": "2",
"location": {
"lat": 40.52,
"lon": -71.64
}
}
- 查询距离 排序
db/shop_list/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"geo_distance": {
"distance": "160km",
"location": {
"lat": 40,
"lon": -70
}
}
}
}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 40,
"lon": -70
},
"order": "asc",
"unit": "km",
"distance_type": "plane"
}
}
]
}