mongodb地理信息应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/liyantianmin/article/details/82694063

1. 二维空间索引

     MongoDB支持二维空间索引,这是设计时考虑到基于位置的查询。例如“找到离目标位置最近的N条记录”。并且可以有效地作为附加条件过滤。

     如果需要使用这种索引,应确定对象中存储的字段是子对象或数组,前两个元素为X,Y坐标。

在文件中,存储的地理位置结构为:

{ loc : [ 50 , 30 ] }  

{ loc : { x : 50 , y : 30 } }  

{ loc : { lat : 40.739037, long: 73.992964 } }  

2d index:

使用2d index 能够将数据作为2维平面上的点存储起来, 在MongoDB 2.2以前 推荐使用2d index索引。
现在MongodDB 2.6了, 推荐使用2dspere index

2dsphere index:

2dsphere index 支持球体的查询和计算,同时它支持数据存储为GeoJSON 和传统坐标。

1.1. 创建二维地理位置索引 

创建二维地理位置索引,代码如下:  

db.places.ensureIndex( { loc : "2d" } )     //应该是固定格式  

     默认的,Mongo假设你索引的是经度/维度,因此配置了一个从-180到180的取值范围,如果你想索引更多,可以指定该索引的范围:

db.places.ensureIndex( { loc : "2d" } , { min : -500 , max : 500 } )  

     上面的代码将衡量索引保证存入的值在-500到500的范围之内。一般来说geo索引仅限于正方形以内且不包括边界以以外的范围,不能再边界上插入值,比如使用上面的代码,点(-500,-500)是不能被插入的。

每个Collection只能建立一个geospatial索引。

1.2. 索引

loc索引可以被用来精确匹配:

db.places.find( { loc : [50,50] } )  

另一种查询是找到目标点附近的点。

db.places.find( { loc : { $near : [50,50] } } )  

上面的一句将按离目标点(50,50)距离最近的100个点(距离倒序排列),如果想指定返回的结果个数,可以使用limit()函数,若不指定,默认是返回100个。

指定返回20个点

每个表Collection只能建立一个geo索引。

db.places.find( { loc : { $near : [50,50] } } ).limit(20)  

1.3. 组合索引

      Mongo空间索引可选的支持第二字段索引.如果想用坐标和其他属性同时作为条件查询,把这个属性也一同索引,附带其他属性加入索引中可使查询更快

代码如下:

db.places.ensureIndex( { loc : "2d" , category : 1 } );  

db.places.find( { loc : { $near : [50,50] }, category : 'coffee' } );  

2.距离查询    

2.1. 距离范围内查询

虽然find()语法为查询的首选,Mongo也提供来了 geoNear 命令来执行相似的函数。geoNear命令有一个额外的好处是结果中返回距离目标点的距离,以及一些过滤信息。

示例:

返回10条距离点(50,50)最近的记录,loc字段由该collection的空间索引自动检测后决定。

> db.runCommand( { geoNear : "places" , near : [50,50], num : 10 } );  
> db.runCommand({geoNear:"asdf", near:[50,50]})  
{  
        "ns" : "test.places",  
        "near" : "1100110000001111110000001111110000001111110000001111",  
        "results" : [  
                {  
                        "dis" : 69.29646421910687,  
                        "obj" : {  
                                "_id" : ObjectId("4b8bd6b93b83c574d8760280"),  
                                "y" : [  
                                        1,  
                                        1  
                                ],  
                                "category" : "Coffee"  
                        }  
                },  
                {  
                        "dis" : 69.29646421910687,  
                        "obj" : {  
                                "_id" : ObjectId("4b8bd6b03b83c574d876027f"),  
                                "y" : [  
                                        1,  
                                        1  
                                ]  
                        }  
                }  
        ],  
        "stats" : {  
                "time" : 0,  
                "btreelocs" : 1,  
                "btreelocs" : 1,  
                "nscanned" : 2,  
                "nscanned" : 2,  
                "objectsLoaded" : 2,  
                "objectsLoaded" : 2,  
                "avgDistance" : 69.29646421910687  
        },  
        "ok" : 1  
}  

2.2. 距离和注释型条件查询     

代码如下:

> db.runCommand( { geoNear : "places" , near : [ 50 , 50 ], num : 10,  

... query : { type : "museum" } } );  

