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 和传统坐标。
创建二维地理位置索引,代码如下:
db.places.ensureIndex( { loc : "2d" } ) //应该是固定格式
默认的,Mongo假设你索引的是经度/维度,因此配置了一个从-180到180的取值范围,如果你想索引更多,可以指定该索引的范围:
db.places.ensureIndex( { loc : "2d" } , { min : -500 , max : 500 } )
上面的代码将衡量索引保证存入的值在-500到500的范围之内。一般来说geo索引仅限于正方形以内且不包括边界以以外的范围,不能再边界上插入值,比如使用上面的代码,点(-500,-500)是不能被插入的。
每个Collection只能建立一个geospatial索引。
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)
Mongo空间索引可选的支持第二字段索引.如果想用坐标和其他属性同时作为条件查询,把这个属性也一同索引,附带其他属性加入索引中可使查询更快
代码如下:
db.places.ensureIndex( { loc : "2d" , category : 1 } );
db.places.find( { loc : { $near : [50,50] }, category : 'coffee' } );
虽然find()语法为查询的首选,Mongo也提供来了 geoNear 命令来执行相似的函数。geoNear命令有一个额外的好处是结果中返回距离目标点的距离,以及一些过滤信息。
示例:
返回10条距离点(50,50)最近的记录,loc字段由该collection的空间索引自动检测后决定。
代码如下:
> db.runCommand( { geoNear : "places" , near : [ 50 , 50 ], num : 10,
... query : { type : "museum" } } );
在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
查询操作符查看如下:
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}
|
|
|
|
|
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.
不过,只要坐标跨度不太大(比如几百几千公里),这两个索引计算出的距离相差几乎可以忽略不计。
建立索引:
查询方式分三种情况:
Inclusion。范围查询,如百度地图“视野内搜索”。
Inetersection。交集查询。不常用。
Proximity。周边查询,如“附近500内的餐厅”。
查询坐标参数则分两种:
坐标对(经纬度)根据查询命令的不同,$maxDistance距离单位可能是弧度和平面单位(经纬度的“度”)
查询当前坐标附近的目标,由近到远排列。
可以通过$near或$nearSphere,这两个方法类似,但默认情况下所用到的索引和距离单位不同。其中radians表示弧度,meters表示米。
> db.tb_coordinate_user.find({'coordinate':{$near: [113.944006, 22.543]}})
> db.tb_coordinate_user.find({'coordinate':{$nearSphere: [113.944006, 22.543]}})
查询结果:
$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。
longitude: xxx, latitude: xxx为当前位置,在地图上显示了周边100条目标记录
MongoDB中的范围搜索(Inclusion)主要用$geoWithin这个命令,它又细分为3种不同类型,如下:
(1)$box 矩形
(2)$center 圆(平面),$centerSphere圆(球面) ,$center和$centerSphere在小范围内的应用几乎没差别(除非这个圆半径几百上千公里)
(3)$polygon 多边形
使用指令$geoWithin 和$box,比如百度地图的视野内搜索(矩形)、或搜狗地图的“拉框搜索”。这个指令使用的是BasicCursor游标(即使建立了2d和2dsphere索引),表示遍历了所有的点,效率会较低。
定义一个矩形范围,需要指定两个坐标,在MongoDB的查询方式如下:
应用场景有:地图搜索租房信息等
查询以某坐标为圆心,指定半径的圆内的数据。
圆形区域搜索分为$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] }}})
查询结果
以longitude: xxx,latitude: xxx为中心点,半径10km的圆内
复杂区域内的查询,这个应用场景比较少见,使用指令$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]
] } } } )
查询结果
假设需要以当前坐标为原点,查询附近指定范围内的餐厅,并直接显示距离。
这个需求用前面提到的$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.
查询方式如下:
可以看到返回了很多详细信息,如查询时间、返回数量、最大距离、平均距离等。
results里面直接返回了距离目标点的距离dis。
演示代码(函数distanceMultiplier与单位有关,后文会解释):
访问效果如下:
距离xxx米
查询2500米范围内的点,最多返回10个结果:
db.runCommand({ geoNear : "tb_coordinate_user" , near : [113.944006, 22.543], distanceMultiplier: 6378137, maxDistance:2500/6378137 ,spherical:true,num:10} )
其他参数还有个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查询在开源的演示程序可以参考。
$near命令必须要求有索引。
$geoWithin可以无需索引,但是建议还是建立索引以提升性能。
MongoDB查询地理位置默认有3种距离单位:
(1)米(meters)
(2)平面单位(flat units,可以理解为经纬度的“一度”)
(3)弧度(radians)。
通过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}}})
下面的查询语句指定距离内的目标:
> 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:
不指定sphericial,结果中的dis乘以就能111换算为km:
总结如下,适用于大部分常用的函数。
查询命令 |
距离单位 |
说明 |
$near |
度 |
|
$nearSphere |
弧度 |
|
$center |
度 |
|
$centerSphere |
弧度 |
|
$polygon |
度 |
|
$geoNear |
度或弧度 |
指定参数spherical为true则为弧度,否则为度。 使用度(结果中dis乘以111换算为km);使用弧度(结果中dis乘以6371换算为km) |
如果坐标以GeoJSON格式,则单位都为米。
详细参考:http://docs.mongodb.org/manual/reference/operator/query-geospatial/
geoNear返回结果中的dis是与目标点的距离,其距离单位是跟查询单位一致的,需要二次计算。
可以直接在查询时指定 distanceMultiplier ,它会将这个参数乘以距离返回,如指定为6371,返回的就是公里数。
上面的返回结果中dis的值,已经是km单位的了。
地理位置索引支持是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次。
目前测试数据来自网络。测试在单实例情况下的附近查询效率。
测试环境:
Mac Pro(处理器Intel Core i5、2.4 GHz、2核、16G内存) + Mongo 2.6 + Rails4.1.4
model 代码:
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默认距离是弧度。
使用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,它的查询时间基本是随着 数据量的增长,而线性增长的。