一. 背景
LBS业务中涉及大量的点数据(商业POI等)和面数据(商圈、行政区边界、定向围栏等),业务中对点-面匹配有极大需求。而不同面数据的复杂程度和空间分布相差各异,如何实现高效且精准的匹配算法,将对离线和在线任务产生巨大帮助。针对此问题,本文探讨了现有的算法,提出了改进方案,将匹配阶段效率提高33倍,整体计算时间从原来的2.7小时减少到10min(减小到原来的1/16)。
二. 空间索引[1倍速]
LBS应用中存在各种各样的空间索引方案,其主要目的是通过快速、模糊的空间关系判断筛选出与查询目标相关的一系列空间对象。常见的空间索引方案包括GeoHash和Google的S2、四叉树、R树、网格索引。
几种常用的空间索引优劣如表1所示,Google S2和GeoHash原理都是将一个面数据通过多个不同层级的索引单元进行近似,因而通常只能进行模糊匹配(如图1所示)。不过以匹配的精确性为代价,Google S2和GeoHash都获得了较好的匹配效率。另外这两种索引为了适应线上服务,通过空间填充曲线(GeoHash为Z形填充曲线,S2为希尔伯特填充曲线)将二维的经纬度数据降维成一个一维索引,方便使用DB进行存储。四叉树、R树、网格索引则是通过索引单元对目标区域的外接矩形进行索引,然后通过点面包含关系判断算法(如射线法)判断得到最终结果,R树由于类似于B树的建树原理,索引层数与需要索引的面数量相关,正比于log(面的数量),四叉树索引层数则和需要索引的空间大小有关,正比于log(面覆盖的空间大小)。相比而言,网格索引则直接通过Hash进行索引,相对四叉树和R树索引效率较高。
在的实际应用当中,由于需要进行大量的精确匹配,Google S2和GeoHash并不是合适的方案,而四叉树和R树则因其索引效率较低在大数据计算中使用较少。而网格索引有着高效、简单的性质以及对离线计算任务的良好适配,因而适合作为作为点面判断的索引。
索引类型 | 索引键数 | 索引效率 | 精确or模糊匹配 | 索引级数 |
---|---|---|---|---|
Google S2 | 1 | 高 | 模糊 | 自定义 |
GeoHash | 1 | 高 | 模糊 | 自定义 |
四叉树 | 2 | 低 | 精确 | log(空间大小) |
R树 | 2 | 高 | 精确 | log(面数据量) |
表1:几种常用空间索引优劣对比
网格索引用于面数据索引和点面匹配的基本流程如下:
网格索引的优点:
索引效率高:对于面数据简单(边界点小于10个)的点面匹配任务,网格索引效率极高。对于18亿点70万面的匹配任务,使用网格索引能在20min左右完成。
索引构建简单:网格索引构建逻辑简单,单个网格索引构建速度快
网格索引的缺点:
灵活性差:所有网格大小一样,难以区别对待不同大小的面,容易造成计算和存储浪费(如图3所示)
空间占用大:网格粒度较细时网格索引占用空间极大
效率提升上限低:无法降低点面包含关系判断计算量大的问题,当面数据较复杂时(如行政区边界有上百个边界点),整体点面匹配耗时仍然很大。
网格索引数量会随网格边长的减小而呈平方倍增大,网格粒度过细不仅会导致建索引的时间过长,还会导致jvm频繁GC,减少甚至消除建索引带来的匹配速度增益。
不同边长网格索引 | 建索引耗时 | 匹配耗时 | 总体耗时(包含广播等) |
---|---|---|---|
10km | 10s | 2.7h | 2.7h |
1km | 12min | 2.6h | 2.8h |
100m | 10+h | - | - |
表2:在18亿点-2800个行政区边界匹配任务中,不同粒度的网格索引效率
三. 拆分剪枝+多级索引[7倍速]
单纯的网格索引由于存在上述缺点导致整体点面匹配效率仍然较低,针对这些问题有两个优化点:
面数据拆分剪枝:网格索引仅仅是召回点附近的候选面,并没有减少最终点面包含关系判断的计算成本,通过对原始面数据的网格化和拆分、剪枝,可以大幅较少需要进行的点面包含关系判断次数及每次判断的计算量。
多级索引:使用多级索引增加灵活性,索引粒度自动适配面大小,大幅降低建索引计算和存储成本
3.1 面数据拆分剪枝
如图4所示,通过将原始的面数据按索引单元网格进行拆分,可以得到两种类型的索引单元:(1)索引单元被面数据填满(2)索引单元未被面数据填满。对于第一类索引单元(蓝色),如果点落在索引单元内,可以直接判定点在面内,而不需要通过射线法进行点面包含关系判断;对于第二类索引单元,只需判断点是否被拆分后的面数据(红色)包含,而拆分后的面数据通常相比原来的面数据简单很多,因而可以大幅减少计算量。
索引类型 | 建索引耗时 | 匹配耗时 | 总体耗时(包含广播等) |
---|---|---|---|
网格索引(10km) | 10s | 2.7h | 2.7h |
网格索引(10km)+ 剪枝 | 4.3min | 2.2h | 2.2h |
网格索引(1km)+剪枝 | 9.7h | 20min | 10h |
网格索引(100m)+剪枝 | 1day+ | - | - |
表3:在18亿点-2800个行政区边界匹配任务中,不同粒度网格索引+剪枝效率
如表3所示,10km网格索引+剪枝相对于纯网格索引整体计算耗时减少18.5%,但是当索引粒度进一步细化时,建索引时间呈现大幅增长的趋势。当索引粒度到1km变长网格时,建索引时间达到了9.7h,索引粒度更细时甚至无法完成计算。 其原因和细粒度纯网格索引类似,当网格粒度过细,网格量急剧增长,计算量和GC消耗的时间也快速增长,导致整体速度被拖慢。
3.2 多级索引
单级索引一个很大的问题在于灵活性较差,无法很好地适配不同尺度的面数据。为了解决这个问题,需要引入多级索引来解决这个问题。
对于一个面数据,首先使用粗粒度网格进行拆分剪枝和索引(图5左),然后对未被面数据填满的索引单元(紫色)进行细粒度索引,得到细粒度下索引单元被面数据填满部分(图5右橙色部分)和未被填满部分(图5右绿色部分)。由此可以得到一个包含3个蓝色索引单元,20个橙色索引单元,39个绿色索引单元的二级索引,相比直接按细粒度网格进行一级索引,网格数减少了3x4-3=9个,对于面积更大的面数据,多级索引的效果提升更大。
索引类型 | 建索引耗时 | 匹配耗时 | 总体耗时 |
---|---|---|---|
网格索引(10km) | 10s | 2.7h | 2.7h |
网格索引(10km)+剪枝 | 4.3min | 2.2h | 2.2h |
4级索引(第一级10km)+剪枝 | 12min | 6min | 23min |
表4:在18亿点-2800个行政区边界匹配任务中,多级索引效率对比
通过多级索引已经将点面匹配任务时间从2.7h减少到23min,也就是7倍速的提升,那是否还能进步一对任务进行加速呢?
4. 分布式建索引[16倍速]
注意到建索引耗时随着索引的复杂性逐渐增加,那么建索引这步是否可以进一步优化呢?注意到每个面数据进行网格拆分、剪枝、索引的计算是相互独立的,因而可以通过Spark并行构建每个面的索引,然后在Driver汇总生成整体索引后广播到每个Executor上。
分布式建索引最终效果如下表所示:
索引类型 | 建索引耗时 | 匹配耗时 | 总体耗时 |
---|---|---|---|
网格索引(10km) | 10s | 2.7h | 2.7h |
4级索引(第一级10km)+剪枝 | 12min | 6min | 23min |
分布式建4级索引(第一级10km)+剪枝 | 1.3min | 6min | 10min |
表5:在18亿点-2800个行政区边界匹配任务中,多级索引效率对比
至此,通过分布式建索引和索引序列化上的一些优化,将建索引耗时从12min减少到了1.3min,整体耗时从23min减少到10min,相对于10km粒度网格索引,匹配阶段速度提升33倍,整体耗时只有原来的1/16。