2.3. 几何范围内查询

    在v1.3.4版本以后,可以几何边界查询。

     $within 参数可以代替$near来查找一个形状之内结果。同时,也支持$box(矩形)和$center(圆环)

    想要查找一个一个矩形之内所有的点,必须制定该矩形的左下角和右上角坐标:

Mongo代码  

> box = [[40, 40], [60, 60]]  

> db.places.find({"loc" : {"$within" : {"$box" : box}}})  

更多信息请参考

http://www.mongodb.org/display/DOCS/Geospatial+Indexing 

2.4. 查询操作符

查询操作符查看如下:

Name

Description

$geoWithin

Selects geometries within a bounding GeoJSON geometry.

$geoIntersects

Selects geometries that intersect with a GeoJSON geometry.

$near

Returns geospatial objects in proximity to a point.

$nearSphere

Returns geospatial objects in proximity to a point on a sphere.

Geometry Specifiers

Name

Description

$geometry

Specifies a geometry in GeoJSON format to geospatial query operators.

$maxDistance

Specifies a distance to limit the results of $near and $nearSphere queries.

$center

Specifies a circle using legacy coordinate pairs to $geoWithin queries when using planar geometry.

$centerSphere

Specifies a circle using either legacy coordinate pairs or GeoJSON format for $geoWithin queries when using spherical geometry.

$box

Specifies a rectangular box using legacy coordinate pairs for $geoWithin queries.

$polygon

Specifies a polygon to using legacy coordinate pairs for $geoWithin queries.

$uniqueDocs

Modifies a $geoWithin and $near queries to ensure that even if a document matches the query multiple times, the query returns the document once.

常用几何组合查询

Query Document

Geometry of the Query Condition

Surface Type for Query Calculation

Units for Query Calculation

Supported by this Index

Returns points, lines and polygons

{ $geoWithin : {

  $geometry : <GeoJSON Polygon>} }

polygon

sphere

meters

2dsphere

{ $geoIntersects : {

  $geometry : <GeoJSON>} }

point, line or polygon

sphere

meters

2dsphere

{ $near : {

  $geometry : <GeoJSON Point>,

  $maxDistance : d} }

point

sphere

meters

2dsphere

The index is required.

Returns points only

{ $geoWithin : {

  $box : [[x1, y1], [x2, y2]]} }

rectangle

flat

flat units

2d

{ $geoWithin : {

  $polygon : [[x1, y1], [x1, y2],[x2, y2],

[x2, y1]]} }

polygon

flat

flat units

2d

{ $geoWithin : {

  $center : [[x1, y1], r],} }

circular region

flat

flat units

2d

{ $geoWithin : {

  $centerSphere :

    [[x, y], radius]} }

circular region

sphere

radians

2d

2dsphere

{ $near : [x1, y1],

  $maxDistance : d}

3. 地理位置索引的常用案例

MongoDB地理位置索引常用的有两种。

2d 平面坐标索引,适用于基于平面的坐标计算。也支持球面距离计算,不过官方推荐使用2dsphere索引。 

2dsphere 几何球体索引,适用于球面几何运算 

关于两个坐标之间的距离,官方推荐2dsphere:

MongoDB supports rudimentary spherical queries on flat 2d indexes for legacy reasons. In general, spherical calculations should use a 2dsphere index, as described in 2dsphere Indexes.

不过,只要坐标跨度不太大(比如几百几千公里),这两个索引计算出的距离相差几乎可以忽略不计。

建立索引:

> db.places.ensureIndex({'coordinate':'2d'})  或者
> db.places.ensureIndex({'coordinate':'2dsphere'})


查询方式:

查询方式分三种情况:

Inclusion。范围查询,如百度地图“视野内搜索”。 

Inetersection。交集查询。不常用。 

Proximity。周边查询,如“附近500内的餐厅”。 

查询坐标参数则分两种:

坐标对(经纬度)根据查询命令的不同,$maxDistance距离单位可能是弧度和平面单位(经纬度的“度”)

3.1. 附近点查询

查询当前坐标附近的目标,由近到远排列。

3.1.1. 附近查询指令--$near或$nearSphere

可以通过$near或$nearSphere,这两个方法类似,但默认情况下所用到的索引和距离单位不同。其中radians表示弧度,meters表示米。

db.<collection>.find( { <location field> :
                   { $nearSphere: [ <x> , <y> ] ,
                     $maxDistance: <distance in radians>
                } } )


