日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

结合MongoDB开发LBS应用

發(fā)布時(shí)間:2025/7/25 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 结合MongoDB开发LBS应用 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

http://www.cnblogs.com/jifeng/p/4356052.html

然后列舉一下需求:
1.實(shí)時(shí)性要高,有頻繁的更新和讀取
2.可按距離排序支持分頁(yè)
3.支持多條件篩選(一個(gè)經(jīng)緯度數(shù)據(jù)還包含其他屬性,比如社交系統(tǒng)的性別、年齡)

方案簡(jiǎn)單介紹:
1.sphinx geo索引
支持按照距離排序,并支持分頁(yè)。但是嘗試mva+geo失敗,還在找原因。
無(wú)法滿足高實(shí)時(shí)性需求。(可能是不了解實(shí)時(shí)增量索引配置有誤)
資源占用小,速度快

2.mongodb geo索引
支持按照距離排序,并支持分頁(yè)。支持多條件篩選。
可滿足實(shí)時(shí)性需求。
資源占用大,數(shù)據(jù)量達(dá)到百萬(wàn)級(jí)請(qǐng)流量在10w左右查詢速度明顯下降。

3.mysql+geohash/ mysql sql查詢
不支持按照距離排序(代價(jià)太大)。支持分頁(yè)。支持多條件篩選。
可滿足實(shí)時(shí)性需求。
資源占用中等,查詢速度不及mongodb。
且geohash按照區(qū)塊將球面轉(zhuǎn)化平面并切割。暫時(shí)沒(méi)有找到跨區(qū)塊查詢方法(不太了解)。

4.redis+geohash
geohash缺點(diǎn)不再贅述
不支持距離排序。支持分頁(yè)查詢。不支持多條件篩選。
可滿足實(shí)時(shí)性需求。
資源占用最小。查詢速度很快。

?

?

http://blog.csdn.net/huangrunqing/article/details/9112227

簡(jiǎn)介

隨著近幾年各類移動(dòng)終端的迅速普及,基于地理位置的服務(wù)(LBS)和相關(guān)應(yīng)用也越來(lái)越多,而支撐這些應(yīng)用的最基礎(chǔ)技術(shù)之一,就是基于地理位置信息的處理。我所在的項(xiàng)目也正從事相關(guān)系統(tǒng)的開發(fā),我們使用的是Symfony2+Doctrine2 ODM+MongoDB的組合。

我們將這些技術(shù)要點(diǎn)整理成文,希望能夠通過(guò)本文的介紹和案例,詳細(xì)解釋如何使用MongoDB進(jìn)行地理位置信息的查詢和處理。在文章的開頭,我們也會(huì)先介紹一下業(yè)界通常用來(lái)處理地理位置信息的一些方案并進(jìn)行比較,讓讀者逐步了解使用MongoDB查詢及處理地理位置信息的優(yōu)勢(shì)。

本文使用了Symfony2和Doctrine2作為Web應(yīng)用的開發(fā)框架,對(duì)于想了解Symfony2的數(shù)據(jù)庫(kù)操作的讀者來(lái)說(shuō)閱讀本文也可以了解和掌握相關(guān)的技術(shù)和使用方法。

1. LBS類應(yīng)用特點(diǎn)

不管是什么LBS應(yīng)用,一個(gè)共同的特點(diǎn)就是:他們的數(shù)據(jù)都或多或少包含了地理位置信息。而如何對(duì)這些信息進(jìn)行查詢、處理、分析,也就成為了支撐LBS應(yīng)用的最基礎(chǔ)也是最關(guān)鍵的技術(shù)問(wèn)題。

而由于地理位置信息的特殊性,在開發(fā)中經(jīng)常會(huì)有比較難以處理的問(wèn)題出現(xiàn),比如:由于用戶所在位置的不固定性,用戶可能會(huì)在很小范圍內(nèi)移動(dòng),而此時(shí)經(jīng)緯度值也會(huì)隨之變化;甚至在同一個(gè)位置,通過(guò)GPS設(shè)備獲取到的位置信息也可能不一樣。所以如果通過(guò)經(jīng)緯度去獲取周邊信息時(shí),就很難像傳統(tǒng)數(shù)據(jù)庫(kù)那樣做查詢并進(jìn)行緩存。

對(duì)于這個(gè)問(wèn)題,有讀者可能會(huì)說(shuō)有別的處理方案,沒(méi)錯(cuò),比如只按經(jīng)緯度固定的幾位小數(shù)點(diǎn)做索引,比如按矩陣將用戶劃分到某固定小范圍的區(qū)域(可以參考后文將會(huì)提到的geohash)等方式,雖然可以繞個(gè)彎子解決,但或多或少操作起來(lái)比較麻煩,也會(huì)犧牲一些精度,甚至無(wú)法做到性能的最優(yōu)化,所以不能算作是最佳的解決辦法。

而最近幾年,直接支持地理位置操作的數(shù)據(jù)庫(kù)層出不窮,其操作友好、性能高的特性也開始被我們慢慢重視起來(lái),其中的佼佼者當(dāng)屬M(fèi)ongoDB。

MongoDB在地理位置信息的處理上有什么優(yōu)勢(shì)?下面我們通過(guò)一個(gè)簡(jiǎn)單的案例來(lái)對(duì)比一下各種技術(shù)方案之間進(jìn)行進(jìn)行地理位置信息處理的差異。

2. 幾個(gè)地理位置信息處理方案的對(duì)比和分析

1. 確定功能需求

對(duì)于任何LBS應(yīng)用來(lái)說(shuō),讓用戶尋找周圍的好友可能都是一個(gè)必不可少的功能,我們就以這個(gè)功能為例,來(lái)看看各種處理方案之間的差異和區(qū)別。

我們假設(shè)有如下功能需求:

  • 顯示我附近的人
  • 由近到遠(yuǎn)排序
  • 顯示距離

2. 可能的技術(shù)方案

