揭开附近的“人”神秘面纱:初识GeoHash算法

一、何为附近的“人”

WX摇一摇附近的妹子、DY直播推荐附近的人、DD推荐附近的出租车、MT推荐附近的美食,我们平时用的APP是如何向我们附近的“人”的呢?为了揭开它们什么的面纱,今天我们就去认识下空间索引:GeoHash算法。

举个实际的场景:当我们点开一个外卖的app就能看到自己附近的餐厅,那我们有没有想过这是怎么实现的呢?come on~

c2419a240a739cb2cf404de598914d6.jpg

首先我们能想到的就是把所有餐厅的经纬度存下来,然后当用户选择附近餐厅是,我们先获取用户的经纬度,然后到数据库中查出所有的经纬度,依次计算它们和用户间的距离。最后根据用户输入的距离范围过滤出合适的餐厅,并根据距离做一个升序排列。这样貌似能查出附近的餐厅,但是餐厅的数量这么多,直接全查出来内存也要爆掉,即使分批处理计算量也十分大。这样用户等待的时间就会特别长。那有什么办法能减少我们的计算量呢?很简单,我们应该只计算用户关心的那一片数据,而不是计算所有的。例如用户在北京,那完全没必要计算海南,黑龙江,新疆,浙江等其它地区的数据。如果我们能快速定位到北京直至某个区,那么我们的计算量将大大减少。我们发现这其实就是索引的功能,但是MySQL对这种二维的地理位置的索引支持并不友好(mongodb有直接的地理位置索引),它对一维的像字符串这样的支持很好。那如果我们的数据在MySQL中,有没有什么方法能将我们的二维坐标转换为一种可比较的字符串呢?这就是我们今天要介绍的geohash算法。

geohash简单来说就是将一个地理坐标转换为一个可比较的字符串的算法。不过生成的字符串表示的是一个矩形的范围,并不是一个点。比如西二旗地铁附近这一片矩形区域就可以用wx4eyu82这个字符串表示,并且越靠前的编码表示额范围越大,比如中国绝大部分地区可以用w这个字母表示的矩形区域内。像wx4eyu82表示的区域一定在wx4e表示的区域范围内。利用这些特性我们就可以实现附近餐厅的功能了,比如我们希望查看西二旗地铁附近的餐厅就可以这样查询:select * from table where geohash like 'wx4eyu82%'; 这样就可以利用索引,快速查询出相关餐厅的信息了。并且我们还可以用wx4eyu82为key,餐厅信息为value做缓存。

二、地理位置推荐

基于地图的空间搜索使用经度(longitude)和纬度(latitude)作为基本的搜索条件进行地理信息的相关检索和查询。其实现方式一般包括以下一种实现方式:

  • 在数据库表中将经度(longitude)和纬度(latitude)分别以两个不同的列进行存储,通过对经度(longitude)和纬度(latitude)两列建二元索引,提供对经度(longitude)和纬度(latitude)的检索。这种方式简单粗暴,可以满足对小规模的数据进行经纬度的检索。但是,在地图搜索相关的应用中,地理信息相关的记录的数据量会很多,二元索引的速度应该不会很理想,再者,基于地理信息的搜索行为相关的搜索基准点相对比较稀疏,数据库的缓存效果也不会太理想。

  • Geohash:通过这种编码方式,可以将经度(longitude)和纬度(latitude)值转换为一个基于base32编码的串,一方面可以实现对地理信息的一元索引;另外,地理上相近的点通过这种编码方式编码之后所得的串的前缀相同(当然会有特例,也就是一些边界条件,本文后面讲描述;前缀长度根据表示精度的不同而不同)。这样,要实现"搜索给定点附近的其他点"时,只需要将该参照点按照Geohash进行编码,按照精度要求从所得编码串中选取一定长度的字串作为前缀进行搜索,这样就可以很方便的实现该功能。

  • ac470379dc56b7dae3bb28c15a0308e.jpg

    三、GeoHash算法

    基本原理

    GeoHash是一种地址编码方法。他能够把二维的空间经纬度数据编码成一个字符串。

    我们知道,经度范围是东经180到西经180,纬度范围是南纬90到北纬90,我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。

    如果纬度范围[-90°, 0°)用二进制0代表,(0°, 90°]用二进制1代表,经度范围[-180°, 0°)用二进制0代表,(0°, 180°]用二进制1代表,那么地球可以分成如下4个部分:

     如果在小块范围内递归对半划分呢?

    可以看到,划分的区域更多了,也更精确了。geohash算法就是基于这种思想,划分的次数更多,区域更多,区域面积更小了。通过将经纬度编码,给地理位置分区

    算法过程

    以经纬度值:(116.389550, 39.928167)进行算法说明,对纬度39.928167进行逼近编码 (地球纬度区间是[-90,90])

  • 区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1
  • 接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0
  • 递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167
  • 如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,序列的长度跟给定的区间划分次数有关,如下图;
  • 同理,地球经度区间是[-180,180],可以对经度116.389550进行编码。通过上述计算,纬度产生的编码为1 1 0 1 0 0 1 0 1 1 0 0 0 1 0,经度产生的编码为1 0 1 1 1 0 0 0 1 1 0 0 0 1 1

    • 合并:偶数位放经度,奇数位放纬度,把2串编码组合生成新串如下图:

       将11100 11101 00100 01111 0000  01101转成十进制,对应着28、29、4、15,0,13 十进制对应的base32编码就是wx4g0e,如下

    • 同理,将编码转换成经纬度的解码算法与之相反。

    GeoHash算法的特点

    Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。 

    GeoHash用一个字符串表示经度和纬度两个坐标。在数据库中可以实现在一列上应用索引(某些情况下无法在两列上同时应用索引)

    GeoHash表示的并不是一个点,而是一个矩形区域。

    GeoHash编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索。

    编码越长,表示的范围越小,位置也越精确。因此我们就可以通过比较GeoHash匹配的位数来判断两个点之间的大概距离。