在日常生活中我们都会用到地图查找附近的功能,比如我们用地图查找附近的餐馆,比如返回距离<=1000米的餐馆,如果考虑用技术实现,我们应该怎么做呢?
如果单纯的从实现来看,我们很容易想到,地球是一个圆的球体,利用自己在地图上的坐标和周围餐厅的坐标再通过计算球面距离的计算公式由计算机求解并计算出结果,然后将结果返回,这样我们可以找出距离<=1000米的餐馆。
但是这样做存在一个问题,那就是计算量的问题,假设自己身处繁华的商业街,周围有很多的餐馆就意味着要有很多次的计算才能得出结果,这对服务器的性能是极大的考验,因为地图的用户很多,每天的搜索量也很大,必须有一个更好的解决方案来解决这个问题。
经纬度坐标其实是一组二维数据,如果能把二维的点数据转化为一维数据,在一维数据的领域就有很多的解决方案,因为一维数据的特点是可排序的,以对字段进行缓存,还可以利用B树对索引字段进行排序然后再用二分查找法进行快速查找。
GeoHash算法就是可以将经纬度的二维数据转化为一维数据的。先描述下该算法,地球纬度区间是[-90,90],地球的经度区间是[-180,180],将经纬度利用二分法的形式让点落于相对应的区间中,并对改区间按规则进行标记。
经度如果上区间标记为1,如果在下区间则标记为0,然后层层划分下去,分的层数越多,获取的经度就越精确。对于纬度也是按二分法进行区间划分,所对应的区间在坐边则标记为0,在右边则标记为1,然后层层划分,这样一个经纬度就会获取2个二进制的字符串,一个是经度的二进制字符串,另外一个是纬度的二进制字符串。然后将经纬度的二进制数进行组合,以奇数为纬度,以偶数为经度进行组合,这样合并后就会得到一串二进制字符串,然后将这串二进制的字符串进行Base32编码,最后就可以
得到该地区所生成的GeoHash值,这个GeoHash在宏观上它并不是一个点而是一块区域信息。
以经纬度(116.3906,39.92324)为例:
(1)对于维度39.92324, 39.92324属于(0, 90),所以取编码为1。然后再将(0, 90)分成 (0, 45), (45, 90)两个区间,而39.92324位于(0, 45),所以编码为0。以此类推,直到精度符合要求为止,得到纬度编码为1011 1000 1100 0111 1001。
(2)经度也用同样的算法,对(-180, 180)依次细分,得到116.3906的编码为1101 0010 1100 0100 0100。
(3)接下来将经度和纬度的编码合并,奇数位是纬度,偶数位是经度,得到编码 11100 11101 00100 01111 00000 01101 01011 00001。
最后,用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,得到(39.92324, 116.3906)的编码为wx4g0ec1。
不难看出这样的编码方式仅用一个字符串保存经纬度信息,并且精度由字符串从头到尾的长度决定,编码长度越长,精度越高。
GeoHash值的前缀相同的位数越多,代表的位置越接近,可以方便索引。(反之不成立,位置接近的GeoHash值不一定相似)。以下是精度表:
但这种方案的缺点是:从GeoHash的编码算法中可以看出,靠近每个方块边界两侧的点虽然十分接近,但所属的编码会完全不同。实际应用中,需要通过去搜索环绕当前方块周围的8个方块来解决该问题。
此时假设我们的需求是1000米范围内的餐馆,我们只需要把geohash的比对长度设置为6就可以了,然后geohash字符串进行匹配,如果两个20位geohash字符串进行比对,参照上图中的精度表,如果前6位是一样的话,就认为这两个地点互相的距离<=1000米,当然为了更加的高效,关于地图上的餐馆的geohash可以提前生成并缓存起来,要用到的时候再提取出来,这样的附件的距离搜索效率将会大大提升。
最后附上代码: