结合MongoDB开发LBS应用
http://www.cnblogs.com/jifeng/p/4356052.html
然后列舉一下需求:
1.實時性要高,有頻繁的更新和讀取
2.可按距離排序支持分頁
3.支持多條件篩選(一個經(jīng)緯度數(shù)據(jù)還包含其他屬性,比如社交系統(tǒng)的性別、年齡)
方案簡單介紹:
1.sphinx geo索引
支持按照距離排序,并支持分頁。但是嘗試mva+geo失敗,還在找原因。
無法滿足高實時性需求。(可能是不了解實時增量索引配置有誤)
資源占用小,速度快
2.mongodb geo索引
支持按照距離排序,并支持分頁。支持多條件篩選。
可滿足實時性需求。
資源占用大,數(shù)據(jù)量達(dá)到百萬級請流量在10w左右查詢速度明顯下降。
3.mysql+geohash/ mysql sql查詢
不支持按照距離排序(代價太大)。支持分頁。支持多條件篩選。
可滿足實時性需求。
資源占用中等,查詢速度不及mongodb。
且geohash按照區(qū)塊將球面轉(zhuǎn)化平面并切割。暫時沒有找到跨區(qū)塊查詢方法(不太了解)。
4.redis+geohash
geohash缺點不再贅述
不支持距離排序。支持分頁查詢。不支持多條件篩選。
可滿足實時性需求。
資源占用最小。查詢速度很快。
?
?
http://blog.csdn.net/huangrunqing/article/details/9112227
簡介
隨著近幾年各類移動終端的迅速普及,基于地理位置的服務(wù)(LBS)和相關(guān)應(yīng)用也越來越多,而支撐這些應(yīng)用的最基礎(chǔ)技術(shù)之一,就是基于地理位置信息的處理。我所在的項目也正從事相關(guān)系統(tǒng)的開發(fā),我們使用的是Symfony2+Doctrine2 ODM+MongoDB的組合。
我們將這些技術(shù)要點整理成文,希望能夠通過本文的介紹和案例,詳細(xì)解釋如何使用MongoDB進(jìn)行地理位置信息的查詢和處理。在文章的開頭,我們也會先介紹一下業(yè)界通常用來處理地理位置信息的一些方案并進(jìn)行比較,讓讀者逐步了解使用MongoDB查詢及處理地理位置信息的優(yōu)勢。
本文使用了Symfony2和Doctrine2作為Web應(yīng)用的開發(fā)框架,對于想了解Symfony2的數(shù)據(jù)庫操作的讀者來說閱讀本文也可以了解和掌握相關(guān)的技術(shù)和使用方法。
1. LBS類應(yīng)用特點
不管是什么LBS應(yīng)用,一個共同的特點就是:他們的數(shù)據(jù)都或多或少包含了地理位置信息。而如何對這些信息進(jìn)行查詢、處理、分析,也就成為了支撐LBS應(yīng)用的最基礎(chǔ)也是最關(guān)鍵的技術(shù)問題。
而由于地理位置信息的特殊性,在開發(fā)中經(jīng)常會有比較難以處理的問題出現(xiàn),比如:由于用戶所在位置的不固定性,用戶可能會在很小范圍內(nèi)移動,而此時經(jīng)緯度值也會隨之變化;甚至在同一個位置,通過GPS設(shè)備獲取到的位置信息也可能不一樣。所以如果通過經(jīng)緯度去獲取周邊信息時,就很難像傳統(tǒng)數(shù)據(jù)庫那樣做查詢并進(jìn)行緩存。
對于這個問題,有讀者可能會說有別的處理方案,沒錯,比如只按經(jīng)緯度固定的幾位小數(shù)點做索引,比如按矩陣將用戶劃分到某固定小范圍的區(qū)域(可以參考后文將會提到的geohash)等方式,雖然可以繞個彎子解決,但或多或少操作起來比較麻煩,也會犧牲一些精度,甚至無法做到性能的最優(yōu)化,所以不能算作是最佳的解決辦法。
而最近幾年,直接支持地理位置操作的數(shù)據(jù)庫層出不窮,其操作友好、性能高的特性也開始被我們慢慢重視起來,其中的佼佼者當(dāng)屬MongoDB。
MongoDB在地理位置信息的處理上有什么優(yōu)勢?下面我們通過一個簡單的案例來對比一下各種技術(shù)方案之間進(jìn)行進(jìn)行地理位置信息處理的差異。
2. 幾個地理位置信息處理方案的對比和分析
1. 確定功能需求
對于任何LBS應(yīng)用來說,讓用戶尋找周圍的好友可能都是一個必不可少的功能,我們就以這個功能為例,來看看各種處理方案之間的差異和區(qū)別。
我們假設(shè)有如下功能需求:
- 顯示我附近的人
- 由近到遠(yuǎn)排序
- 顯示距離
2. 可能的技術(shù)方案
排除一些不通用和難以實現(xiàn)的技術(shù),我們羅列出以下幾種方案:
我們一個個來分析這幾種方案。
方案1:基于MySQL數(shù)據(jù)庫
MySQL的使用非常簡單。對于大部分已經(jīng)使用MySQL的網(wǎng)站來說,使用這種方案沒有任何遷移和部署成本。
而在MySQL中查詢“最近的人”也僅需一條SQL即可,
SELECT id, ( 6371 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians ( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distanceFROM places HAVING distance < 25 ORDER BY distance LIMIT 0 , 100;注:這條SQL查詢的是在lat,lng這個坐標(biāo)附近的目標(biāo),并且按距離正序排列,SQL中的distance單位為公里。
但使用SQL語句進(jìn)行查詢的缺點也顯而易見,每條SQL的計算量都會非常大,性能將會是嚴(yán)重的問題。
先別放棄,我們嘗試對這條SQL做一些優(yōu)化。
可以將圓形區(qū)域抽象為正方形,如下圖
根據(jù)維基百科上的球面計算公式,可以根據(jù)圓心坐標(biāo)計算出正方形四個點的坐標(biāo)。
然后,查詢這個正方形內(nèi)的目標(biāo)點。
SQL語句可以簡化如下:
SELECT * FROM places WHERE ((lat BETWEEN ? AND ?) AND (lng BETWEEN ? AND ?))這樣優(yōu)化后,雖然數(shù)據(jù)不完全精確,但性能提升很明顯,并且可以通過給lat lng字段做索引的方式進(jìn)一步加快這條SQL的查詢速度。對精度有要求的應(yīng)用也可以在這個結(jié)果上再進(jìn)行計算,排除那些在方塊范圍內(nèi)但不在原型范圍內(nèi)的數(shù)據(jù),已達(dá)到對精度的要求。
可是這樣查詢出來的結(jié)果,是沒有排序的,除非再進(jìn)行一些SQL計算。但那又會在查詢的過程中產(chǎn)生臨時表排序,可能會造成性能問題。
方案2:GeoHash索引,基于MySQL
GeoHash是一種地址編碼,通過切分地圖區(qū)域為小方塊(切分次數(shù)越多,精度越高),它能把二維的經(jīng)緯度編碼成一維的字符串。也就是說,理論上geohash字符串表示的并不是一個點,而是一個矩形區(qū)域,只要矩形區(qū)域足夠小,達(dá)到所需精度即可。(其實MongoDB的索引也是基于geohash)
如:wtw3ued9m就是目前我所在的位置,降低一些精度,就會是wtw3ued,再降低一些精度,就會是wtw3u。(點擊鏈接查看坐標(biāo)編碼對應(yīng)Google地圖的位置)
所以這樣一來,我們就可以在MySQL中用LIKE ‘wtw3u%’來限定區(qū)域范圍查詢目標(biāo)點,并且可以對結(jié)果集做緩存。更不會因為微小的經(jīng)緯度變化而無法用上數(shù)據(jù)庫的Query Cache。
這種方案的優(yōu)點顯而易見,僅用一個字符串保存經(jīng)緯度信息,并且精度由字符串從頭到尾的長度決定,可以方便索引。
但這種方案的缺點是:從geohash的編碼算法中可以看出,靠近每個方塊邊界兩側(cè)的點雖然十分接近,但所屬的編碼會完全不同。實際應(yīng)用中,雖然可以通過去搜索環(huán)繞當(dāng)前方塊周圍的8個方塊來解決該問題,但一下子將原來只需要1次SQL查詢變成了需要查詢9次,這樣不僅增大了查詢量,也將原本簡單的方案復(fù)雜化了。
除此之外,這個方案也無法直接得到距離,需要程序協(xié)助進(jìn)行后續(xù)的排序計算。
方案3:MySQL空間存儲
MySQL的空間擴(kuò)展(MySQL Spatial Extensions),它允許在MySQL中直接處理、保存和分析地理位置相關(guān)的信息,看起來這是使用MySQL處理地理位置信息的“官方解決方案”。但恰恰很可惜的是:它卻不支持某些最基本的地理位置操作,比如查詢在半徑范圍內(nèi)的所有數(shù)據(jù)。它甚至連兩坐標(biāo)點之間的距離計算方法都沒有(MySQL Spatial的distance方法在5.*版本中不支持)
官方指南的做法是這樣的:
GLength(LineStringFromWKB(LineString(point1, point2)))這條語句的處理邏輯是先通過兩個點產(chǎn)生一個LineString的類型的數(shù)據(jù),然后調(diào)用GLength得到這個LineString的實際長度。
這么做雖然有些復(fù)雜,貌似也解決了距離計算的問題,但讀者需要注意的是:這種方法計算的是歐式空間的距離,簡單來說,它給出的結(jié)果是兩個點在三維空間中的直線距離,不是飛機(jī)在地球上飛的那條軌跡,而是筆直穿過地球的那條直線。
所以如果你的地理位置信息是用經(jīng)緯度進(jìn)行存儲的,你就無法簡單的直接使用這種方式進(jìn)行距離計算。
方案4:使用MongoDB存儲地理位置信息
MongoDB原生支持地理位置索引,可以直接用于位置距離計算和查詢。
另外,它也是如今最流行的NoSQL數(shù)據(jù)庫之一,除了能夠很好地支持地理位置計算之外,還擁有諸如面向集合存儲、模式自由、高性能、支持復(fù)雜查詢、支持完全索引等等特性。
對于我們的需求,在MongoDB只需一個命令即可得到所需要的結(jié)果:
db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], num:100 })查詢結(jié)果默認(rèn)將會由近到遠(yuǎn)排序,而且查詢結(jié)果也包含目標(biāo)點對象、距離目標(biāo)點的距離等信息。
由于geoNear是MongoDB原生支持的查詢函數(shù),所以性能上也做到了高度的優(yōu)化,完全可以應(yīng)付生產(chǎn)環(huán)境的壓力。
方案總結(jié)
基于MongoDB做附近查詢是很方便的一件事情。
MongoDB在地理位置信息方面的表現(xiàn)遠(yuǎn)遠(yuǎn)不限于此,它還支持更多更加方便的功能,如范圍查詢、距離自動計算等。
接下來,我們結(jié)合Symfony2來詳細(xì)地演示一些使用MongoDB進(jìn)行地理位置信息處理的例子。
3. 結(jié)合Symfony2演示
運行環(huán)境
參考環(huán)境:Nginx1.2 + PHP5.4 + MongoDB2.4.3 + Symfony2.1
建立coordinate和places兩個document文件,前者是作為places內(nèi)的一個embed字段. 為方便演示效果,這里同時設(shè)置了兩個索引 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;...}坐標(biāo)保存以longitude, latitude這個順序(沒有明確的限制和區(qū)別,但我們在此遵循官方的推薦)。
另外,為直觀顯示查詢效果,默認(rèn)使用百度地圖標(biāo)記查詢數(shù)據(jù)。
程序說明
我們用到的代碼包是doctrine/mongodb-odm-bundle(下文稱ODM),這個代碼包提供了在Symfony2環(huán)境下的MongoDB數(shù)據(jù)庫支持,使用這個代碼包,可以讓我們更加方便的在Symfony2環(huán)境下操作MongoDB數(shù)據(jù)庫。。
ODM封裝了MongoDB中常用的一些地理位置函數(shù),如周邊搜索和范圍搜索。
ODM中的操作默認(rèn)距離單位是度,只有g(shù)eoSphere支持弧度單位(必須在參數(shù)中指定spherical(true))
4. MongoDB的地理位置查詢
注意事項
地理位置索引:
MongoDB地理位置索引常用的有兩種。
- 2d 平面坐標(biāo)索引,適用于基于平面的坐標(biāo)計算。也支持球面距離計算,不過官方推薦使用2dsphere索引。
- 2dsphere 幾何球體索引,適用于球面幾何運算
關(guān)于兩個坐標(biāo)之間的距離,官方推薦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.
不過,只要坐標(biāo)跨度不太大(比如幾百幾千公里),這兩個索引計算出的距離相差幾乎可以忽略不計。
建立索引:
> db.places.ensureIndex({'coordinate':'2d'}) > db.places.ensureIndex({'coordinate':'2dsphere'})查詢方式:
查詢方式分三種情況:
而查詢坐標(biāo)參數(shù)則分兩種:
坐標(biāo)對(經(jīng)緯度)根據(jù)查詢命令的不同,$maxDistance距離單位可能是 弧度 和 平面單位(經(jīng)緯度的“度”):
db.<collection>.find( { <location field> :{ $nearSphere: [ <x> , <y> ] ,$maxDistance: <distance in radians>} } )GeoJson $maxDistance距離單位默認(rèn)為米:
db.<collection>.find( { <location field> :{ $nearSphere :{ $geometry :{ type : "Point" ,coordinates : [ <longitude> , <latitude> ] } ,$maxDistance : <distance in meters>} } } )案例A:附近的人
查詢當(dāng)前坐標(biāo)附近的目標(biāo),由近到遠(yuǎn)排列。
可以通過$near或$nearSphere,這兩個方法類似,但默認(rèn)情況下所用到的索引和距離單位不同。
查詢方式:
> db.places.find({'coordinate':{$near: [121.4905, 31.2646]}}) > db.places.find({'coordinate':{$nearSphere: [121.4905, 31.2646]}})查詢結(jié)果:
{ "_id" : 115, "coordinate" : { "longitude" : 121.4915, "latitude" : 31.25933 }, "title" : "僅售148元,市場價298元的星程上服假日酒店全日房一間入住一天, 節(jié)假日通用,精致生活,品質(zhì)享受", "address" : "虹口區(qū)天水路90號" }…(100條)上述查詢坐標(biāo)[121.4905, 31.2646]附近的100個點,從最近到最遠(yuǎn)排序。
默認(rèn)返回100條數(shù)據(jù),也可以用limit()指定結(jié)果數(shù)量,如
> db.places.find({'coordinate':{$near: [121.4905, 31.2646]}}).limit(2)指定最大距離 $maxDistance
> db.places.find({'coordinate':{$near: [121.4905, 31.2646], $maxDistance:2}})結(jié)合Symfony2進(jìn)行演示:
這里用near,默認(rèn)以度為單位,公里數(shù)除以111(關(guān)于該距離單位后文有詳細(xì)解釋)。
/*** @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為當(dāng)前位置,我們在地圖上顯示了周邊100條目標(biāo)記錄
案例B:區(qū)域內(nèi)搜索
MongoDB中的范圍搜索(Inclusion)主要用$geoWithin這個命令,它又細(xì)分為3種不同類型,如下:
$center和$centerSphere在小范圍內(nèi)的應(yīng)用幾乎沒差別(除非這個圓半徑幾百上千公里)。
下面我們介紹一下這三種查詢的案例。
矩形區(qū)域
這個比較常用,比如百度地圖的視野內(nèi)搜索(矩形)、或搜狗地圖的“拉框搜索”
定義一個矩形范圍,我們需要指定兩個坐標(biāo),在MongoDB的查詢方式如下:
> db.places.find( { coordinate : { $geoWithin : { $box :[ [ 121.44, 31.25 ] , [ 121.5005, 31.2846 ] ] } } } )查詢結(jié)果:
{ "_id" : 90472, "title" : "【魯迅公園】僅售99元!酒店門市價288元的上海虹元商務(wù)賓館客房一間入住一天(需持本人有效 身份證件辦理登記):大床房/標(biāo)準(zhǔn)房(2選1)!不含早餐!不涉外!2012年9月29日-10月6日 不可使用拉手券!可延遲退房至14:00!", "address" : "上海市虹口區(qū)柳營路8號", "coordinate" : { "longitude" : 121.47, "latitude" : 31.27145 } } ... ...Symfony2演示代碼:
指定兩個坐標(biāo)點
/*** @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 訪問,效果如下:
圓形區(qū)域
應(yīng)用場景有:地圖搜索租房信息
查詢以某坐標(biāo)為圓心,指定半徑的圓內(nèi)的數(shù)據(jù)。
前面已提到,圓形區(qū)域搜索分為$center和$centerSphere這兩種類型,它們的區(qū)別主要在于支持的索引和默認(rèn)距離單位不同。
2d索引能同時支持$center和$centerSphere,2dsphere索引支持$centerSphere。關(guān)于距離單位,$center默認(rèn)是度,$centerSphere默認(rèn)距離是弧度。
查詢方式如下:
> 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] }}}) 查詢結(jié)果 { "_id" : 115, "coordinate" : { "longitude" : 121.4915, "latitude" : 31.25933 }, "title" : "僅售148元,市場價298元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受", "address" : "虹口區(qū)天水路90號" } ...Symfony2演示代碼:
指定圓心坐標(biāo)和半徑
/*** @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的圓內(nèi)
多邊形
復(fù)雜區(qū)域內(nèi)的查詢,這個應(yīng)用場景比較少見。指定至少3個坐標(biāo)點,查詢方式如下(五邊形):
> 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] ] } } } )查詢結(jié)果
{ "_id" : 90078, "title" : "僅售9.9元,市場價38元的燕太太燕窩單人甜品餐,用耐心守候一盅燉品,用愛滋補(bǔ)一生情誼", "address" : "河南南路489號香港名都購物廣場1F125燕太太燕窩", "coordinate" : { "longitude" : 121.48912, "latitude" : 31.22355 } } ...Symfony2演示代碼(這里為方便,直接寫死了5個坐標(biāo)點):
/*** @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 訪問,效果如下:
案例C:附近的餐廳
我們假設(shè)需要以當(dāng)前坐標(biāo)為原點,查詢附近指定范圍內(nèi)的餐廳,并直接顯示距離。
這個需求用前面提到的$near是可以實現(xiàn)的,但是距離需要二次計算。這里我們用$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.
查詢方式如下(關(guān)于下面的示例用到了distanceMultipler函數(shù),后文會詳細(xì)解釋):
> 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元的星程上服假日酒店全日房一間入住一天, 節(jié)假日通用,精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號"}},{"dis" : 0.00010610660597329082,"obj" : {"_id" : 465,"coordinate" : {"longitude" : 121.48406,"latitude" : 31.26202},"title" : "【四川北路】熱烈慶祝康駿會館成立8周年!僅售169元!市場價399元的 康駿會館四川北路一店(僅限3星級技師)全身精油按摩一人次!全程約90分鐘! 男女不限!僅限四川北路一店使用,非本市所有門店通用!拉手券消費僅限每日19:00前! 健康有道,駿越萬里!","address" : "虹口區(qū)四川北路1896號-1904號201室"}}],"stats" : {"time" : 0,"btreelocs" : 0,"nscanned" : 18,"objectsLoaded" : 12,"avgDistance" : 0.00009964377923093564,"maxDistance" : 0.0001064199324957278},"ok" : 1 }可以看到返回了很多詳細(xì)信息,如查詢時間、返回數(shù)量、最大距離、平均距離等。
另外,results里面直接返回了距離目標(biāo)點的距離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米
小結(jié)
前面演示的查詢代碼中,坐標(biāo)都是按照 longitude, latitude這個順序的。
這個是官方建議的坐標(biāo)順序,但是網(wǎng)上很多文檔是相反的順序,經(jīng)測試發(fā)現(xiàn),只要查詢時指定的坐標(biāo)順序與數(shù)據(jù)庫內(nèi)的坐標(biāo)順序一致,出來的結(jié)果就是正確的,沒有特定的先后順序之分。
但鑒于官方文檔的推薦,我在此還是建議大家按照官方推薦的順序。
案例A的$near和案例B的$center從需求上看差不多,但是$center或$centerSphere是屬于$geoWithin的類型,$near方法查詢后會對結(jié)果集對距離進(jìn)行排序,而$geoWithin是無序的。
常用的查詢方式已經(jīng)介紹完了,不常用的比如geoIntersect查詢,這里不做介紹,但是已經(jīng)包含在開源的演示程序里了,有興趣的讀者可以自行測試研究。
下面介紹前文提到的距離單位等問題。
5. 需要注意的問題
索引
$near命令必須要求有索引。
$geoWithin可以無需索引,但是建議還是建立索引以提升性能。
距離單位
MongoDB查詢地理位置默認(rèn)有3種距離單位:
- 米(meters)
- 平面單位(flat units,可以理解為經(jīng)緯度的“一度”)
- 弧度(radians)。
通過GeoJSON格式查詢,單位默認(rèn)是米,通過其它方式則比較混亂,下面詳細(xì)解釋一下。
下面的查詢語句指定距離內(nèi)的目標(biāo):
> db.places.find({'coordinate':{$near: [121.4905, 31.2646], $maxDistance:2}})現(xiàn)在$maxDistance參數(shù)是2,但是如果我要查詢?nèi)纭案浇?00米內(nèi)的餐廳”這樣的需求,這個參數(shù)應(yīng)該是多少?
關(guān)于距離計算,MongoDB的官方文檔僅僅提到了弧度計算,未說明水平單位(度)計算。
關(guān)于弧度計算,官方文檔的說明是:
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.
所以如果用弧度查詢,則以公里數(shù)除以6371,如“附近500米的餐廳”:
> db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], spherical: true,$maxDistance: 0.5/6371 })那如果不用弧度,以水平單位(度)查詢時,距離單位如何處理?
答案是以公里數(shù)除以111(推薦值),原因如下:
經(jīng)緯度的一度,分為經(jīng)度一度和緯度一度。
地球不同緯度之間的距離是一樣的,地球子午線(南極到北極的連線)長度39940.67公里,緯度一度大約110.9公里
但是不同緯度的經(jīng)度一度對應(yīng)的長度是不一樣的:
在地球赤道,一圈大約為40075KM,除以360度,每一個經(jīng)度大概是:40075/360=111.32KM
上海,大概在北緯31度,對應(yīng)一個經(jīng)度的長度是:40075*sin(90-31)/360=95.41KM
北京在北緯40度,對應(yīng)的是85KM
前面提到的參數(shù)111,這個值只是估算,并不完全準(zhǔn)確,任意兩點之間的距離,平均緯度越大,這個參數(shù)則誤差越大。詳細(xì)原因可以參考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返回結(jié)果集中的dis,如果指定了spherical為true, dis的值為弧度,不指定則為度。
指定 spherical為true,結(jié)果中的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元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號"}}],"stats" : {"time" : 0,"btreelocs" : 0,"nscanned" : 18,"objectsLoaded" : 12,"avgDistance" : 0.00009964377923093564,"maxDistance" : 0.0001064199324957278},"ok" : 1 }不指定sphericial,結(jié)果中的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元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號"}} ],"stats" : {"time" : 0,"btreelocs" : 0,"nscanned" : 18,"objectsLoaded" : 12,"avgDistance" : 0.006150808243357531,"maxDistance" : 0.00695541352612983},"ok" : 1 }說到這里讀者是不是已經(jīng)有點迷糊了?沒關(guān)系,在開發(fā)中其實你并不需要去知道各種距離單位的歷史和使用它的原因,我在此為你總結(jié)了一張表,大部分常用的函數(shù)和所使用的距離單位都已經(jīng)被我整理了出來,你只需要參考表上所列的距離單位直接使用即可。
| $near | 度 | 官方文檔上關(guān)于這一點是錯的 |
| $nearSphere | 弧度 | ? |
| $center | 度 | ? |
| $centerSphere | 弧度 | ? |
| $polygon | 度 | ? |
| $geoNear | 度或弧度 | 指定參數(shù)spherical為true則為弧度,否則為度 |
如果坐標(biāo)以GeoJSON格式,則單位都為米。
當(dāng)然如果你的操作比較復(fù)雜,或者希望知道更加詳細(xì)的對照關(guān)系,也可以參考官方的這個更詳細(xì)的對比表格:http://docs.mongodb.org/manual/reference/operator/query-geospatial/
單位自動換算
如上面兩個geoNear示例,結(jié)果中的dis,前文已經(jīng)提過這是與目標(biāo)點的距離,但是這個距離單位是跟查詢單位一致的,需要二次計算,不太方便。
而其實可以直接在查詢時指定?distanceMultiplier?,它會將這個參數(shù)乘以距離返回,如指定為6371,返回的就是公里數(shù)。
> 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元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號"}},……],"stats" : {"time" : 0,"btreelocs" : 0,"nscanned" : 15,"objectsLoaded" : 9,"avgDistance" : 0.6348305174802911,"maxDistance" : 0.0001064199324957278},"ok" : 1 }注意上面的結(jié)果中dis的值,已經(jīng)是km單位的了。
結(jié)語
通過前面的案例演示,相信大家對MongoDB的地理位置特性已經(jīng)比較了解。
MongoDB還有很多很酷的功能,地址位置支持僅是其中一項。希望以后能有機(jī)會為各位讀者介紹如何結(jié)合Symfony2使用MongoDB進(jìn)行應(yīng)用開發(fā)的更多案例。
文中的演示程序已經(jīng)發(fā)布在了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://dev.mysql.com/doc/refman/4.1/en/functions-that-test-spatial-relationships-between-geometries.html#function_distance
http://blog.nosqlfan.com/html/1811.html
http://en.wikipedia.org/wiki/Geohash
總結(jié)
以上是生活随笔為你收集整理的结合MongoDB开发LBS应用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Solr4.7实现LBS(地理位置搜索)
- 下一篇: 基于LBS的地理位置附近的搜索以及由近及