GeoJson $maxDistance距离单位默认为米:

  db.<collection>.find( { <location field> :
                   { $nearSphere :
                     { $geometry :
                        { type : "Point" ,
                          coordinates : [ <longitude> , <latitude> ] } ,
                       $maxDistance : <distance in meters>
             } } } )

查询方式:

> db.tb_coordinate_user.find({'coordinate':{$near: [113.944006, 22.543]}})

> db.tb_coordinate_user.find({'coordinate':{$nearSphere: [113.944006, 22.543]}})

查询结果:

{ 
    "_id" : 115, 
    "coordinate" : { 
        "longitude" : 121.4915, 
        "latitude" : 31.25933 
    }, 
    "title" : "仅售148元,市场价298元的星程上服假日酒店全日房一间入住一天", 
    "address" : "虹口区天水路90号" 
}

上述查询坐标[121.4905, 31.2646]附近的100个点,从最近到最远排序。

3.1.2. 指定结果数量之limit函数

$near和$nearSphere指令默认返回100条数据,也可以用limit()指定结果数量,如

> db.places.find({'coordinate':{$near: [121.4905, 31.2646]}}).limit(2)

指定最大距离 $maxDistance,单位为公里

> db.places.find({'coordinate':{$near: [121.4905, 31.2646], $maxDistance:2}})

代码如下:

用near,默认以度为单位,公里数除以111。

/**
 * @Route("/near", name="near")
 * @Template()
 */
public function nearAction(){
    $longitude = (float)$this->getRequest()->get('lon',121.4905);
    $latitude = (float)$this->getRequest()->get('lat',31.2646);
    //2km
    $max = (float)$this->getRequest()->get('max', 2);//返回2km内的结果
    $places = $this->getPlaceRepository()->createQueryBuilder()
        ->field('coordinate')->near($longitude, $latitude)
        ->maxDistance($max/111)
        ->getQuery()->toarray();
    return compact('places','max','longitude','latitude');
}


效果如下:

 

longitude: xxx, latitude: xxx为当前位置,在地图上显示了周边100条目标记录

3.2. 区域内搜索之$geoWithin指令

MongoDB中的范围搜索(Inclusion)主要用$geoWithin这个命令,它又细分为3种不同类型,如下:

(1)$box 矩形 

(2)$center 圆(平面),$centerSphere圆(球面) ,$center和$centerSphere在小范围内的应用几乎没差别(除非这个圆半径几百上千公里)

(3)$polygon 多边形 

3.2.1. 矩形区域之$box指令

使用指令$geoWithin 和$box,比如百度地图的视野内搜索(矩形)、或搜狗地图的“拉框搜索”。这个指令使用的是BasicCursor游标(即使建立了2d和2dsphere索引),表示遍历了所有的点,效率会较低。

定义一个矩形范围,需要指定两个坐标,在MongoDB的查询方式如下:

> db.places.find( 
    { 
        coordinate : { 
            $geoWithin : { 
                $box :[ [ 121.44, 31.25 ] , [ 121.5005, 31.2846 ] ] 
            } 
        } 
    } 
)


查询结果:

{ 
    "_id" : 90472, 
    "title" : "【鲁迅公园】仅售99元!酒店门市价288元的上海虹元商务宾馆客房一间入住一天", 
    "address" : "上海市虹口区柳营路8号", 
    "coordinate" : { 
        "longitude" : 121.47, 
        "latitude" : 31.27145 
    } 
}


演示代码:

/**
 * @Route("/box", name="box")
 * @Template()
 */
public function boxAction(){
    $request = $this->getRequest();
    $longitude = (float)$request->get('lon',121.462035);
    $latitude = (float)$request->get('lat',31.237641);
    $longitude2 = (float)$request->get('lon2',121.522098);
    $latitude2 = (float)$request->get('lat2',31.215284);
    $places = $this->getPlaceRepository()->createQueryBuilder()
        ->field('coordinate')->withinBox($longitude, $latitude, 
$longitude2, $latitude2)
        ->getQuery()->toarray();
    return compact('places','longitude','latitude', 'longitude2', 'latitude2');
}


访问效果如下:

3.2.2. 圆形区域之$center和$centerSphere指令

应用场景有:地图搜索租房信息等

查询以某坐标为圆心,指定半径的圆内的数据。

圆形区域搜索分为$center和$centerSphere这两种类型,它们的区别主要在于支持的索引和默认距离单位不同。

2d索引能同时支持$center和$centerSphere,2dsphere索引支持$centerSphere。

