随着近几年各种移动终端的迅速普及,基于地理位置的服务(LBS)和相关应用也愈来愈多,而支撑这些应用的最基础技术之一,就是基于地理位置信息的处理。我所在的项目也正从事相关系统的开发,咱们使用的是Symfony2+Doctrine2 ODM+MongoDB的组合。html
咱们将这些技术要点整理成文,但愿可以经过本文的介绍和案例,详细解释如何使用MongoDB进行地理位置信息的查询和处理。在文章的开头,咱们也会先介绍一下业界一般用来处理地理位置信息的一些方案并进行比较,让读者逐步了解使用MongoDB查询及处理地理位置信息的优点。mysql
本文使用了Symfony2和Doctrine2做为Web应用的开发框架,对于想了解Symfony2的数据库操做的读者来讲阅读本文也能够了解和掌握相关的技术和使用方法。git
无论是什么LBS应用,一个共同的特色就是:他们的数据都或多或少包含了地理位置信息。而如何对这些信息进行查询、处理、分析,也就成为了支撑LBS应用的最基础也是最关键的技术问题。github
而因为地理位置信息的特殊性,在开发中常常会有比较难以处理的问题出现,好比:因为用户所在位置的不固定性,用户可能会在很小范围内移动,而此时经纬度值也会随之变化;甚至在同一个位置,经过GPS设备获取到的位置信息也可能不同。因此若是经过经纬度去获取周边信息时,就很难像传统数据库那样作查询并进行缓存。算法
对于这个问题,有读者可能会说有别的处理方案,没错,好比只按经纬度固定的几位小数点作索引,好比按矩阵将用户划分到某固定小范围的区域(能够参考后文将会提到的geohash)等方式,虽然能够绕个弯子解决,但或多或少操做起来比较麻烦,也会牺牲一些精度,甚至没法作到性能的最优化,因此不能算做是最佳的解决办法。sql
而最近几年,直接支持地理位置操做的数据库层出不穷,其操做友好、性能高的特性也开始被咱们慢慢重视起来,其中的佼佼者当属MongoDB。mongodb
MongoDB在地理位置信息的处理上有什么优点?下面咱们经过一个简单的案例来对比一下各类技术方案之间进行进行地理位置信息处理的差别。数据库
对于任何LBS应用来讲,让用户寻找周围的好友可能都是一个必不可少的功能,咱们就以这个功能为例,来看看各类处理方案之间的差别和区别。缓存
咱们假设有以下功能需求:
显示我附近的人
由近到远排序
显示距离
排除一些不通用和难以实现的技术,咱们罗列出如下几种方案:
基于MySQL数据库
采用GeoHash索引,基于MySQL
MySQL空间存储(MySQL Spatial Extensions)
使用MongoDB存储地理位置信息
咱们一个个来分析这几种方案。
MySQL的使用很是简单。对于大部分已经使用MySQL的网站来讲,使用这种方案没有任何迁移和部署成本。
而在MySQL中查询“最近的人”也仅需一条SQL便可,
SELECT id, ( 6371 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians ( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance FROM places HAVING distance < 25 ORDER BY distance LIMIT 0 , 100;
注:这条SQL查询的是在lat,lng这个坐标附近的目标,而且按距离正序排列,SQL中的distance单位为千米。
但使用SQL语句进行查询的缺点也显而易见,每条SQL的计算量都会很是大,性能将会是严重的问题。
先别放弃,咱们尝试对这条SQL作一些优化。
能够将圆形区域抽象为正方形,以下图
根据维基百科上的球面计算公式,能够根据圆心坐标计算出正方形四个点的坐标。
而后,查询这个正方形内的目标点。
SQL语句能够简化以下:
SELECT * FROM places WHERE ((lat BETWEEN ? AND ?) AND (lng BETWEEN ? AND ?))
这样优化后,虽然数据不彻底精确,但性能提高很明显,而且能够经过给lat lng字段作索引的方式进一步加快这条SQL的查询速度。对精度有要求的应用也能够在这个结果上再进行计算,排除那些在方块范围内但不在原型范围内的数据,已达到对精度的要求。
但是这样查询出来的结果,是没有排序的,除非再进行一些SQL计算。但那又会在查询的过程当中产生临时表排序,可能会形成性能问题。
GeoHash是一种地址编码,经过切分地图区域为小方块(切分次数越多,精度越高),它能把二维的经纬度编码成一维的字符串。也就是说,理论上geohash字符串表示的并非一个点,而是一个矩形区域,只要矩形区域足够小,达到所需精度便可。(其实MongoDB的索引也是基于geohash)
如:wtw3ued9m就是目前我所在的位置,下降一些精度,就会是wtw3ued,再下降一些精度,就会是wtw3u。(点击连接查看坐标编码对应Google地图的位置)
因此这样一来,咱们就能够在MySQL中用LIKE ‘wtw3u%’来限定区域范围查询目标点,而且能够对结果集作缓存。更不会由于微小的经纬度变化而没法用上数据库的Query Cache。
这种方案的优势显而易见,仅用一个字符串保存经纬度信息,而且精度由字符串从头至尾的长度决定,能够方便索引。
但这种方案的缺点是:从geohash的编码算法中能够看出,靠近每一个方块边界两侧的点虽然十分接近,但所属的编码会彻底不一样。实际应用中,虽然能够经过去搜索环绕当前方块周围的8个方块来解决该问题,但一会儿将原来只须要1次SQL查询变成了须要查询9次,这样不只增大了查询量,也将本来简单的方案复杂化了。
除此以外,这个方案也没法直接获得距离,须要程序协助进行后续的排序计算。
MySQL的空间扩展(MySQL Spatial Extensions),它容许在MySQL中直接处理、保存和分析地理位置相关的信息,看起来这是使用MySQL处理地理位置信息的“官方解决方案”。但偏偏很惋惜的是:它却不支持某些最基本的地理位置操做,好比查询在半径范围内的全部数据。它甚至连两坐标点之间的距离计算方法都没有(MySQL Spatial的distance方法在5.*版本中不支持)
官方指南的作法是这样的:
GLength(LineStringFromWKB(LineString(point1, point2)))
这条语句的处理逻辑是先经过两个点产生一个LineString的类型的数据,而后调用GLength获得这个LineString的实际长度。
这么作虽然有些复杂,貌似也解决了距离计算的问题,但读者须要注意的是:这种方法计算的是欧式空间的距离,简单来讲,它给出的结果是两个点在三维空间中的直线距离,不是飞机在地球上飞的那条轨迹,而是笔直穿过地球的那条直线。
因此若是你的地理位置信息是用经纬度进行存储的,你就没法简单的直接使用这种方式进行距离计算。
MongoDB原生支持地理位置索引,能够直接用于位置距离计算和查询。
另外,它也是现在最流行的NoSQL数据库之一,除了可以很好地支持地理位置计算以外,还拥有诸如面向集合存储、模式自由、高性能、支持复杂查询、支持彻底索引等等特性。
对于咱们的需求,在MongoDB只需一个命令便可获得所须要的结果:
db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], num:100 })
查询结果默认将会由近到远排序,并且查询结果也包含目标点对象、距离目标点的距离等信息。
因为geoNear是MongoDB原生支持的查询函数,因此性能上也作到了高度的优化,彻底能够应付生产环境的压力。
基于MongoDB作附近查询是很方便的一件事情。
MongoDB在地理位置信息方面的表现远远不限于此,它还支持更多更加方便的功能,如范围查询、距离自动计算等。
接下来,咱们结合Symfony2来详细地演示一些使用MongoDB进行地理位置信息处理的例子。
参考环境:Nginx1.2 + PHP5.4 + MongoDB2.4.3 + Symfony2.1
创建coordinate和places两个document文件,前者是做为places内的一个embed字段. 为方便演示效果,这里同时设置了两个索引 2d 和 2dsphere
Document/Coordinate.php /** * @MongoDB\EmbeddedDocument */ class Coordinate { /** * @MongoDB\Field(type="float") */ public $longitude; /** * @MongoDB\Field(type="float") */ public $latitude; ... } Document/Place.php /** * @MongoDB\Document(collection="places") * @MongoDB\ChangeTrackingPolicy("DEFERRED_EXPLICIT") * @MongoDB\Indexes({ * @MongoDB\Index(keys={"coordinate"="2d"}), * @MongoDB\Index(keys={"coordinate"="2dsphere"}) * }) */ class Place { /** * * @MongoDB\Id(strategy="INCREMENT") */ protected $id; /** * @MongoDB\Field(type="string") */ protected $title; /** * @MongoDB\Field(type="string") */ protected $address; /** * @MongoDB\EmbedOne(targetDocument="HenterGEO\GEOBundle\Document\Coordinate") */ protected $coordinate; /** * @MongoDB\Distance */ public $distance; ... }
坐标保存以longitude, latitude这个顺序(没有明确的限制和区别,但咱们在此遵循官方的推荐)。
另外,为直观显示查询效果,默认使用百度地图标记查询数据。
咱们用到的代码包是doctrine/mongodb-odm-bundle(下文称ODM),这个代码包提供了在Symfony2环境下的MongoDB数据库支持,使用这个代码包,可让咱们更加方便的在Symfony2环境下操做MongoDB数据库。。
ODM封装了MongoDB中经常使用的一些地理位置函数,如周边搜索和范围搜索。
ODM中的操做默认距离单位是度,只有geoSphere支持弧度单位(必须在参数中指定spherical(true))
下文大多数直接对MongoDB的数据库操做将使用Mongo Shell进行演示。在演示网站页面和功能时,将结合Symfony二、Doctrine-MongoDB进行演示。
本文演示所用的MongoDB版本为2.4.3,版本号比较新,因此某些查询方式在低版本里面并不支持。
以places这个collection为例,大部分例子都须要相似下面格式的测试数据支持: { "_id" : 2, "coordinate" : { "longitude" : 121.3449, "latitude" : 31.17528 }, "title" : "仅售75元,市场价210元的顶呱呱田鸡火锅3-4人套餐,无餐具费,冬日暖锅,欢迎品尝", "address" : "闵行区航新路634号" }
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距离单位多是 弧度 和 平面单位(经纬度的“度”):
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> } } } )
查询当前坐标附近的目标,由近到远排列。
能够经过$near或$nearSphere,这两个方法相似,但默认状况下所用到的索引和距离单位不一样。
查询方式:
> db.places.find({'coordinate':{$near: [121.4905, 31.2646]}}) > db.places.find({'coordinate':{$nearSphere: [121.4905, 31.2646]}})
查询结果:
{ "_id" : 115, "coordinate" : { "longitude" : 121.4915, "latitude" : 31.25933 }, "title" : "仅售148元,市场价298元的星程上服假日酒店全日房一间入住一天, 节假日通用,精致生活,品质享受", "address" : "虹口区天水路90号" } …(100条)
上述查询坐标[121.4905, 31.2646]附近的100个点,从最近到最远排序。
默认返回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}})
结合Symfony2进行演示:
这里用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); $places = $this->getPlaceRepository()->createQueryBuilder() ->field('coordinate')->near($longitude, $latitude) ->maxDistance($max/111) ->getQuery()->toarray(); return compact('places','max','longitude','latitude'); }
经过 domain.dev/near 访问,效果以下:
longitude: xxx, latitude: xxx为当前位置,咱们在地图上显示了周边100条目标记录
MongoDB中的范围搜索(Inclusion)主要用$geoWithin这个命令,它又细分为3种不一样类型,以下:
$box 矩形
$center 圆(平面),$centerSphere圆(球面)
$polygon 多边形
$center和$centerSphere在小范围内的应用几乎没差异(除非这个圆半径几百上千千米)。
下面咱们介绍一下这三种查询的案例。
这个比较经常使用,好比百度地图的视野内搜索(矩形)、或搜狗地图的“拉框搜索”
定义一个矩形范围,咱们须要指定两个坐标,在MongoDB的查询方式以下:
> db.places.find( { coordinate : { $geoWithin : { $box :[ [ 121.44, 31.25 ] , [ 121.5005, 31.2846 ] ] } } } )
查询结果:
{ "_id" : 90472, "title" : "【鲁迅公园】仅售99元!酒店门市价288元的上海虹元商务宾馆客房一间入住一天(需持本人有效 身份证件办理登记):大床房/标准房(2选1)!不含早餐!不涉外!2012年9月29日-10月6日 不可以使用拉手券!可延迟退房至14:00!", "address" : "上海市虹口区柳营路8号", "coordinate" : { "longitude" : 121.47, "latitude" : 31.27145 } } ... ...
Symfony2演示代码:
指定两个坐标点
/** * @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'); }
经过 domain.dev/box 访问,效果以下:
应用场景有:地图搜索租房信息
查询以某坐标为圆心,指定半径的圆内的数据。
前面已提到,圆形区域搜索分为$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号" } ...
Symfony2演示代码:
指定圆心坐标和半径
/** * @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'); }
经过 domain.dev/center 访问,效果以下:
以longitude: xxx,latitude: xxx为中心点,半径10km的圆内
复杂区域内的查询,这个应用场景比较少见。指定至少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 } } ...
Symfony2演示代码(这里为方便,直接写死了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'); }
经过 domain.dev/polygon 访问,效果以下:
咱们假设须要以当前坐标为原点,查询附近指定范围内的餐厅,并直接显示距离。
这个需求用前面提到的$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.
查询方式以下(关于下面的示例用到了distanceMultipler函数,后文会详细解释):
> 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" : "【四川北路】热烈庆祝康骏会馆成立8周年!仅售169元!市场价399元的 康骏会馆四川北路一店(仅限3星级技师)全身精油按摩一人次!全程约90分钟! 男女不限!仅限四川北路一店使用,非本市全部门店通用!拉手券消费仅限每日19:00前! 健康有道,骏越万里!", "address" : "虹口区四川北路1896号-1904号201室" } } ], "stats" : { "time" : 0, "btreelocs" : 0, "nscanned" : 18, "objectsLoaded" : 12, "avgDistance" : 0.00009964377923093564, "maxDistance" : 0.0001064199324957278 }, "ok" : 1 }
能够看到返回了不少详细信息,如查询时间、返回数量、最大距离、平均距离等。
另外,results里面直接返回了距离目标点的距离dis。
Symfony2演示代码:
/** * @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'); }
经过 domian.dev/distance 访问,效果以下:
距离xxx米
前面演示的查询代码中,坐标都是按照 longitude, latitude这个顺序的。
这个是官方建议的坐标顺序,可是网上不少文档是相反的顺序,经测试发现,只要查询时指定的坐标顺序与数据库内的坐标顺序一致,出来的结果就是正确的,没有特定的前后顺序之分。
但鉴于官方文档的推荐,我在此仍是建议你们按照官方推荐的顺序。
案例A的$near和案例B的$center从需求上看差很少,可是$center或$centerSphere是属于$geoWithin的类型,$near方法查询后会对结果集对距离进行排序,而$geoWithin是无序的。
经常使用的查询方式已经介绍完了,不经常使用的好比geoIntersect查询,这里不作介绍,可是已经包含在开源的演示程序里了,有兴趣的读者能够自行测试研究。
下面介绍前文提到的距离单位等问题。
$near命令必需要求有索引。
$geoWithin能够无需索引,可是建议仍是创建索引以提高性能。
MongoDB查询地理位置默认有3种距离单位:
米(meters)
平面单位(flat units,能够理解为经纬度的“一度”)
弧度(radians)。
经过GeoJSON格式查询,单位默认是米,经过其它方式则比较混乱,下面详细解释一下。
下面的查询语句指定距离内的目标:
> db.places.find({'coordinate':{$near: [121.4905, 31.2646], $maxDistance:2}})
如今$maxDistance参数是2,可是若是我要查询如“附近500米内的餐厅”这样的需求,这个参数应该是多少?
关于距离计算,MongoDB的官方文档仅仅提到了弧度计算,未说明水平单位(度)计算。
关于弧度计算,官方文档的说明是:
To convert: distance to radians: divide the distance by the radius of the sphere (e.g. the Earth) in the same units as the distance measurement. radians to distance: multiply the radian measure by the radius of the sphere (e.g. the Earth) in the units system that you want to convert the distance to.
The radius of the Earth is approximately 3,959 miles or 6,371 kilometers.
因此若是用弧度查询,则以千米数除以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查询方式,也就是说距离单位用弧度。
The current implementation assumes an idealized model of a flat earth, meaning that an arcdegree of latitude (y) and longitude (x) represent the same distance everywhere. This is only true at the equator where they are both about equal to 69 miles or 111km. However, at the 10gen offices at { x : -74 , y : 40.74 } one arcdegree of longitude is about 52 miles or 83 km (latitude is unchanged). This means that something 1 mile to the north would seem closer than something 1 mile to the east.
$geoNear返回结果集中的dis,若是指定了spherical为true, dis的值为弧度,不指定则为度。
指定 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元,市场价298元的星程上服假日酒店全日房一间入住一天,节假日通用, 精致生活,品质享受", "address" : "虹口区天水路90号" } } ], "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元,市场价298元的星程上服假日酒店全日房一间入住一天,节假日通用, 精致生活,品质享受", "address" : "虹口区天水路90号" } } ], "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则为弧度,不然为度
若是坐标以GeoJSON格式,则单位都为米。
固然若是你的操做比较复杂,或者但愿知道更加详细的对照关系,也能够参考官方的这个更详细的对比表格:http://docs.mongodb.org/manual/reference/operator/query-geospatial/
如上面两个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元,市场价298元的星程上服假日酒店全日房一间入住一天,节假日通用, 精致生活,品质享受", "address" : "虹口区天水路90号" } }, … … ], "stats" : { "time" : 0, "btreelocs" : 0, "nscanned" : 15, "objectsLoaded" : 9, "avgDistance" : 0.6348305174802911, "maxDistance" : 0.0001064199324957278 }, "ok" : 1 }
注意上面的结果中dis的值,已是km单位的了。
经过前面的案例演示,相信你们对MongoDB的地理位置特性已经比较了解。
MongoDB还有不少很酷的功能,地址位置支持仅是其中一项。但愿之后能有机会为各位读者介绍如何结合Symfony2使用MongoDB进行应用开发的更多案例。
文中的演示程序已经发布在了Github上,地址是https://github.com/henter/HenterGEO,读者能够直接使用。
参考:
http://docs.mongodb.org/manual/
https://wiki.10gen.com/pages/viewpage.action?pageId=21268367&navigatingVersions=true
http://en.wikipedia.org/wiki/Radian
http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
http://www.phpchina.com/resource/manual/mysql/spatial-extensions-in-mysql.html
http://derickrethans.nl/spatial-indexes-mysql.html
http://dev.mysql.com/doc/refman/5.6/en/spatial-extensions.html
http://blog.nosqlfan.com/html/1811.html
http://en.wikipedia.org/wiki/Geohash
周攀,多年互联网从业经验,精通Symfony2框架,微博阅后即焚应用SnapWeibo的做者,斯诺克爱好者,养猫达人。读者能够经过他的微博 @周攀Henter或邮件(henter at henter dot me)与他取得联系。