隨着近幾年各種移動終端的迅速普及,基於地理位置的服務(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)與他取得聯繫。