关于距离单位,$center默认是度,$centerSphere默认距离是弧度。

查询方式如下:

> db.places.find({'coordinate':{$geoWithin:{$centerSphere:[ [121.4905, 31.2646] ,0.6/111] }}})

> db.places.find({'coordinate':{$geoWithin:{$centerSphere:[ [121.4905, 31.2646] ,0.6/6371] }}})

查询结果

{ 
    "_id" : 115, 
    "coordinate" : { 
        "longitude" : 121.4915, 
        "latitude" : 31.25933 
    }, 
    "title" : "仅售148元,市场价298元的星程上服假日酒店全日房一间入住一天", 
    "address" : "虹口区天水路90号" 
}


代码如下(指定圆心坐标和半径):

/**
 * @Route("/center", name="center")
 * @Template()
 */
public function centerAction(){
    $request = $this->getRequest();
    $longitude = (float)$request->get('lon',121.4905);
    $latitude = (float)$request->get('lat',31.2646);
    //10km
    $max = (float)$request->get('max', 10);
    $places = $this->getPlaceRepository()->createQueryBuilder()
        ->field('coordinate')->withinCenter($longitude, $latitude, $max/111)
        ->getQuery()->toarray();
    return compact('places','max','longitude','latitude');
}

访问效果如下:

以longitude: xxx,latitude: xxx为中心点,半径10km的圆内

3.2.3. 多边形之$polygon指令

复杂区域内的查询,这个应用场景比较少见,使用指令$geoWithin 和$polygon。

指定至少3个坐标点,查询方式如下(五边形):

> db.places.find( { coordinate : { $geoWithin : { $polygon : [ 

    [121.45183 , 31.243816] ,

    [121.533181, 31.24344] ,

    [121.535049, 31.208983] ,

    [121.448955, 31.214913] ,

    [121.440619, 31.228748]

] } } } )

查询结果

{ 
    "_id" : 90078, 
    "title" : "仅售9.9元,市场价38元的燕太太燕窝单人甜品餐,用耐心守候一盅炖品,用爱滋补一生情谊", 
    "address" : "河南南路489号香港名都购物广场1F125燕太太燕窝", 
    "coordinate" : { 
        "longitude" : 121.48912, 
        "latitude" : 31.22355 
    } 
}


代码如下(这里为方便,直接写了5个坐标点):

/**
 * @Route("/polygon", name="polygon")
 * @Template()
 */
public function polygonAction(){
    $points = [];
    $points[] = [121.45183,31.243816];
    $points[] = [121.533181,31.24344];
    $points[] = [121.535049,31.208983];
    $points[] = [121.448955,31.214913];
    $points[] = [121.440619,31.228748];
    $sumlon = $sumlat = 0;
    foreach($points as $p){
        $sumlon += $p[0];
        $sumlat += $p[1];
    }
    $center = [$sumlon/count($points), $sumlat/count($points)]; 	$places=$this->getPlaceRepository()->createQueryBuilder()->field('coordinate')->withinPolygon($points[0], $points[1], $points[2], 
$points[3], $points[4])->getQuery()->toarray();
    return compact('places','points', 'center');
}

访问效果如下:

 

3.3. 附近距离查询之$geoNear指令

假设需要以当前坐标为原点,查询附近指定范围内的餐厅,并直接显示距离。

这个需求用前面提到的$near是可以实现的,但是距离需要二次计算。这里可以用$geoNear这个命令查询。

$geoNear与$near功能类似,但提供更多功能和返回更多信息,官方文档是这么解释的:The geoNear command provides an alternative to the $near operator. In addition to the functionality of $near, geoNear returns additional diagnostic information.

查询方式如下:

> db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], spherical: true,
 maxDistance:1/6371, num:2 })
{
    "ns" : "mongo_test.places",
    "near" : "1110001100111100001011010110010111001000110011111101",
    "results" : [
        {
            "dis" : 0.00009318095248858048,
            "obj" : {
                "_id" : 115,
                "coordinate" : {
                    "longitude" : 121.4915,
                    "latitude" : 31.25933
                },
                "title" : "仅售148元,市场价298元的星程上服假日酒店全日房一间入住一天,节假日通用,精致生活,品质享受",
                "address" : "虹口区天水路90号"
            }
        },
        {
            "dis" : 0.00010610660597329082,
            "obj" : {
                "_id" : 465,
                "coordinate" : {
                    "longitude" : 121.48406,
                    "latitude" : 31.26202
                },
                "title" : "热烈庆祝xxx成立8周年!",
                "address" : "虹口区四川北路"
            }
        }
    ],
    "stats" : {
        "time" : 0,
        "btreelocs" : 0,
        "nscanned" : 18,
        "objectsLoaded" : 12,
        "avgDistance" : 0.00009964377923093564,
        "maxDistance" : 0.0001064199324957278
    },
    "ok" : 1
}

