背景
Elasticsearch是一个分布式的可支持海量数据搜索与分析的搜索引擎,在我们针对地理位置搜索的时候通常会选着使用其中的GEO类型进行存储(包含 geo_shape 和 geo_point两种),那么Elasticsearch是如何优雅的存储地理位置坐标的呢?
这里我们就需要知道一个东西——GeoHash,它是由 Gustavo Niemeyer 于2008年发明的公共领域地理编码系统,将地理位置编码为一小段字母和数字。
感知
我们可以随便打开一个具备GeoHash转换功能的网站,输入一个经纬坐标进行转换,我们将会得到一串32位编码的字符串,这便是GeoHash的样子。

原理
经过感性的感知GeoHash是什么,我们还需要了解其中的生成原理和如何使用。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本身逐层逼近的过程,我们可以了解到其中必然会存在误差。其实在Elasticsearch中使用GEO查询一样会存在这个问题。Elasticsearch的默认等级是9,也就是精准到4.8米左右。
Elasticsearch官方并不建议我们修改精度,这可能会导致系统大幅度的变慢,在实际开发过程中,测试将精度等级调整至11级,查询多边形交叠数据返回时长高达4-8秒,难以应用生产环境。
geohash长度 | Lat误差/km | Lng误差/km | Lat位数 | Lng位数 | km误差 |
1 | ±23 | ±23 | 2 | 3 | ±2500 |
2 | ± 2.8 | ±5.6 | 5 | 5 | ±630 |
3 | ± 0.70 | ± 0.7 | 7 | 8 | ±78 |
4 | ± 0.087 | ± 0.18 | 10 | 10 | ±20 |
5 | ± 0.022 | ± 0.022 | 12 | 13 | ±2.4 |
6 | ± 0.0027 | ± 0.0055 | 15 | 15 | ±0.61 |
7 | ±0.00068 | ±0.00068 | 17 | 18 | ±0.076 |
8 | ±0.000086 | ±0.000172 | 20 | 20 | ±0.01911 |
9 | ±0.000021 | ±0.000021 | 22 | 23 | ±0.00478 |
10 | ±0.00000268 | ±0.00000536 | 25 | 25 | ±0.0005971 |
11 | ±0.00000067 | ±0.00000067 | 27 | 28 | ±0.0001492 |
12 | ±0.00000008 | ±0.00000017 | 30 | 30 | ±0.0000186 |
在Elasticsearch官方文档中也有写到:
首先,你需要决定使用什么样的精度。虽然你也可以使用 12 级的精度来索引所有的地理坐标点,但是你真的需要精确到数厘米吗?如果你把精度控制在一个实际一些的值,比如
1km
,那么你可以节省大量的索引空间
我们也在网上找到了类似的英文文献:
Geohashes divide the world into a grid of 32 cells—4 rows and 8 columns—each represented by a letter or number. The g cell covers half of Greenland, all of Iceland, and most of Great Britian. Each cell can be further divided into another 32 cells, which can be divided into another 32 cells, and so on. The gc cell covers Ireland and England, gcp covers most of London and part of Southern England, and gcpuuz94k is the entrance to Buckingham Palace, accurate to about 5 meters.
Bear in mind that whatever tree level you choose you always risk to get some false positives as geohash will never be 100% precise representation of your shape. It's a precision vs performance trade-off and geohash is the performance wise choice.
翻译:
GeoHash将世界划分为32个单元格--4行8列,每个单元格由一个字母或数字代表。g单元涵盖了格陵兰岛的一半,冰岛的全部,以及大英帝国的大部分。每个单元可以进一步划分为另外32个单元,这些单元又可以划分为另外32个单元,以此类推。gc单元覆盖了爱尔兰和英格兰,gcp覆盖了伦敦的大部分和英格兰南部的一部分,gcpuuz94k是白金汉宫的入口,精确到5米左右。
请记住,无论你选择什么样的树形级别,你总是有可能得到一些误报,因为geohash永远不会100%精确地表示你的形状。这是一个精度与性能的权衡,geohash是性能上的明智选择。
修改
如果我们确实需要修改 Elasticsearch 的Geohash精度要怎么做呢?
修改示例:
PUT /attractions
{
"mappings": {
"restaurant": {
"properties": {
"name": {
"type": "string"
},
"location": {
"type": "geo_point",
"geohash_prefix": true,
"geohash_precision": "1km"
}
}
}
}
}
将 geohash_prefix 设为 true 来告诉 Elasticsearch 使用指定精度来索引 geohash 的前缀。
精度可以是一个具体的数字,代表的 geohash 的长度,也可以是一个距离。 1km 的精度对应的 geohash 的长度是 7 。换句话说定义距离也不能保证一定是准确的这个误差距离。