作者
SoWhat
头图
CSDN下载自东方IC
引言
小麦同学是个吃货+技术宅,平日里就喜欢拿着手机地图点点按按来查询一些好玩的东西。某一天到北海公园游玩,肚肚饿了,于是乎打开手机地图,搜索北海公园附近的餐馆,并选了其中一家用餐。
饱暖思yin欲的小麦饭后思考「地图后台如何根据自己所在位置查询来查询附近餐馆的呢」?苦思冥想了半天,小麦想出了个方法:计算所在位置P与北京所有餐馆的距离,然后返回距离=米的餐馆。小得意了一会儿,小麦发现北京的餐馆何其多啊,这样计算不得了,于是想了,既然知道经纬度了,那它应该知道自己在西城区,那应该计算所在位置P与西城区所有餐馆的距离啊,机机运用了递归的思想,想到了西城区也很多餐馆啊,应该计算所在位置P与所在街道所有餐馆的距离,这样计算量又小了,效率也提升了。
小麦的计算思想很朴素,就是通过「过滤」的方法来减小参与计算的餐馆数目,从某种角度上讲,机机在使用索引技术。
一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL、oracle、PostgreSQL等)都在使用B树。B树索引本质上是对索引字段进行排序,然后通过类似「二分查找」的方法进行快速查找,即它要求索引的字段是可排序的,一般而言,可排序的是一维字段,比如时间、年龄、薪水等等。但是对于空间上的一个点(二维,包括经度和纬度),如何排序呢?又如何索引呢?解决的方法很多,下文介绍一种方法来解决这一问题。
思想:如果能通过某种方法将二维的点数据转换成一维的数据,那样不就可以继续使用B树索引了嘛。那这种方法真的存在嘛,答案是肯定的。目前很火的GeoHash算法就是运用了上述思想,下面我们就开始GeoHash之旅吧。
感性认识
先来两个干货,在线查看GPS某个区域的GeoHash值。
1.
除Peano空间填充曲线外,还有很多空间填充曲线,如图所示,其中效果公认较好是Hilbert空间填充曲线,相较于Peano曲线而言,Hilbert曲线没有较大的突变。为什么GeoHash不选择Hilbert空间填充曲线呢?可能是Peano曲线思路以及计算上比较简单吧,事实上,Peano曲线就是一种四叉树线性编码方式。
使用注意点
1.临界问题
由于GeoHash是将区域划分为一个个规则矩形,并对每个矩形进行编码,这样在查询附近POI信息时会导致以下问题,比如红色的点是我们的位置,绿色的两个点分别是附近的两个餐馆,但是在查询的时候会发现距离较远餐馆的GeoHash编码与我们一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。这个问题往往产生在边界处。
解决的思路很简单,我们查询时,除了使用定位点的GeoHash编码进行匹配外,还使用周围8个区域的GeoHash编码,这样可以避免这个问题。
2.注意点
我们已经知道现有的GeoHash算法使用的是Peano空间填充曲线,这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近餐馆时候,首先筛选GeoHash编码「相似的POI(pointofinterest)点」,然后进行实际距离计算。
3.使用心得
GeoHash只是空间索引的一种方式,特别适合点数据,而对「线、面数据采用R树索引」更有优势(可为什么需要空间索引)。
GeoHash值可以区分精度,位数越多,精度越高,表达的地理位置越精细;如一位的GeoHash值把地球划分为32个矩形,8位的geohash值把地球划分为32^8个小矩形
适合根据某个经纬度坐标position计算出GeoHash值,然后和数据库中精度更高的GeoHash值做前缀比较
空间索引
常见问题:如何根据自己所在位置查询来查询附近50米的POI(pointofinterest,比如商家、景点等)呢(图1a)?
每个POI都有经纬度信息,用图1b的SQL语句在mySQL中建立了POI_spatial的表,其中lat和lng两个字段来代表纬度和经度。为后续分析方便起见,我人造了40万个POI数据。
方法一:暴力方法
该方法的思路很直接:计算位置与所有POI的距离,并保留距离小于50米的POI。
插句题外话,计算经纬度之间的距离不能像求欧式距离那样平方开根号,因为地球是个不规整的球体(图2a),普通计算适合都是默认按最简单的完美球体假设,两点之间的距离函数应该如图2b所示。
该方法的复杂度为:40万*距离函数。我们将球体距离函数写为mysql存储过程distance,之后我们执行查询操作(图3),发现花费了4.66秒。
方法二:矩形过滤方法
该方法采用逐步细化的方式,一般分为两部:
先用矩形框过滤(图4a),判断一个点在矩形框内很简单,只要进行两次判断(LtMinlatLtMax;LnMinlngLnMax),落在矩形框内的POI个数为n(n40万);用球面距离公式计算位置与矩形框内n个POI的距离(图4b),并保留距离小于50米的POI矩形过滤方法的复杂度:40万矩形过滤函数+n距离函数(n40万)。
根据这个思路我们执行SQl查询(图5)(注:经度或纬度每隔0.度,距离相差约米,由此推算出矩形左下角和右上角坐标),发现过滤后正好剩下两个POI。
此查询花费了0.36秒,相比于方法一查询时间大大降低,但是对于一次查询来说还是很长。时间长的原因在于遍历了40万次。
方法三:B树对经度或纬度建立索引
方法二耗时的原因在于执行了遍历操作,为了不进行遍历,我们自然想到了索引。我们对纬度进行了B树索引。
altertablepoi_spatialaddindexlatindex(lat);altertablepoi_spatialaddindexlngindex(lng);
此方法包括三个步骤:
通过B树快速找到某纬度范围的POI(图6a),个数为m(m40万),复杂度为Log(40万)*过滤函数;在步骤a过滤得到的m个POI中查找某经度范围的POI(图6b),个数为n(nm),复杂度为m*过滤函数;用球面距离公式计算位置与步骤b得到的n个POI的距离(图6c),并保留距离小于50米的POI
执行SQL查询(图7),发现时间已经大大降低,从方法2的0.36秒下降到0.01秒
B树能索引空间数据吗?
这时候有人会说了:方法三效果如此好,能够满足我们附近POI查询问题啊,看来B树用来索引空间数据也是可以的嘛!那么B树真的能够索引空间数据吗?
只能对经度或纬度索引(一维索引),与期望的不符我们期待的是快速找出落在某一空间范围的POI(如矩形)(图8a),而不是快速找出落在某纬度或经度范围的POI(图8b),想象一下,我要查询北京某区的POI,但是B树索引不仅给我找出了北京的,还有与北京同一维度的天津、大同、甚至国外城市的POI,当数据量很大时,效率很低。
当数据是多维,比如三维(x,y,z),B树怎么索引?比如z可能是高程值,也可能是时间。有人会说B树其实可以对「多个字段进行索引」,但这时需要指定优先级,形成一个组合字段,而空间数据在各个维度方向上不存在优先级,我们不能说纬度比经度更重要,也不能说纬度比高程更重要。当空间数据不是点,而是线(道路、地铁、河流等),面(行政区边界、建筑物等),B树怎么索引?对于面来说,它由一系列首尾相连的经纬度坐标点组成,一个面可能有成百上千个坐标,这时数据库怎么存储,B树怎么索引,这些都是问题。既然传统的索引不能很好的索引空间数据,我们自然需要一种方法能对空间数据进行索引,即空间索引。
实战
SpringBoot+Redis实现geo操作。调用Java三方依赖判断两点距离判断[3]一个IP坐标是否在中国地图内,核心思想就是看点到线上的交点看是否在右边。具体看参考文档实战代码。Reference
[1]