可以看到返回了很多详细信息,如查询时间、返回数量、最大距离、平均距离等。

results里面直接返回了距离目标点的距离dis。

演示代码(函数distanceMultiplier与单位有关,后文会解释):

/**
 * @Route("/distance", name="distance")
 * @Template()
 */
public function distanceAction(){
    $longitude = (float)$this->getRequest()->get('lon',121.4905);
    $latitude = (float)$this->getRequest()->get('lat',31.2646);
    //2km
    $max = (float)$this->getRequest()->get('max', 2);
    $places = $this->getPlaceRepository()->createQueryBuilder()
        ->field('coordinate')
        ->geoNear($longitude, $latitude)
        ->spherical(true)
        ->distanceMultiplier(6371)
        ->maxDistance($max/6371)
        ->limit(100)
        ->getQuery()
        ->execute()
        ->toArray();
    return compact('places','longitude', 'latitude', 'max');
}

访问效果如下:

距离xxx米

3.3.1. 具体距离查询

查询2500米范围内的点,最多返回10个结果:

db.runCommand({ geoNear : "tb_coordinate_user" , near : [113.944006, 22.543], distanceMultiplier: 6378137, maxDistance:2500/6378137 ,spherical:true,num:10} )

3.3.2. 联合查询

其他参数还有个Query,用于联合查询,如:

db.runCommand({ geoNear : “collectionName” , near : [120.123456,30.654321], distanceMultiplier: 6378137, num : 10, spherical:true , query:{Name:”肯德基”}} )

之前的代码中,坐标都是按照 longitude, latitude这个顺序的。

这个是官方建议的坐标顺序,但是网上很多文档是相反的顺序,经测试发现,只要查询时指定的坐标顺序与数据库内的坐标顺序一致,出来的结果就是正确的,没有特定的先后顺序之分。

鉴于官方文档的推荐,我在此还是建议大家按照官方推荐的顺序。

$near和$center从需求上看差不多,但是$center或$centerSphere是属于$geoWithin的类型,$near方法查询后会对结果集对距离进行排序,而$geoWithin是无序的。

其他的查询如geoIntersect查询在开源的演示程序可以参考。

3.4. 索引要求区别

$near命令必须要求有索引。

$geoWithin可以无需索引,但是建议还是建立索引以提升性能。

3.5. 距离单位

MongoDB查询地理位置默认有3种距离单位:

(1)米(meters) 

(2)平面单位(flat units,可以理解为经纬度的“一度”) 

(3)弧度(radians)。 

3.5.1. 米

通过GeoJSON格式查询,单位默认是米。

查询如下:

距离经纬为[118.783799,31.979234]的5000米范围内的点。

GeoJson $maxDistance距离单位默认为米。查询方式如下:

db.<collection>.find( { <location field> :

                   { $nearSphere :

                     { $geometry :

                        { type : "Point" ,

                          coordinates : [ <longitude> , <latitude> ] } ,

                       $maxDistance : <distance in meters>

             } } } )

示例如:

db.tb_coordinate_user.find({'coordinate':{$near: {$geometry: {type: "Point" ,coordinates: [113.944006, 22.543]},$maxDistance: 1000}}})

3.5.2. 弧度与度

下面的查询语句指定距离内的目标:

> db.places.find({'coordinate':{$near: [121.4905, 31.2646], $maxDistance:2}})

现在$maxDistance参数是2公里。

$geoNear返回结果集中的dis,如果指定了spherical为true, dis的值为弧度,不指定则为度。

如果用弧度查询,则以公里数除以6371,如“附近500米的餐厅”:

> db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], spherical: true,

 $maxDistance: 0.5/6371 })

