1 什么是时空索引
通常可以听到最多的几个与时空索引技术相关的词,例如“点面匹配”,“地理围栏进场判定”,“POI推荐/周边美食推荐”等,其背后核心技术之一就是时空索引,但以上词语是基于场景的通俗描述,而非准确描述,例如点面匹配:一个点是否落入一个区域面中,其是一个简单的拓扑判断计算即可,而要知道一个点落入N个面中哪一个,或者反之,则需要进行搜索,此时问题背后的解题技术就是时空索引,其本质是从某个时空数据集中快速召回一个/若干个时空间相关实体。
1.1 时空索引问题定义
时空索引/空间索引问题,简单地说就是空间数据查询,作为一种辅助性的空间数据结构,空间索引介于空间操作算法和空间对象之间,它通过筛选作用,大量与特定空间操作无关的空间对象被排除,从而提高空间操作的速度和效率。 具体地,我们需要从大量POI库中召回我们需要的特定poi、邻近poi(如酒店餐馆等),或者某个点落在哪个区域围栏内(有个大的围栏数据集),也就是被查询的数据集是一个海量的poi集或者围栏数据集,为了减少查询时匹配次数,需要为海量数据集构建索引结构,提高查询效率。
带有时间纬度的空间数据索引就是时空索引,它也是多种业务场景中,如POI兴趣匹配推荐的基础核心技术之一。
1.2 时空索引问题主要应用场景
(1)海量地址POI的近邻召回: 在poi库中,实时召回一定距离或围栏范围内的地址POI,如附近餐饮门店推荐等,通常是设定一定距离,然后召回输入参考点(用户位置等)附近的全部门店信息; (2)地址围栏搜索召回: 在地理围栏AOI库中(如商圈库、多级行政区划等),快速搜索某点漏入哪个围栏内,或者召回一定围栏范围内,覆盖了哪些围栏库中的围栏(如一定范围内有哪些小区);行政区划解析亦属于此类。 (3)同时带有时间与空间属性的各类人及车辆轨迹数据的召回: 在菜鸟裹裹/饿了么场景中,需要实时召回一定范围内的小件员/骑手提供配送服务,而小件员轨迹是实时更新的;在离线中也需要对轨迹进行空间索引召回。
2 时空索引核心技术
这里,离线大规模数据集的空间索引实现,例如点面匹配,搜索某个poi落入哪些围栏内,(在一定区域围栏内搜索召回poi点一样),一般这种情况是要进行表的join操作,一个是待索引的点数据表,另一个是被索引空间数据集表(围栏数据集)。目前时空索引最常用的三种算法是Geohash、Rtree和Google S2算法,以及其对应改进/扩展算法,例如支持时间纬度的索引;其中Geohash和Google S2的原理是类似的,把空间的多维数据降维到一维曲线上,而其中一维曲线的好坏也决定了算法的性能好坏;Rtree索引是多种tree类索引算法最典型之一,其他还有KDtree等,其本质是把多维的空间数据简化为多层的简单MBR(最小外接矩形)实体匹配计算,从而提升搜索效率。
2.1 Geohash索引
Geohash索引是目前互联网一般业务中使用最广泛的空间索引之一,因其原理简单,易于实现;但其算法性能是相对较低的,尤其是在较大且稀疏数据集中,其索引性能低的原因是因为通常召回冗余度较高。
(1)算法原理
GeoHash是由Gustavo Niemeyer提出的,目的原本是为地球上的每一个点(根据经纬度)确定一条短的URL作为唯一标识。只是后来被广泛的应用到空间检索方面。GeoHash所做的事就是把一个坐标点映射到一个字符串上,每一个字符串代表的就是一个以经纬度划分的矩形区域。Geohash是一种分级的数据结构,把空间划分为网格。Geohash 属于空间填充曲线中的 Z 阶曲线(Z-order curve)的实际应用。何为 Z 阶曲线?
这个曲线比较简单,生成它也比较容易,只需要把每个 Z 首尾相连即可。Geohash算法的理论基础就是基于 Z 曲线的生成原理。Z 阶曲线同样可以扩展到三维空间。Geohash 能够提供任意精度的分段级别。一般分级从 1-12 级。Geohash位数与实际表示大小可以参考:
Geohash根据业务场景,首先,分别对待索引的点数据和被索引的围栏数据集进行Geohash编码,选择合适的Geohash位数来进行索引匹配,然后,对召回的围栏进行二次点是否在围栏内的判断即可,点在围栏内的算法可采用射线法。其生成不同长度格网可视化如下:
这里需要注意的是,在实际应用中,Geohash简单易用是其优点,但由于Geohash网格固定的特性,可能造成在边界处搜索遗漏问题,解决办法是对待索引的点进行“9宫格”邻域召回,分别去索引匹配围栏数据集,然后聚合。也就是假设待索引的点数据是A表,对A进行9倍扩张,即分别为9宫格的geohash编码列转行(trans_array函数),其他字段不变,然后再进行匹配,最后还是进行二次判断并聚合。
(2)算法实践
以下是推荐的参考Geohash开源库及应用示例(java):
<!-- Geohash-->
<dependency>
<groupId>ch.hsr</groupId>
<artifactId>geohash</artifactId>
<version>1.3.0</version>
</dependency>
/**
* 求与当前geohash及相邻的8个格子的geohash值。
*/
import java.util.ArrayList;
import ch.hsr.geohash.GeoHash;
import com.aliyun.odps.udf.UDF;
public class GeohashAdjacent extends UDF{
public String evaluate(String lng,String lat,String len) {
try{
String geohashAdjacents="";
GeoHash geohashCode=GeoHash.withCharacterPrecision(Double.valueOf(lat), Double.valueOf(lng), Integer.valueOf(len));
geohashAdjacents=geohashCode.toBase32();
GeoHash geohashAdjacentArray[]=geohashCode.getAdjacent();
for(int i=0;i<geohashAdjacentArray.length;i++){
geohashAdjacents+=","+geohashAdjacentArray[i].toBase32();
}
return geohashAdjacents;
}catch (Exception e) {
e.printStackTrace();
return "parms error";
}
}
public String evaluate(String lng,String lat,String len,String gridDimension) {
try{
String geohashAdjacents="";
int gridDim=Integer.valueOf(gridDimension);
if(gridDim>30){
return "gridDim is too long!";
}
if(gridDim%2==0){gridDim+=1;}
geohashAdjacents=evaluate(lng,lat,len);
String []geohashs=geohashAdjacents.split(",");
ArrayList<String> geohashList=new ArrayList<String>();
for(int i=0;i<geohashs.length;i++){
geohashList.add(geohashs[i]);
}
for(int i=3;i<gridDim;i+=2){
int listSize=geohashList.size();
for(int j=0;j<listSize;j++){
String []geohashArrays=getNearGeohashes(geohashList.get(j)).split(",");
for(int k=0;k<geohashArrays.length;k++){
if(!geohashList.contains(geohashArrays[k])){
geohashList.add(geohashArrays[k]);
}
}
}
}
geohashAdjacents=geohashList.toString().replace("[", "").replace("]", "");
return geohashAdjacents;
}catch (Exception e) {
e.printStackTrace();
return "parms error";
}
}
public String getNearGeohashes(String geohashString) {
try{
String geohashAdjacents="";
GeoHash geohashCode=GeoHash.fromGeohashString(geohashString);
GeoHash geohashAdjacentArray[]=geohashCode.getAdjacent();
for(int i=0;i<geohashAdjacentArray.length;i++){
geohashAdjacents+=geohashAdjacentArray[i].toBase32();
if(i<geohashAdjacentArray.length-1){
geohashAdjacents+=",";
}
}
return geohashAdjacents;
}catch (Exception e) {
e.printStackTrace();
return "parms error";
}
}
public static void main(String[] args) {
GeohashAdjacent geohashAdjacent=new GeohashAdjacent();
String geohashAdjacents=geohashAdjacent.evaluate("121.355", "31.3252", "7","15");
System.out.println(geohashAdjacents);
}
}
2.2 R树空间索引
作为geohash索引的替代者,rtree索引综合性能较好,且易于扩展,也是目前业界采用较多的时空索引算法之一。R 树利用空间实体外接矩形建立空间索引。R 树空间索引建立每个实体的外接矩形(rectangles,R),通过外接矩形的最大、最小坐标检索空间实体。对这些虚拟矩形建立空间索引,设计虚拟的矩形目录,将空间对象包含它含在矩形内,以虚拟矩形为空间索引,它包含指向所包围的空间实体的指针。为提高检索效率,R 树空间索引还将空间位置相近的实体外接矩形重新组织为更大的虚拟矩形,形成多级空间索引。
(1)算法原理
R树是一种多级平衡树,它是B树在多维空间上的扩展。在R树中存放的数据并不是原始数据,而是这些数据的最小边界矩形(MBR),空间对象的MBR被包含于R树的叶结点中。在R树空间索引中,设计一些虚拟的矩形目标,将一些空间位置相近的目标,包含在这个矩形内,这些虚拟的矩形作为空间索引,它含有所包含的空间对象的指针。虚拟矩形还可以进一步细分,即可以再套虚拟矩形形成多级空间索引。
一个围栏实体数据集构造Rtree的过程
R树的每个结点不存放空间要素的值。叶结点中存储该结点对应的空间要素的外包络矩形和空间要素标识,这个外包络矩形是个广义上的概念,二维上是矩形,三维空间上就是长方体,以此类推到高维空间。非叶结点(叶结点的父亲、祖先结点)存放其子女结点集合的整体外包络矩形和指向其子女结点的指针。注意,空间要素相关的信息只存在叶结点上。Rtree其最终空间存储形态可视化为:
在构造R树的时候,尽可能让空间要素的空间位置的远近体现在其最近的共同祖先的远近上,形象的说就是让聚集在一起的空间要素尽可能早的组合在一起,也相对减少了每一层的空间冗余;相对Geohash等固定大小格网,Rtree它按数据本身来组织索引结构。这使其具有很强的灵活性和可调节性。从而提高查询搜索效率;但其不足之处也是在末端叶子节点的聚合中,难以做到平衡,同时也会在同级中的MBR存在相互覆盖而增加的冗余部分,这会增加一定的匹配链路,从而影响查询效率。
(2)算法实践
采用R树数据结构进行索引,即对被索引数据集构建Rtree索引,推荐通用的时空数据JTS开源库,或者专门实现Rtree功能且支持更丰富查询方式(如领域TOP N搜索等)的JSI库(GitHub - aled/jsi: Java Spatial Index)。
在odps上实现R树索引推荐两种做法:一种是被索引数据集不太大且静态时,可以直接在UDF中以文件方式存储,使用UDF时先索引构建后进行点索引;另一种当被索引空间数据集较大,或数据集动态更新时,可以以表资源的方式进行数据加载。以下是部分Rtree的实践实例(JTS库-Java):
<dependency>
<groupId>com.vividsolutions</groupId>
<artifactId>jts</artifactId>
<version>1.13</version>
</dependency>
/**
* 离线AOI数据空间索引Rtree的构建。
*/
import com.aliyun.odps.udf.UDF;
import java.util.List;
import com.aliyun.odps.udf.ExecutionContext;
import com.aliyun.odps.udf.UDFException;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.index.strtree.STRtree;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
public class IndexAoiByRtreeInit extends UDF{
private STRtree allAoiRtree =null; //R树结点容量设置
private static GeometryFactory geometryFactory = new GeometryFactory();
@Override
public void setup(ExecutionContext ctx) throws UDFException{
super.setup(ctx);
try {
if (allAoiRtree!=null){
return ;
}
allAoiRtree=new STRtree(4);
Iterable<Object[]> row=ctx.readResourceTable("##data1");
if (row == null){
return ;
}
row.forEach(item ->{
AllRangeAoi tempaoi= new AllRangeAoi();
tempaoi.id=String.valueOf(item[0]);
tempaoi.name=String.valueOf(item[1]);
tempaoi.type=String.valueOf(item[2]);
tempaoi.citycode=String.valueOf(item[3]);
String wkt=String.valueOf(item[4]);
tempaoi.area=String.valueOf(item[5]);
tempaoi.aoiCenterlng=Double.parseDouble(String.valueOf(item[6]));
tempaoi.aoiCenterlat=Double.parseDouble(String.valueOf(item[7]));
tempaoi.version="1.0";
try {
tempaoi.polygon=createPolygonByWKT(wkt);
} catch (ParseException e) {
e.printStackTrace();
}
Envelope envelope=tempaoi.polygon.getEnvelopeInternal();
allAoiRtree.insert(envelope, tempaoi);
});
allAoiRtree.build();
System.out.println("AllAoiRtree depth:"+allAoiRtree.depth());
System.out.println("AllAoiRtree size:"+allAoiRtree.size());
} catch (Exception e) {
throw new UDFException(e);
}
}
public String evaluate(String longitude,String latitude) {
try{
AllRangeAoi allRangeAoi=indexAllAoiArea(Double.parseDouble(longitude),Double.parseDouble(latitude));
String result="";
if(allRangeAoi!=null&&allRangeAoi.getId()!=null){
String id = allRangeAoi.getId();
String citycode = allRangeAoi.getCityCode();
String type = allRangeAoi.getType();
Double aoiCenterlng = allRangeAoi.getAoiCenterlng();
Double aoiCenterlat = allRangeAoi.getAoiCenterlat();
result=new StringBuilder(id).append(",").append(citycode).append(",").append(aoiCenterlng).append(",").append(aoiCenterlat).append(",")
.append(type).toString();
}
return result;
}catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static Polygon createPolygonByWKT(String Polygon_points) throws ParseException{
WKTReader reader = new WKTReader( geometryFactory );
Polygon polygon = (Polygon) reader.read(Polygon_points);//"POLYGON((20 10, 30 0, 40 10, 30 20, 20 10))";
return polygon;
}
public STRtree getAllAoiRtree(){
return allAoiRtree;
}
@SuppressWarnings("unchecked")
public AllRangeAoi indexAllAoiArea(double longitude,double latitude) {
AllRangeAoi rangeAoi=new AllRangeAoi();
try {
Coordinate coord = new Coordinate(longitude,latitude);
Point point = geometryFactory.createPoint( coord );
Envelope enve1=point.getEnvelopeInternal();
List<AllRangeAoi> aoiList=allAoiRtree.query(enve1);
double currentDistance=9999.0,tempdistance=0.0;
if(aoiList!=null&&aoiList.size()==1){
rangeAoi=aoiList.get(0);
// 如果不包含,进行二次召回逻辑,索引200米范围内AOI
// 特殊场景,L型AOI,由于RTree是根据四个点进行召回,会误召回
if (!rangeAoi.polygon.contains(point)) {
rangeAoi=null;
// enve1.expandBy(0.002);
// aoiList=allAoiRtree.query(enve1);
// if(aoiList!=null){
// for(AllRangeAoi aoi:aoiList) {
// tempdistance=point.distance(aoi.getPolygon());
// if(point.distance(aoi.getPolygon())<currentDistance){
// rangeAoi=aoi;
// currentDistance=tempdistance;
// }
// }
// }
}
}
else if(aoiList!=null&&aoiList.size()>1){
for(AllRangeAoi aoi:aoiList) {
if(point.isWithinDistance(aoi.getPolygon(), 0)==true){//covers与contains区别:包含,不含边界
tempdistance=(longitude-aoi.aoiCenterlng)*(longitude-aoi.aoiCenterlng)+(latitude-aoi.aoiCenterlat)*(latitude-aoi.aoiCenterlat);
if(tempdistance<currentDistance){
rangeAoi=aoi;
currentDistance=tempdistance;
}
}
}
// if(rangeAoi==null||rangeAoi.id==null){
// for(AllRangeAoi aoi:aoiList) {
// tempdistance=point.distance(aoi.getPolygon());
// if(tempdistance<currentDistance){
// rangeAoi=aoi;
// currentDistance=tempdistance;
// }
// }
// }
}
//未落入任何AOI,则索引200米范围内AOI
// else{
// enve1.expandBy(0.002);
// aoiList=allAoiRtree.query(enve1);
// if(aoiList!=null){
// for(AllRangeAoi aoi:aoiList) {
// tempdistance=point.distance(aoi.getPolygon());
// if(point.distance(aoi.getPolygon())<currentDistance){
// rangeAoi=aoi;
// currentDistance=tempdistance;
// }
// }
// }
// }
} catch (Exception e) {
e.printStackTrace();
}
return rangeAoi;
}
public class AllRangeAoi{
String id;
String name;
String type;
String citycode;
String area;
double aoiCenterlng;
double aoiCenterlat;
String version;
Polygon polygon;
public String getId(){
return id;
}
public Polygon getPolygon(){
return polygon;
}
public String getType(){
return type;
}
public String getCityCode() {
return citycode;
}
public double getAoiCenterlng() {
return aoiCenterlng;
}
public double getAoiCenterlat() {
return aoiCenterlat;
}
}
public static void main(String[] args) {
IndexAoiByRtreeInit indexOfAllRangeAoi=new IndexAoiByRtreeInit();
AllRangeAoi allRangeAoi=null;
long start = System.currentTimeMillis();
for(int i=0;i<30;i++){
allRangeAoi=indexOfAllRangeAoi.indexAllAoiArea(120.338715+i*0.005, 30.177994+i*0.005);
System.out.println("AllAoi id:"+allRangeAoi.id);
}
long end = System.currentTimeMillis();
System.out.println("request time:" + (end - start));
System.out.println("AllAoiRtree depth:"+indexOfAllRangeAoi.getAllAoiRtree().depth());
}
}
2.3 Google S2索引
Google S2顾名思义是Google公司提出的一种类似Geohash式空间降维的空间索引算法,也较多地用于滴滴、UBER等强时空相关的科技公司中,其性能也较好。S2其实是来自几何数学中的一个数学符号 S²,它表示的是单位球。S2 这个库其实是被设计用来解决球面上各种几何问题的。
(1)算法原理
Google S2与Geohash相同的是,都是通过某种空间填充曲线来对多维地理空间数据进行降维至一维,从而提升索引效果,不同的是Google S2采用的是更严密希尔伯特曲线(Hilbert curve),如下图:
希尔伯特曲线的特点:
- 降维。作为空间填充曲线,希尔伯特曲线可以对多维空间有效的降维。
- 稳定。当n阶希尔伯特曲线,n趋于无穷大的时候,曲线上的点的位置基本上趋于稳定。
- 连续。希尔伯特曲线是连续的,所以能保证一定可以填满空间。
作为同为降维算法,相对Geohash,s2有30级,Geohash只有12级。S2的层级变化较平缓,方便选择。另S2功能强大,解决了向量计算,面积计算,多边形覆盖,距离计算等问题,减少了开发工作量。Google S2索引算法其最终空间存储形态与部分格网精度如下:
(2)算法实践
Google S2算法库的使用实例(Java):
(https://github.com/google/s2-geometry-library-java/tree/master/src/com/google/common/geometry)
<!--google的S2包-->
<dependency>
<groupId>io.sgr</groupId>
<artifactId>s2-geometry-library-java</artifactId>
<version>1.0.0</version>
</dependency>
<!--附带的google common组件包-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
1)经纬度 转 CellId
//注意使用的是WGS84坐标(GPS导航坐标)
//parent()可以指定等级,默认是30级
double lat = 36.683;
double lng = 117.1412;
int currentlevel = 4;
S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lng);
S2CellId cellId = S2CellId.fromLatLng(s2LatLng ).parent(currentlevel);
//CellID(face=1, pos=15d0000000000000, level=4)
System.out.println("CellID" + cellid);
//CellID.pos:1571756269952303104
System.out.println("CellID.pos:" + cellid.pos());
//CellID.id: 3877599279165997056,level:4
System.out.println("CellID.id: " + cellid.id() + ",level:" + cellid.level());
2)CellId 转 经纬度
S2LatLng s2LatLng = new S2CellId(cellId.id()).toLatLng();
Double lat = s2LatLng.latDegrees();
Double lng = s2LatLng.lngDegrees();
//经纬度转S2LatLng
S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lng);
//S2LatLng转S2CellId
S2CellId cellId = S2CellId.fromLatLng(s2LatLng);
//S2CellId转token
String token=s2CellId.toToken(); //任意形状的所有S2块的token集合,可以借用工具在地图上显示
//token转S2CellId
S2CellId s2CellId = S2CellId.fromToken(token);
//S2LatLng转point
S2Point point = s2LatLng.toPoint();
//point转S2LatLng
S2LatLng latLng = new S2LatLng(point);
3)S2计算距离
S2LatLng startS2 = S2LatLng.fromDegrees(55.8241, 137.8347);
S2LatLng endS2 = S2LatLng.fromDegrees(55.8271, 137.8347);
double distance = startS2.getEarthDistance(endS2);
System.out.println("距离为:"+distance+" m");
4)经纬度构建S2矩形
S2LatLng startS2 = S2LatLng.fromDegrees(0.8293, 72.004); //左下角
S2LatLng endS2 = S2LatLng.fromDegrees(55.8271, 137.8347); //右上角
S2LatLngRect rect = new S2LatLngRect(startS2, endS2);
5)经纬度构建S2多边形
//注意,一般需要多边形内侧,此处需要按照逆时针顺序添加。
//多边形经纬度可借用工具获取
List<S2Point> pointList = Lists.newArrayList();
pointList.add(S2LatLng.fromDegrees(lat, lng).toPoint());
pointList.add(S2LatLng.fromDegrees(lat, lng).toPoint());
pointList.add(S2LatLng.fromDegrees(lat, lng).toPoint());
pointList.add(S2LatLng.fromDegrees(lat, lng).toPoint());
S2Loop s2Loop = new S2Loop(pointList);
S2Polygon s2Polygon = new S2Polygon(s2Loop);
6)经纬度构建圆形
double radius = 600.5; //半径
Double capHeight = (2 * S2.M_PI) * (radius / 40075017);//40075017为地球周长
S2LatLng s2LatLng= S2LatLng.fromDegrees(lat, lng);
S2Cap cap = S2Cap.fromAxisHeight(s2LatLng.toPoint(),capHeight * capHeight / 2);
7)获取任意形状内所有S2块
//S2Region cap 任意区域
S2RegionCoverer coverer = new S2RegionCoverer();
//最小格子和最大格子,总格子数量
coverer.setMaxLevel(15);
coverer.setMinLevel(7);
coverer.setMaxCells(200);
List<S2CellId> list = coverer.getCovering(cap).cellIds();
for (S2CellId s:list) {
System.out.println(s);
}
//可以用于区域内目标检索,根据cellid建立索引,查询区域内cellid in (list)的餐馆、出租车
8)判断点是否在任意形状内
//S2Region cap 任意区域
S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lng);
boolean contains = cap.contains(s2LatLng.toPoint());
System.out.println(contains);
9)不同等级S2块包含的S2子块
public static List<S2CellId> childrenCellId(S2CellId s2CellId, Integer desLevel) {
return childrenCellId(s2CellId, s2CellId.level(), desLevel);
}
//递归调用,每个格子一分为四
private static List<S2CellId> childrenCellId(S2CellId s2CellId, Integer curLevel, Integer desLevel) {
if (curLevel < desLevel) {
//计算当前格子每个格子的差值
long interval = (s2CellId.childEnd().id() - s2CellId.childBegin().id()) / 4;
List<S2CellId> s2CellIds = Lists.newArrayList();
for (int i = 0; i < 4; i++) {
long id = s2CellId.childBegin().id() + interval * i;
s2CellIds.addAll(childrenCellId(new S2CellId(id), curLevel + 1, desLevel));
}
return s2CellIds;
} else {
return Lists.newArrayList(s2CellId);
}
}
10)判断当前cellId的level
getLevel(cellId.id())
//判断当前cellId的level
private static int getLevel(long input) {
int n = 0;
while (input % 2 == 0) {
input = input / 2;
n++;
}
return 30 - n / 2;
}
Google S2算法库的使用实例(Python):
import s2sphere
def get_cellid_from_latlng(lat, lng, level=20):
ll = s2sphere.LatLng.from_degrees(lat, lng)
cell = s2sphere.CellId().from_lat_lng(ll)
return cell.parent(level).to_token()
def lat_lng_to_cell_id(lat, lng, level=10):
region_cover = s2sphere.RegionCoverer()
region_cover.min_level = level
region_cover.max_level = level
region_cover.max_cells = 1
p1 = s2sphere.LatLng.from_degrees(lat, lng)
p2 = s2sphere.LatLng.from_degrees(lat, lng)
covering = region_cover.get_covering(
s2sphere.LatLngRect.from_point_pair(p1, p2))
# we will only get our desired cell ;)
return covering[0].id()
def middle_of_cell(cell_id):
cell = s2sphere.CellId(cell_id)
lat_lng = cell.to_lat_lng()
return lat_lng.lat().degrees, lat_lng.lng().degrees
def coords_of_cell(cell_id):
cell = s2sphere.Cell(s2sphere.CellId(int(cell_id)))
coords = []
for v in range(0, 4):
vertex = s2sphere.LatLng.from_point(cell.get_vertex(v))
coords.append([vertex.lat().degrees, vertex.lng().degrees])
return coords
def get_position_from_cell(cell_id):
cell = s2sphere.CellId(id_=int(cell_id)).to_lat_lng()
return s2sphere.math.degrees(cell.lat().radians), s2sphere.math.degrees(cell.lng().radians), 0
if __name__ == "__main__":
print(get_cellid_from_latlng(39.993379, 116.513977, 10))
print(get_cellid_from_latlng(39.993379, 116.513977, 11))
print(lat_lng_to_cell_id(39.993379, 116.513977, 20))
print(middle_of_cell(lat_lng_to_cell_id(39.993379, 116.513977, 20)))
print(coords_of_cell(lat_lng_to_cell_id(39.993379, 116.513977, 20)))
print(get_position_from_cell(lat_lng_to_cell_id(39.993379, 116.513977, 20)))
3 复杂场景中的时空索引应用解决方案
3.1 场景轨迹数据的时空索引算法
在菜鸟快递员上门揽件、饿了么骑手接单派送场景中,除了静态的用户/商户POI需要构建索引,快递员/骑手的实时轨迹数据也需要进行实时召回,而轨迹数据的累积也会使索引数据集不断增大,如何设计一个面向复杂场景--既需要满足一般POI/AOI的静态索引,也兼顾对大规模历史轨迹(对其行为规律的发现)和实时轨迹位置召回,且高效的时空数据索引?
这里介绍个人对Rtree基础上改进的一种时空索引召回算法--Scalable balanced st-rtree:
(1)现有典型时空索引算法及其优缺点:
(2)高性能&兼容性Scalable balanced st-rtree的建模:
Scalable balancedst-rtree通过轨迹的时间切片及分段间link,较好地解决了带时间维度轨迹数据的标准化问题,使其可以被容纳到Rtree空间索引结构中;另针对Rtree的因层级MBR之间的重叠而导致的重复搜索问题,这里在每一层级MBR中引入balanced kmeans聚类,使不同MBR内的node簇的覆盖度极小化,从而减少不同层级的搜索对比次数,最终达到提升空间索引效率的目的。
(3)几种常用索引算法的指标对比
以上对比可以看出,在全域AOI数据集的索引中,城市空间覆盖率99.8%,准确率(围栏及标签)90%+,时空索引相对传统Geohash/Rtree主流传统算法效率提升10%+。
3.2 静态POI/AOI/路网等空间数据索引算法解决方案
(1)静态离线时空数据的索引解决方案,以上3种典型算法都可以满足要求,不同的是,Geohash和Google S2都是预先生成索引表的方式,而Rtree类Tree算法则是需要实时导入数据至内存进行构建索引引擎(构建一次即可)--这会对内存资源有一定要求。具体地:
- 小数据量时:推荐Geohash即可,易于理解与实现,操作简单,召回需注意邻近边界问题,可通过召回邻近9宫格方式解决;
- 需邻域/范围召回时:推荐Rtree索引,MBR的索引方式针对邻域召回更友好,相关开源库已有完整函数实现。
- 大规模数据时:如果内存允许,可以使用Rtree,否则推荐Google S2降维的索引方式,相对Geohash格网冗余度较低,性能较好。
(2)时空索引在线索引服务的构建,可根据实际场景来采取解决方案,具体推荐下面3种解决方案:
- 低索引性能要求时:除了以上3种算法,也可直接采用数据库索引即可,推荐集团postgresSQL数据库,导入被索引空间数据集,可方便构建索引(系统自带空间索引);
- 高性能较小数据量时:文件或数据库存储,然后在应用中加载至内存构建索引,可采用Geohash或R树索引,推荐R树索引,RT性能一般可达1ms以内;
- 高性能大规模数据量时:在菜鸟的城市全域AOI中,围栏数据量是500万+,大小约8G,需要专门内存数据库来存储且方便构建空间索引,推荐阿里云的时空tair数据库。
4 参考文献
[2]空间索引_百度百科