前言

最近项目需要使用到通过用户的经纬度查询附近的地理位置,了解到geohash可以快速的查找附近的地点,就找时间了解下是geohash的实现方式。

geohash介绍

geohash能将地理位置编码成字母和数字组成字符串。每个字符串表示一个地图上的矩形区域,举行区域的大小取决于字符串的长度,长度越长,矩形区域越小。每个矩形区域中的位置都共享相同的geohash字符串。正因为如此,我们可以将geohash的字符串当做key,来存储所有的地理位置坐标。

上边提到字符串越长,表示的区域越小,也就是位置越精确。我们可以从geohash字符串的后边移除字符串来增大矩形区域的位置,当然也降低了经度。

geohash字符串相近的地点其距离也越近,相似前缀越长,两个地点越近。但也有特殊情况,地理位置近但其geohash字符串差别很大,这种情况在下边介绍。

主要用途

geohash主要用在如下两个场景:

  1. 将geohash字符串当做一个唯一的标识
  2. 用来表示地点数据,将其存储到数据库中

geohash也经常用于地理标记。

使用geohash有两个优点:一是用geohash索引的数据表示的是一个矩形区域中所有的点,在数据库中单个索引的效率也比多个索引高;二是我们可以使用geohash来进行快速但不精确的查询:geohash相近的字符串也有相近的距离。

算法和例子

geohash的字符串其实是base32的结果。
我们用ws104来作为例子说明如何从geohash解析成经纬度。

先使用base32对字符串ws104进行解码。
下面是base32的字符表:

十进制 0 1 2 3 4 5 6 7
base32 0 1 2 3 4 5 6 7
十进制 8 9 10 11 12 13 14 15
base32 8 9 b c d e f g
十进制 16 17 18 19 20 21 22 23
base32 h j k m n p q r
十进制 24 25 26 27 28 29 30 31
base32 s t u v w x y z

由此可以得到ws104对应的二进制串是:11100 11000 00001 00000 00100

从左边开始数从0开始,偶数位是经度(1101 0001 0001 0),奇数位是维度(1010 0000 0000)

这些二进制数从左到右对经纬度进行一系列分解,对于维度其范围在-90到90度,对于经度其范围在-180到180度。0表示较小的部分,1表示较大的部分。经度第一位是1,因此经度的范围在0到180度的范围内。我们依次进行此操作,对于维度的操作一样。

这样我们也可以理解为什么geohash字符串长度越长,表示的数据越准确。

下面是对维度进行分解的数据:

位置 二进制值 最小值 中间值 最大值 平均值 最大误差
0 1 -90.000 0 90.000 45.000 45.000
1 0 0 45.000 90.000 22.500 22.500
2 1 0 22.500 45.000 33.750 11.250
3 0 22.500 33.750 45.000 28.125 5.625
4 0 22.500 28.125 33.750 25.3125 2.8125
5 0 22.500 25.3125 28.125 23.90625 1.40625
6 0 22.500 23.90625 25.3125 23.203125 0.703125
7 0 22.500 23.203125 23.90625 22.8515625 0.3515625
8 0 22.500 22.8515625 23.203125 22.67578125 0.17578125
9 0 22.500 22.67578125 22.8515625 22.587890625 0.087890625
10 0 22.500 22.587890625 22.67578125 22.5439453125 0.0439453125
11 0 22.500 22.5439453125 22.587890625 22.52197265625 0.02197265625

因此这个位置的维度在范围22.500到22.5439453125之间,误差为0.02197265625。

同理可以算出经度范围在113.994140625到114.0380859375之间,误差为0.02197265625。

位置 二进制值 最小值 中间值 最大值 平均值 最大误差
0 1 -180.000 0 180.000 90.000 90.000
1 1 0 90.000 180.000 135.000 45.000
2 0 90.000 135.000 180.000 112.500 22.500
3 1 90.000 112.500 135.000 123.750 11.250
4 0 112.500 123.750 135.000 118.125 5.625
5 0 112.500 118.125 123.750 115.3125 2.8125
6 0 112.500 115.3125 118.125 113.90625 1.40625
7 1 112.500 113.90625 115.3125 114.609375 0.703125
8 0 113.90625 114.609375 115.3125 114.2578125 0.3515625
9 0 113.90625 114.2578125 114.609375 114.08203125 0.17578125
10 0 113.90625 114.08203125 114.2578125 113.994140625 0.087890625
11 1 113.90625 113.994140625 114.08203125 114.0380859375 0.0439453125
12 0 113.994140625 114.0380859375 114.08203125 114.01611328125 0.02197265625

下面我们给出geohash字符串长度跟精度的关系

geohash长度 维度位数 经度位数 维度误差 经度误差 距离误差(km)
1 2 3 $\pm$23 $\pm$23 $\pm$2500
2 5 5 $\pm$2.8 $\pm$5.6 $\pm$630
3 7 8 $\pm$0.70 $\pm$0.70 $\pm$78
4 10 10 $\pm$0.087 $\pm$0.18 $\pm$20
5 12 13 $\pm$0.022 $\pm$0.022 $\pm$2.4
6 15 15 $\pm$0.0027 $\pm$0.0055 $\pm$0.61
7 17 18 $\pm$0.00068 $\pm$0.00068 $\pm$0.076
8 20 20 $\pm$0.000085 $\pm$0.00017 $\pm$0.019

使用上的限制

边界情况

前面提到我们可以通过前缀匹配来大致的寻找附近的点。但是,这不是总能够成立的。

南极和北极的地点相近但是它们的经度差别很大,没有相同的前缀;在180度子午线(经线)两侧的地点,距离很近,但是在不同的区域内,也不会有相同的前缀。同样的,划分的区域边界,也存在类似的情况。

非线性的距离

由于geohash是基于经纬度坐标的距离进行计算,两个geohash点映射出来的距离并不是真实的距离。这是因为我们很难将球体上的坐标映射到一个均匀的二维空间。

尽管存在许多问题,但仍有可能的解决办法,这些算法已经成功的在Elasticsearch,MongoDB,HBase中实现用于近似查找。

参考

1) GeoHash核心原理解析
2) Geohash-Wikipedia