如果不用弧度,以水平单位(度)查询时,距离单位是以公里数除以111(推荐值),原因如下:经纬度的一度,分为经度一度和纬度一度。地球不同纬度之间的距离是一样的,地球子午线(南极到北极的连线)长度39940.67公里,纬度一度大约110.9公里,但是不同纬度的经度一度对应的长度是不一样的。在地球赤道,一圈大约为40075KM,除以360度,每一个经度大概是:40075/360=111.32KM。上海,大概在北纬31度,对应一个经度的长度是:40075*sin(90-31)/360=95.41KM。北京在北纬40度,对应的是85KM。前面提到的参数111,这个值只是估算,并不完全准确,任意两点之间的距离,平均纬度越大,这个参数则误差越大。

详细原因可以参考wiki上的解释:http://en.wikipedia.org/wiki/Latitude

但是,即便如此,“度”这个单位只用于平面,由于地球是圆的,在大范围使用时会有误差。

官方建议使用sphere查询方式,也就是距离单位用弧度。

$geoNear返回结果集中,指定 spherical为true,结果中的dis需要乘以6371就能换算为km:

> db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], 
spherical: true, num:1 })
{
    "ns" : "mongo_test.places",
    "near" : "1110001100111100001011010110010111001000110011111101",
    "results" : [
        {
            "dis" : 0.00009318095248858048,
            "obj" : {
                "_id" : 115,
                "coordinate" : {
                    "longitude" : 121.4915,
                    "latitude" : 31.25933
                },
                "title" : "仅售148元",
                "address" : "虹口区天水路"
            }
        }
 
    ],
    "stats" : {
        "time" : 0,
        "btreelocs" : 0,
        "nscanned" : 18,
        "objectsLoaded" : 12,
        "avgDistance" : 0.00009964377923093564,
        "maxDistance" : 0.0001064199324957278
    },
    "ok" : 1
}

不指定sphericial,结果中的dis乘以就能111换算为km:

> db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], num:1 })
{
    "ns" : "mongo_test.places",
    "near" : "1110001100111100001011010110010111001000110011111101",
    "results" : [
        {
            "dis" : 0.005364037658335473,
            "obj" : {
                "_id" : 115,
                "coordinate" : {
                    "longitude" : 121.4915,
                    "latitude" : 31.25933
                },
                "title" : "仅售148元",
                "address" : "虹口区天水路"
            }
        }   
 
    ],
    "stats" : {
        "time" : 0,
        "btreelocs" : 0,
        "nscanned" : 18,
        "objectsLoaded" : 12,
        "avgDistance" : 0.006150808243357531,
        "maxDistance" : 0.00695541352612983
    },
    "ok" : 1
}

总结如下,适用于大部分常用的函数。

查询命令

距离单位

说明

$near

$nearSphere

弧度

$center

$centerSphere

弧度

$polygon

$geoNear

度或弧度

指定参数spherical为true则为弧度,否则为度。

使用度(结果中dis乘以111换算为km);使用弧度(结果中dis乘以6371换算为km)

如果坐标以GeoJSON格式,则单位都为米。

详细参考:http://docs.mongodb.org/manual/reference/operator/query-geospatial/

3.6. 结果倍增函数distanceMultiplier 

geoNear返回结果中的dis是与目标点的距离,其距离单位是跟查询单位一致的,需要二次计算。

可以直接在查询时指定 distanceMultiplier ,它会将这个参数乘以距离返回,如指定为6371,返回的就是公里数。

> db.runCommand({ geoNear : "places", near : [121.4905, 31.2646], spherical : true,
 maxDistance : 1/6371, distanceMultiplier: 6371})
{
    "ns" : "mongo_test.places",
    "near" : "1110001100111100001011010110010111001000110011111101",
    "results" : [
        {
            "dis" : 0.5936558483047463,
            "obj" : {
                "_id" : 115,
                "coordinate" : {
                    "longitude" : 121.4915,
                    "latitude" : 31.25933
                },
                "title" : "仅售148元",
                "address" : "虹口区天水路"
            }
        },
        …
        …
    ],
    "stats" : {
        "time" : 0,
        "btreelocs" : 0,
        "nscanned" : 15,
        "objectsLoaded" : 9,
        "avgDistance" : 0.6348305174802911,
        "maxDistance" : 0.0001064199324957278
    },
    "ok" : 1
}

上面的返回结果中dis的值,已经是km单位的了。

4. 地理位置索引原理--Geohash

地理位置索引支持是MongoDB的一特色。Geohash就是将地理位置转化为可建立B+Tree形式的索引。

首先将需要索引的整个地图分成16×16的方格,如下图(左下角为坐标0,0 右上角为坐标16,16):

