隨著近幾年各類移動終端的迅速普及,基于地理位置的服務(wù)(LBS)和相關(guān)應(yīng)用也越來越多,而支撐這些應(yīng)用的最基礎(chǔ)技術(shù)之一,就是基于地理位置信息的處理。我所在的項(xiàng)目也正從事相關(guān)系統(tǒng)的開發(fā),我們使用的是Symfony2+Doctrine2 ODM+MongoDB的組合。
我們將這些技術(shù)要點(diǎn)整理成文,希望能夠通過本文的介紹和案例,詳細(xì)解釋如何使用MongoDB進(jìn)行地理位置信息的查詢和處理。在文章的開頭,我們也會先介紹一下業(yè)界通常用來處理地理位置信息的一些方案并進(jìn)行比較,讓讀者逐步了解使用MongoDB查詢及處理地理位置信息的優(yōu)勢。
本文使用了Symfony2和Doctrine2作為Web應(yīng)用的開發(fā)框架,對于想了解Symfony2的數(shù)據(jù)庫操作的讀者來說閱讀本文也可以了解和掌握相關(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ù)點(diǎn)做索引,比如按矩陣將用戶劃分到某固定小范圍的區(qū)域(可以參考后文將會提到的geohash)等方式,雖然可以繞個彎子解決,但或多或少操作起來比較麻煩,也會犧牲一些精度,甚至無法做到性能的最優(yōu)化,所以不能算作是最佳的解決辦法。
而最近幾年,直接支持地理位置操作的數(shù)據(jù)庫層出不窮,其操作友好、性能高的特性也開始被我們慢慢重視起來,其中的佼佼者當(dāng)屬M(fèi)ongoDB。
MongoDB在地理位置信息的處理上有什么優(yōu)勢?下面我們通過一個簡單的案例來對比一下各種技術(shù)方案之間進(jìn)行進(jìn)行地理位置信息處理的差異。
對于任何LBS應(yīng)用來說,讓用戶尋找周圍的好友可能都是一個必不可少的功能,我們就以這個功能為例,來看看各種處理方案之間的差異和區(qū)別。
我們假設(shè)有如下功能需求:
排除一些不通用和難以實(shí)現(xiàn)的技術(shù),我們羅列出以下幾種方案:
我們一個個來分析這幾種方案。
MySQL的使用非常簡單。對于大部分已經(jīng)使用MySQL的網(wǎng)站來說,使用這種方案沒有任何遷移和部署成本。
而在MySQL中查詢“最近的人”也僅需一條SQL即可,
注:這條SQL查詢的是在lat,lng這個坐標(biāo)附近的目標(biāo),并且按距離正序排列,SQL中的distance單位為公里。
但使用SQL語句進(jìn)行查詢的缺點(diǎn)也顯而易見,每條SQL的計(jì)算量都會非常大,性能將會是嚴(yán)重的問題。
先別放棄,我們嘗試對這條SQL做一些優(yōu)化。
可以將圓形區(qū)域抽象為正方形,如下圖
根據(jù)維基百科上的球面計(jì)算公式,可以根據(jù)圓心坐標(biāo)計(jì)算出正方形四個點(diǎn)的坐標(biāo)。
然后,查詢這個正方形內(nèi)的目標(biāo)點(diǎn)。
SQL語句可以簡化如下:
這樣優(yōu)化后,雖然數(shù)據(jù)不完全精確,但性能提升很明顯,并且可以通過給lat lng字段做索引的方式進(jìn)一步加快這條SQL的查詢速度。對精度有要求的應(yīng)用也可以在這個結(jié)果上再進(jìn)行計(jì)算,排除那些在方塊范圍內(nèi)但不在原型范圍內(nèi)的數(shù)據(jù),已達(dá)到對精度的要求。
可是這樣查詢出來的結(jié)果,是沒有排序的,除非再進(jìn)行一些SQL計(jì)算。但那又會在查詢的過程中產(chǎn)生臨時表排序,可能會造成性能問題。
GeoHash是一種地址編碼,通過切分地圖區(qū)域?yàn)樾》綁K(切分次數(shù)越多,精度越高),它能把二維的經(jīng)緯度編碼成一維的字符串。也就是說,理論上geohash字符串表示的并不是一個點(diǎn),而是一個矩形區(qū)域,只要矩形區(qū)域足夠小,達(dá)到所需精度即可。(其實(shí)MongoDB的索引也是基于geohash)
如:wtw3ued9m就是目前我所在的位置,降低一些精度,就會是wtw3ued,再降低一些精度,就會是wtw3u。(點(diǎn)擊鏈接查看坐標(biāo)編碼對應(yīng)Google地圖的位置)
所以這樣一來,我們就可以在MySQL中用LIKE ‘wtw3u%’來限定區(qū)域范圍查詢目標(biāo)點(diǎn),并且可以對結(jié)果集做緩存。更不會因?yàn)槲⑿〉慕?jīng)緯度變化而無法用上數(shù)據(jù)庫的Query Cache。
這種方案的優(yōu)點(diǎn)顯而易見,僅用一個字符串保存經(jīng)緯度信息,并且精度由字符串從頭到尾的長度決定,可以方便索引。
但這種方案的缺點(diǎn)是:從geohash的編碼算法中可以看出,靠近每個方塊邊界兩側(cè)的點(diǎn)雖然十分接近,但所屬的編碼會完全不同。實(shí)際應(yīng)用中,雖然可以通過去搜索環(huán)繞當(dāng)前方塊周圍的8個方塊來解決該問題,但一下子將原來只需要1次SQL查詢變成了需要查詢9次,這樣不僅增大了查詢量,也將原本簡單的方案復(fù)雜化了。
除此之外,這個方案也無法直接得到距離,需要程序協(xié)助進(jìn)行后續(xù)的排序計(jì)算。
MySQL的空間擴(kuò)展(MySQL Spatial Extensions),它允許在MySQL中直接處理、保存和分析地理位置相關(guān)的信息,看起來這是使用MySQL處理地理位置信息的“官方解決方案”。但恰恰很可惜的是:它卻不支持某些最基本的地理位置操作,比如查詢在半徑范圍內(nèi)的所有數(shù)據(jù)。它甚至連兩坐標(biāo)點(diǎn)之間的距離計(jì)算方法都沒有(MySQL Spatial的distance方法在5.*版本中不支持)
官方指南的做法是這樣的:
這條語句的處理邏輯是先通過兩個點(diǎn)產(chǎn)生一個LineString的類型的數(shù)據(jù),然后調(diào)用GLength得到這個LineString的實(shí)際長度。
這么做雖然有些復(fù)雜,貌似也解決了距離計(jì)算的問題,但讀者需要注意的是:這種方法計(jì)算的是歐式空間的距離,簡單來說,它給出的結(jié)果是兩個點(diǎn)在三維空間中的直線距離,不是飛機(jī)在地球上飛的那條軌跡,而是筆直穿過地球的那條直線。
所以如果你的地理位置信息是用經(jīng)緯度進(jìn)行存儲的,你就無法簡單的直接使用這種方式進(jìn)行距離計(jì)算。
MongoDB原生支持地理位置索引,可以直接用于位置距離計(jì)算和查詢。
另外,它也是如今最流行的NoSQL數(shù)據(jù)庫之一,除了能夠很好地支持地理位置計(jì)算之外,還擁有諸如面向集合存儲、模式自由、高性能、支持復(fù)雜查詢、支持完全索引等等特性。
對于我們的需求,在MongoDB只需一個命令即可得到所需要的結(jié)果:
查詢結(jié)果默認(rèn)將會由近到遠(yuǎn)排序,而且查詢結(jié)果也包含目標(biāo)點(diǎn)對象、距離目標(biāo)點(diǎn)的距離等信息。
由于geoNear是MongoDB原生支持的查詢函數(shù),所以性能上也做到了高度的優(yōu)化,完全可以應(yīng)付生產(chǎn)環(huán)境的壓力。
基于MongoDB做附近查詢是很方便的一件事情。
MongoDB在地理位置信息方面的表現(xiàn)遠(yuǎn)遠(yuǎn)不限于此,它還支持更多更加方便的功能,如范圍查詢、距離自動計(jì)算等。
接下來,我們結(jié)合Symfony2來詳細(xì)地演示一些使用MongoDB進(jìn)行地理位置信息處理的例子。
參考環(huán)境:Nginx1.2 + PHP5.4 + MongoDB2.4.3 + Symfony2.1
建立coordinate和places兩個document文件,前者是作為places內(nèi)的一個embed字段. 為方便演示效果,這里同時設(shè)置了兩個索引 2d 和 2dsphere
坐標(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))
MongoDB地理位置索引常用的有兩種。
關(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)跨度不太大(比如幾百幾千公里),這兩個索引計(jì)算出的距離相差幾乎可以忽略不計(jì)。
建立索引:
查詢方式分三種情況:
而查詢坐標(biāo)參數(shù)則分兩種:
坐標(biāo)對(經(jīng)緯度)根據(jù)查詢命令的不同,$maxDistance距離單位可能是 弧度 和 平面單位(經(jīng)緯度的“度”):
GeoJson $maxDistance距離單位默認(rèn)為米:
查詢當(dāng)前坐標(biāo)附近的目標(biāo),由近到遠(yuǎn)排列。
可以通過$near或$nearSphere,這兩個方法類似,但默認(rèn)情況下所用到的索引和距離單位不同。
查詢方式:
查詢結(jié)果:
上述查詢坐標(biāo)[121.4905, 31.2646]附近的100個點(diǎn),從最近到最遠(yuǎn)排序。
默認(rèn)返回100條數(shù)據(jù),也可以用limit()指定結(jié)果數(shù)量,如
指定最大距離 $maxDistance
結(jié)合Symfony2進(jìn)行演示:
這里用near,默認(rèn)以度為單位,公里數(shù)除以111(關(guān)于該距離單位后文有詳細(xì)解釋)。
通過 domain.dev/near 訪問,效果如下:
longitude: xxx, latitude: xxx為當(dāng)前位置,我們在地圖上顯示了周邊100條目標(biāo)記錄
MongoDB中的