排除一些不通用和難以實(shí)現(xiàn)的技術(shù),我們羅列出以下幾種方案:

  • 基于MySQL數(shù)據(jù)庫(kù)
  • 采用GeoHash索引,基于MySQL
  • MySQL空間存儲(chǔ)(MySQL Spatial Extensions)
  • 使用MongoDB存儲(chǔ)地理位置信息
  • 我們一個(gè)個(gè)來(lái)分析這幾種方案。

    方案1:基于MySQL數(shù)據(jù)庫(kù)

    MySQL的使用非常簡(jiǎn)單。對(duì)于大部分已經(jīng)使用MySQL的網(wǎng)站來(lái)說(shuō),使用這種方案沒(méi)有任何遷移和部署成本。

    而在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這個(gè)坐標(biāo)附近的目標(biāo),并且按距離正序排列,SQL中的distance單位為公里。

    但使用SQL語(yǔ)句進(jìn)行查詢的缺點(diǎn)也顯而易見,每條SQL的計(jì)算量都會(huì)非常大,性能將會(huì)是嚴(yán)重的問(wèn)題。

    先別放棄,我們嘗試對(duì)這條SQL做一些優(yōu)化。

    可以將圓形區(qū)域抽象為正方形,如下圖

    根據(jù)維基百科上的球面計(jì)算公式,可以根據(jù)圓心坐標(biāo)計(jì)算出正方形四個(gè)點(diǎn)的坐標(biāo)。

    然后,查詢這個(gè)正方形內(nèi)的目標(biāo)點(diǎn)。

    SQL語(yǔ)句可以簡(jiǎn)化如下:

    SELECT * FROM places WHERE ((lat BETWEEN ? AND ?) AND (lng BETWEEN ? AND ?))

    這樣優(yōu)化后,雖然數(shù)據(jù)不完全精確,但性能提升很明顯,并且可以通過(guò)給lat lng字段做索引的方式進(jìn)一步加快這條SQL的查詢速度。對(duì)精度有要求的應(yīng)用也可以在這個(gè)結(jié)果上再進(jìn)行計(jì)算,排除那些在方塊范圍內(nèi)但不在原型范圍內(nèi)的數(shù)據(jù),已達(dá)到對(duì)精度的要求。

    可是這樣查詢出來(lái)的結(jié)果,是沒(méi)有排序的,除非再進(jìn)行一些SQL計(jì)算。但那又會(huì)在查詢的過(guò)程中產(chǎn)生臨時(shí)表排序,可能會(huì)造成性能問(wèn)題。

    方案2:GeoHash索引,基于MySQL

    GeoHash是一種地址編碼,通過(guò)切分地圖區(qū)域?yàn)樾》綁K(切分次數(shù)越多,精度越高),它能把二維的經(jīng)緯度編碼成一維的字符串。也就是說(shuō),理論上geohash字符串表示的并不是一個(gè)點(diǎn),而是一個(gè)矩形區(qū)域,只要矩形區(qū)域足夠小,達(dá)到所需精度即可。(其實(shí)MongoDB的索引也是基于geohash)

    如:wtw3ued9m就是目前我所在的位置,降低一些精度,就會(huì)是wtw3ued,再降低一些精度,就會(huì)是wtw3u。(點(diǎn)擊鏈接查看坐標(biāo)編碼對(duì)應(yīng)Google地圖的位置)

    所以這樣一來(lái),我們就可以在MySQL中用LIKE ‘wtw3u%’來(lái)限定區(qū)域范圍查詢目標(biāo)點(diǎn),并且可以對(duì)結(jié)果集做緩存。更不會(huì)因?yàn)槲⑿〉慕?jīng)緯度變化而無(wú)法用上數(shù)據(jù)庫(kù)的Query Cache。

    這種方案的優(yōu)點(diǎn)顯而易見,僅用一個(gè)字符串保存經(jīng)緯度信息,并且精度由字符串從頭到尾的長(zhǎng)度決定,可以方便索引。

    但這種方案的缺點(diǎn)是:從geohash的編碼算法中可以看出,靠近每個(gè)方塊邊界兩側(cè)的點(diǎn)雖然十分接近,但所屬的編碼會(huì)完全不同。實(shí)際應(yīng)用中,雖然可以通過(guò)去搜索環(huán)繞當(dāng)前方塊周圍的8個(gè)方塊來(lái)解決該問(wèn)題,但一下子將原來(lái)只需要1次SQL查詢變成了需要查詢9次,這樣不僅增大了查詢量,也將原本簡(jiǎn)單的方案復(fù)雜化了。

    除此之外,這個(gè)方案也無(wú)法直接得到距離,需要程序協(xié)助進(jìn)行后續(xù)的排序計(jì)算。

    方案3:MySQL空間存儲(chǔ)

    MySQL的空間擴(kuò)展(MySQL Spatial Extensions),它允許在MySQL中直接處理、保存和分析地理位置相關(guān)的信息,看起來(lái)這是使用MySQL處理地理位置信息的“官方解決方案”。但恰恰很可惜的是:它卻不支持某些最基本的地理位置操作,比如查詢?cè)诎霃椒秶鷥?nèi)的所有數(shù)據(jù)。它甚至連兩坐標(biāo)點(diǎn)之間的距離計(jì)算方法都沒(méi)有(MySQL Spatial的distance方法在5.*版本中不支持)

    官方指南的做法是這樣的:

    GLength(LineStringFromWKB(LineString(point1, point2)))

    這條語(yǔ)句的處理邏輯是先通過(guò)兩個(gè)點(diǎn)產(chǎn)生一個(gè)LineString的類型的數(shù)據(jù),然后調(diào)用GLength得到這個(gè)LineString的實(shí)際長(zhǎng)度。

    這么做雖然有些復(fù)雜,貌似也解決了距離計(jì)算的問(wèn)題,但讀者需要注意的是:這種方法計(jì)算的是歐式空間的距離,簡(jiǎn)單來(lái)說(shuō),它給出的結(jié)果是兩個(gè)點(diǎn)在三維空間中的直線距離,不是飛機(jī)在地球上飛的那條軌跡,而是筆直穿過(guò)地球的那條直線。

    所以如果你的地理位置信息是用經(jīng)緯度進(jìn)行存儲(chǔ)的,你就無(wú)法簡(jiǎn)單的直接使用這種方式進(jìn)行距離計(jì)算。

    方案4:使用MongoDB存儲(chǔ)地理位置信息

    MongoDB原生支持地理位置索引,可以直接用于位置距離計(jì)算和查詢。

    另外,它也是如今最流行的NoSQL數(shù)據(jù)庫(kù)之一,除了能夠很好地支持地理位置計(jì)算之外,還擁有諸如面向集合存儲(chǔ)、模式自由、高性能、支持復(fù)雜查詢、支持完全索引等等特性。

    對(duì)于我們的需求,在MongoDB只需一個(gè)命令即可得到所需要的結(jié)果:

    db.runCommand( { geoNear: "places", near: [ 121.4905, 31.2646 ], num:100 })

    查詢結(jié)果默認(rèn)將會(huì)由近到遠(yuǎn)排序,而且查詢結(jié)果也包含目標(biāo)點(diǎn)對(duì)象、距離目標(biāo)點(diǎn)的距離等信息。

    由于geoNear是MongoDB原生支持的查詢函數(shù),所以性能上也做到了高度的優(yōu)化,完全可以應(yīng)付生產(chǎn)環(huán)境的壓力。

    方案總結(jié)

    基于MongoDB做附近查詢是很方便的一件事情。

    MongoDB在地理位置信息方面的表現(xiàn)遠(yuǎn)遠(yuǎn)不限于此,它還支持更多更加方便的功能,如范圍查詢、距離自動(dòng)計(jì)算等。

    接下來(lái),我們結(jié)合Symfony2來(lái)詳細(xì)地演示一些使用MongoDB進(jìn)行地理位置信息處理的例子。

    3. 結(jié)合Symfony2演示

    運(yùn)行環(huán)境

    參考環(huán)境:Nginx1.2 + PHP5.4 + MongoDB2.4.3 + Symfony2.1

    建立coordinate和places兩個(gè)document文件,前者是作為places內(nèi)的一個(gè)embed字段. 為方便演示效果,這里同時(shí)設(shè)置了兩個(gè)索引 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這個(gè)順序(沒(méi)有明確的限制和區(qū)別,但我們?cè)诖俗裱俜降耐扑])。

    另外,為直觀顯示查詢效果,默認(rèn)使用百度地圖標(biāo)記查詢數(shù)據(jù)。

    程序說(shuō)明

    我們用到的代碼包是doctrine/mongodb-odm-bundle(下文稱ODM),這個(gè)代碼包提供了在Symfony2環(huán)境下的MongoDB數(shù)據(jù)庫(kù)支持,使用這個(gè)代碼包,可以讓我們更加方便的在Symfony2環(huán)境下操作MongoDB數(shù)據(jù)庫(kù)。。

    ODM封裝了MongoDB中常用的一些地理位置函數(shù),如周邊搜索和范圍搜索。

    ODM中的操作默認(rèn)距離單位是度,只有g(shù)eoSphere支持弧度單位(必須在參數(shù)中指定spherical(true))

    4. MongoDB的地理位置查詢

    注意事項(xiàng)

  • 下文大多數(shù)直接對(duì)MongoDB的數(shù)據(jù)庫(kù)操作將使用Mongo Shell進(jìn)行演示。在演示網(wǎng)站頁(yè)面和功能時(shí),將結(jié)合Symfony2、Doctrine-MongoDB進(jìn)行演示。
  • 本文演示所用的MongoDB版本為2.4.3,版本號(hào)比較新,所以某些查詢方式在低版本里面并不支持。
  • 以places這個(gè)collection為例,大部分例子都需要類似下面格式的測(cè)試數(shù)據(jù)支持: { "_id" : 2, "coordinate" : { "longitude" : 121.3449, "latitude" : 31.17528 }, "title" : "僅售75元,市場(chǎng)價(jià)210元的頂呱呱田雞火鍋3-4人套餐,無(wú)餐具費(fèi),冬日暖鍋,歡迎品嘗", "address" : "閔行區(qū)航新路634號(hào)" }
  • 地理位置索引:

    MongoDB地理位置索引常用的有兩種。

    • 2d 平面坐標(biāo)索引,適用于基于平面的坐標(biāo)計(jì)算。也支持球面距離計(jì)算,不過(guò)官方推薦使用2dsphere索引。
    • 2dsphere 幾何球體索引,適用于球面幾何運(yùn)算

    關(guān)于兩個(gè)坐標(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.

    不過(guò),只要坐標(biāo)跨度不太大(比如幾百幾千公里),這兩個(gè)索引計(jì)算出的距離相差幾乎可以忽略不計(jì)。

    建立索引:

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

    查詢方式:

    查詢方式分三種情況:

  • Inclusion。范圍查詢,如百度地圖“視野內(nèi)搜索”。
  • Inetersection。交集查詢。不常用。
  • Proximity。周邊查詢,如“附近500內(nèi)的餐廳”。
  • 而查詢坐標(biāo)參數(shù)則分兩種:

  • 坐標(biāo)對(duì)(經(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)排列。

    可以通過(guò)$near或$nearSphere,這兩個(gè)方法類似,但默認(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元,市場(chǎng)價(jià)298元的星程上服假日酒店全日房一間入住一天, 節(jié)假日通用,精致生活,品質(zhì)享受", "address" : "虹口區(qū)天水路90號(hào)" }…(100條)

    上述查詢坐標(biāo)[121.4905, 31.2646]附近的100個(gè)點(diǎn),從最近到最遠(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'); }

    通過(guò) domain.dev/near 訪問(wèn),效果如下:

    longitude: xxx, latitude: xxx為當(dāng)前位置,我們?cè)诘貓D上顯示了周邊100條目標(biāo)記錄

    案例B:區(qū)域內(nèi)搜索

    MongoDB中的范圍搜索(Inclusion)主要用$geoWithin這個(gè)命令,它又細(xì)分為3種不同類型,如下:

  • $box 矩形
  • $center 圓(平面),$centerSphere圓(球面)
  • $polygon 多邊形
  • $center和$centerSphere在小范圍內(nèi)的應(yīng)用幾乎沒(méi)差別(除非這個(gè)圓半徑幾百上千公里)。

    下面我們介紹一下這三種查詢的案例。

    矩形區(qū)域

    這個(gè)比較常用,比如百度地圖的視野內(nèi)搜索(矩形)、或搜狗地圖的“拉框搜索”

    定義一個(gè)矩形范圍,我們需要指定兩個(gè)坐標(biāo),在MongoDB的查詢方式如下:

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

    查詢結(jié)果:

    { "_id" : 90472, "title" : "【魯迅公園】?jī)H售99元!酒店門市價(jià)288元的上海虹元商務(wù)賓館客房一間入住一天(需持本人有效 身份證件辦理登記):大床房/標(biāo)準(zhǔn)房(2選1)!不含早餐!不涉外!2012年9月29日-10月6日 不可使用拉手券!可延遲退房至14:00!", "address" : "上海市虹口區(qū)柳營(yíng)路8號(hào)", "coordinate" : { "longitude" : 121.47, "latitude" : 31.27145 } } ... ...

    Symfony2演示代碼:

    指定兩個(gè)坐標(biāo)點(diǎn)

    /*** @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'); }

    通過(guò) domain.dev/box 訪問(wèn),效果如下:

    圓形區(qū)域

    應(yīng)用場(chǎng)景有:地圖搜索租房信息

    查詢以某坐標(biāo)為圓心,指定半徑的圓內(nèi)的數(shù)據(jù)。

    前面已提到,圓形區(qū)域搜索分為$center和$centerSphere這兩種類型,它們的區(qū)別主要在于支持的索引和默認(rèn)距離單位不同。

    2d索引能同時(shí)支持$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元,市場(chǎng)價(jià)298元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受", "address" : "虹口區(qū)天水路90號(hào)" } ...

    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'); }

    通過(guò) domain.dev/center 訪問(wèn),效果如下:

    以longitude: xxx,latitude: xxx為中心點(diǎn),半徑10km的圓內(nèi)

    多邊形

    復(fù)雜區(qū)域內(nèi)的查詢,這個(gè)應(yīng)用場(chǎng)景比較少見。指定至少3個(gè)坐標(biāo)點(diǎn),查詢方式如下(五邊形):

    > 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元,市場(chǎng)價(jià)38元的燕太太燕窩單人甜品餐,用耐心守候一盅燉品,用愛滋補(bǔ)一生情誼", "address" : "河南南路489號(hào)香港名都購(gòu)物廣場(chǎng)1F125燕太太燕窩", "coordinate" : { "longitude" : 121.48912, "latitude" : 31.22355 } } ...

    Symfony2演示代碼(這里為方便,直接寫死了5個(gè)坐標(biāo)點(diǎn)):

    /*** @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'); }

    通過(guò) domain.dev/polygon 訪問(wèn),效果如下:

    案例C:附近的餐廳

    我們假設(shè)需要以當(dāng)前坐標(biāo)為原點(diǎn),查詢附近指定范圍內(nèi)的餐廳,并直接顯示距離。

    這個(gè)需求用前面提到的$near是可以實(shí)現(xiàn)的,但是距離需要二次計(jì)算。這里我們用$geoNear這個(gè)命令查詢。

    $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ù),后文會(huì)詳細(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元,市場(chǎng)價(jià)298元的星程上服假日酒店全日房一間入住一天, 節(jié)假日通用,精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號(hào)"}},{"dis" : 0.00010610660597329082,"obj" : {"_id" : 465,"coordinate" : {"longitude" : 121.48406,"latitude" : 31.26202},"title" : "【四川北路】熱烈慶祝康駿會(huì)館成立8周年!僅售169元!市場(chǎng)價(jià)399元的 康駿會(huì)館四川北路一店(僅限3星級(jí)技師)全身精油按摩一人次!全程約90分鐘! 男女不限!僅限四川北路一店使用,非本市所有門店通用!拉手券消費(fèi)僅限每日19:00前! 健康有道,駿越萬(wàn)里!","address" : "虹口區(qū)四川北路1896號(hào)-1904號(hào)201室"}}],"stats" : {"time" : 0,"btreelocs" : 0,"nscanned" : 18,"objectsLoaded" : 12,"avgDistance" : 0.00009964377923093564,"maxDistance" : 0.0001064199324957278},"ok" : 1 }

    可以看到返回了很多詳細(xì)信息,如查詢時(shí)間、返回?cái)?shù)量、最大距離、平均距離等。

    另外,results里面直接返回了距離目標(biāo)點(diǎn)的距離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'); }

    通過(guò) domian.dev/distance 訪問(wèn),效果如下:

    距離xxx米

    小結(jié)

    前面演示的查詢代碼中,坐標(biāo)都是按照 longitude, latitude這個(gè)順序的。

    這個(gè)是官方建議的坐標(biāo)順序,但是網(wǎng)上很多文檔是相反的順序,經(jīng)測(cè)試發(fā)現(xiàn),只要查詢時(shí)指定的坐標(biāo)順序與數(shù)據(jù)庫(kù)內(nèi)的坐標(biāo)順序一致,出來(lái)的結(jié)果就是正確的,沒(méi)有特定的先后順序之分。

    但鑒于官方文檔的推薦,我在此還是建議大家按照官方推薦的順序。

    案例A的$near和案例B的$center從需求上看差不多,但是$center或$centerSphere是屬于$geoWithin的類型,$near方法查詢后會(huì)對(duì)結(jié)果集對(duì)距離進(jìn)行排序,而$geoWithin是無(wú)序的。

    常用的查詢方式已經(jīng)介紹完了,不常用的比如geoIntersect查詢,這里不做介紹,但是已經(jīng)包含在開源的演示程序里了,有興趣的讀者可以自行測(cè)試研究。

    下面介紹前文提到的距離單位等問(wèn)題。

    5. 需要注意的問(wèn)題

    索引

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

    $geoWithin可以無(wú)需索引,但是建議還是建立索引以提升性能。

    距離單位

    MongoDB查詢地理位置默認(rèn)有3種距離單位:

    • 米(meters)
    • 平面單位(flat units,可以理解為經(jīng)緯度的“一度”)
    • 弧度(radians)。

    通過(guò)GeoJSON格式查詢,單位默認(rèn)是米,通過(guò)其它方式則比較混亂,下面詳細(xì)解釋一下。

    下面的查詢語(yǔ)句指定距離內(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)的餐廳”這樣的需求,這個(gè)參數(shù)應(yīng)該是多少?

    關(guān)于距離計(jì)算,MongoDB的官方文檔僅僅提到了弧度計(jì)算,未說(shuō)明水平單位(度)計(jì)算。

    關(guān)于弧度計(jì)算,官方文檔的說(shuō)明是:

    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í),距離單位如何處理?

    答案是以公里數(shù)除以111(推薦值),原因如下:

    經(jīng)緯度的一度,分為經(jīng)度一度和緯度一度。

    地球不同緯度之間的距離是一樣的,地球子午線(南極到北極的連線)長(zhǎng)度39940.67公里,緯度一度大約110.9公里

    但是不同緯度的經(jīng)度一度對(duì)應(yīng)的長(zhǎng)度是不一樣的:

    在地球赤道,一圈大約為40075KM,除以360度,每一個(gè)經(jīng)度大概是:40075/360=111.32KM

    上海,大概在北緯31度,對(duì)應(yīng)一個(gè)經(jīng)度的長(zhǎng)度是:40075*sin(90-31)/360=95.41KM

    北京在北緯40度,對(duì)應(yīng)的是85KM

    前面提到的參數(shù)111,這個(gè)值只是估算,并不完全準(zhǔn)確,任意兩點(diǎn)之間的距離,平均緯度越大,這個(gè)參數(shù)則誤差越大。詳細(xì)原因可以參考wiki上的解釋:http://en.wikipedia.org/wiki/Latitude

    但是,即便如此,“度”這個(gè)單位只用于平面,由于地球是圓的,在大范圍使用時(shí)會(huì)有誤差。

    官方建議使用sphere查詢方式,也就是說(shuō)距離單位用弧度。

    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元,市場(chǎng)價(jià)298元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號(hào)"}}],"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元,市場(chǎng)價(jià)298元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號(hào)"}} ],"stats" : {"time" : 0,"btreelocs" : 0,"nscanned" : 18,"objectsLoaded" : 12,"avgDistance" : 0.006150808243357531,"maxDistance" : 0.00695541352612983},"ok" : 1 }

    說(shuō)到這里讀者是不是已經(jīng)有點(diǎn)迷糊了?沒(méi)關(guān)系,在開發(fā)中其實(shí)你并不需要去知道各種距離單位的歷史和使用它的原因,我在此為你總結(jié)了一張表,大部分常用的函數(shù)和所使用的距離單位都已經(jīng)被我整理了出來(lái),你只需要參考表上所列的距離單位直接使用即可。

    查詢命令距離單位說(shuō)明
    $near官方文檔上關(guān)于這一點(diǎn)是錯(cuò)的
    $nearSphere弧度?
    $center?
    $centerSphere弧度?
    $polygon?
    $geoNear度或弧度指定參數(shù)spherical為true則為弧度,否則為度

    如果坐標(biāo)以GeoJSON格式,則單位都為米。

    當(dāng)然如果你的操作比較復(fù)雜,或者希望知道更加詳細(xì)的對(duì)照關(guān)系,也可以參考官方的這個(gè)更詳細(xì)的對(duì)比表格:http://docs.mongodb.org/manual/reference/operator/query-geospatial/

    單位自動(dòng)換算

    如上面兩個(gè)geoNear示例,結(jié)果中的dis,前文已經(jīng)提過(guò)這是與目標(biāo)點(diǎn)的距離,但是這個(gè)距離單位是跟查詢單位一致的,需要二次計(jì)算,不太方便。

    而其實(shí)可以直接在查詢時(shí)指定?distanceMultiplier?,它會(huì)將這個(gè)參數(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元,市場(chǎng)價(jià)298元的星程上服假日酒店全日房一間入住一天,節(jié)假日通用, 精致生活,品質(zhì)享受","address" : "虹口區(qū)天水路90號(hào)"}},……],"stats" : {"time" : 0,"btreelocs" : 0,"nscanned" : 15,"objectsLoaded" : 9,"avgDistance" : 0.6348305174802911,"maxDistance" : 0.0001064199324957278},"ok" : 1 }

    注意上面的結(jié)果中dis的值,已經(jīng)是km單位的了。

    結(jié)語(yǔ)

    通過(guò)前面的案例演示,相信大家對(duì)MongoDB的地理位置特性已經(jīng)比較了解。

    MongoDB還有很多很酷的功能,地址位置支持僅是其中一項(xiàng)。希望以后能有機(jī)會(huì)為各位讀者介紹如何結(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应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。