单纯的[x,y]的数据是无法建立索引的,所以MongoDB在建立索引的时候,会根据相应字段的坐标计算一个可以用来做索引的hash值,即geohash,下面我们以地图上坐标为[4,6]的点(图中红叉位置)为例。

第一步将整个地图分成等大小的四块,如下图:

划分成四块后定义这四块的值,如下(左下为00,左上为01,右下为10,右上为11):

01

11

00

10

这样[4,6]点的geohash目前在00的块中,则 geohash目前的值为00

继续再将该小块进行切割,如下:

这时[4,6]点位于该块的右上区域,右上的值为11,这样[4,6]点的geohash目前的值就变为:0011

继续把该块进行划分为4块:

目前geohash的值为001101

继续划分,则最终知道[4,6]点的geohash值为:00110100

使用geohash做索引,则地图上点相近的点就成了有相同前缀的geohash值。

geohash值的精度是与划分地图的次数成正比的,上例对地图划分了四次。

MongoDB默认是进行26次划分,这个值在建立索引时是配的。具体建立二维地理位置索引的命令如下:

db.map.ensureIndex({point : "2d"}, {min : 0, max : 16, bits : 4})

其中的bits参数就是划分次数,默认为26次。

5. 性能测试

目前测试数据来自网络。测试在单实例情况下的附近查询效率。

5.1. Mongodb附近查询

测试环境:

Mac Pro(处理器Intel Core i5、2.4 GHz、2核、16G内存) + Mongo 2.6 + Rails4.1.4

model 代码:

class User
   field :location,        type: Array
   index({ location: "2d"}, { background: true })
   # 或者 index({ location: "2dsphere"}, { background: true })
  class << self
    def nearby(coordinate, max_distance=5)
       # 5公里内, 符合条件的记录, 默认取100个。同时会按照距离的远近 进行排序。
       self.geo_near(coordinate).max_distance(max_distance.fdiv   6371).spherical.distance_multiplier(6371000)
    end
  endend


使用命令1的 查询时间:

User.nearby([117.490219, 40.962954]).count

# 5公里内, 符合条件的记录, 默认取100个。同时会按照距离的远近 进行排序。

# 距离 存在 attributes["geo_near_distance"] 中, 

代码如下

User.nearby([117.490219, 40.962954]).first["geo_near_distance"]

通过测试发现, 使用2d index 在数据量 变大的过程中,查询时间会变的非常慢, 而使用2d sphere index 基本可以控制在0.5s左右。

使用命令2:

User.where(:location => {"$within" => {"$centerSphere" => [[116.490219, 42.962954], (5.fdiv(6371) )]}}).count

# 5公里内, 符合条件的记录、默认会选出所有符合条件的结果。

# 缺点是 需要自己进行排序, 且需要自己计算 geo_near_distance。

命令2 因为不需要 对 符合条件的结果 进行排序, 所以 查询时间 相比 命令1的 查询时间大大减少。

备注:
每1w条 数据的插入时间是 8s左右。

MongoDB查询地理位置默认有3种距离单位:
米(meters)
平面单位(flat units,可以理解为经纬度的“一度”)
弧度(radians)

2d索引能同时支持$center和$centerSphere,
2dsphere索引支持$centerSphere。
关于距离单位,$center默认是度,$centerSphere默认距离是弧度。

5.2. 对比PostgreSQL附近查询

使用PostgreSQL存储地理位置信息

测试环境介绍:

Mac Pro(处理器Intel Core i5、2.4 GHz、2核、16G内存) + PostgreSQL 9.3.5 + PostGis2.1.3(PostgreSQL的扩展) + Rails4.1.4

备注:postgis完整实现了opengis 的 Simple Features标准之中的空间对象模型和函数

测试命令

User.select("users.*, st_distance(location, 'point(116.458104 39.966293)') as distance").where("st_dwithin(location, 'point(116.458104 39.966293)', 10000)").order("distance")

# 查找10公里 内结果, 并按照距离进行排序

测试结果:

 

关于地理位置的计算, 其实Mysql、MongoDB、PostgreSQL都支持,只不过MongoDB 和PostgreSQL 支持的更好一些。
而且通过 测试我们可以发现 MongoDB 在数据量变大的时候,查询的瓶颈会变的越来越大。反过来看PostgreSQL,它的查询时间基本是随着 数据量的增长,而线性增长的。

猜你喜欢

转载自blog.csdn.net/liyantianmin/article/details/82694063