隨著近幾年各類移動終端的迅速普及,基于地理位置的服務(LBS)和相關應用也越來越多,而支撐這些應用的最基礎技術之一,就是基于地理位置信息的處理。我所在的項目也正從事相關系統的開發,我們使用的是Symfony2+Doctrine2 ODM+MongoDB的組合。
我們將這些技術要點整理成文,希望能夠通過本文的介紹和案例,詳細解釋如何使用MongoDB進行地理位置信息的查詢和處理。在文章的開頭,我們也會先介紹一下業界通常用來處理地理位置信息的一些方案并進行比較,讓讀者逐步了解使用MongoDB查詢及處理地理位置信息的優勢。
本文使用了Symfony2和Doctrine2作為Web應用的開發框架,對于想了解Symfony2的數據庫操作的讀者來說閱讀本文也可以了解和掌握相關的技術和使用方法。
而由于地理位置信息的特殊性,在開發中經常會有比較難以處理的問題出現,比如:由于用戶所在位置的不固定性,用戶可能會在很小范圍內移動,而此時經緯度值也會隨之變化;甚至在同一個位置,通過GPS設備獲取到的位置信息也可能不一樣。所以如果通過經緯度去獲取周邊信息時,就很難像傳統數據庫那樣做查詢并進行緩存。
對于這個問題,有讀者可能會說有別的處理方案,沒錯,比如只按經緯度固定的幾位小數點做索引,比如按矩陣將用戶劃分到某固定小范圍的區域(可以參考后文將會提到的geohash)等方式,雖然可以繞個彎子解決,但或多或少操作起來比較麻煩,也會犧牲一些精度,甚至無法做到性能的最優化,所以不能算作是最佳的解決辦法。
而最近幾年,直接支持地理位置操作的數據庫層出不窮,其操作友好、性能高的特性也開始被我們慢慢重視起來,其中的佼佼者當屬MongoDB。
MongoDB在地理位置信息的處理上有什么優勢?下面我們通過一個簡單的案例來對比一下各種技術方案之間進行進行地理位置信息處理的差異。
對于任何LBS應用來說,讓用戶尋找周圍的好友可能都是一個必不可少的功能,我們就以這個功能為例,來看看各種處理方案之間的差異和區別。
我們假設有如下功能需求:
排除一些不通用和難以實現的技術,我們羅列出以下幾種方案:
我們一個個來分析這幾種方案。
MySQL的使用非常簡單。對于大部分已經使用MySQL的網站來說,使用這種方案沒有任何遷移和部署成本。
而在MySQL中查詢“最近的人”也僅需一條SQL即可,
注:這條SQL查詢的是在lat,lng這個坐標附近的目標,并且按距離正序排列,SQL中的distance單位為公里。
但使用SQL語句進行查詢的缺點也顯而易見,每條SQL的計算量都會非常大,性能將會是嚴重的問題。
先別放棄,我們嘗試對這條SQL做一些優化。
可以將圓形區域抽象為正方形,如下圖
根據維基百科上的球面計算公式,可以根據圓心坐標計算出正方形四個點的坐標。
然后,查詢這個正方形內的目標點。
SQL語句可以簡化如下:
這樣優化后,雖然數據不完全精確,但性能提升很明顯,并且可以通過給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.*版本中不支持)
官方指南的做法是這樣的:
這條語句的處理邏輯是先通過兩個點產生一個LineString的類型的數據,然后調用GLength得到這個LineString的實際長度。
這么做雖然有些復雜,貌似也解決了距離計算的問題,但讀者需要注意的是:這種方法計算的是歐式空間的距離,簡單來說,它給出的結果是兩個點在三維空間中的直線距離,不是飛機在地球上飛的那條軌跡,而是筆直穿過地球的那條直線。
所以如果你的地理位置信息是用經緯度進行存儲的,你就無法簡單的直接使用這種方式進行距離計算。
MongoDB原生支持地理位置索引,可以直接用于位置距離計算和查詢。
另外,它也是如今最流行的NoSQL數據庫之一,除了能夠很好地支持地理位置計算之外,還擁有諸如面向集合存儲、模式自由、高性能、支持復雜查詢、支持完全索引等等特性。
對于我們的需求,在MongoDB只需一個命令即可得到所需要的結果:
查詢結果默認將會由近到遠排序,而且查詢結果也包含目標點對象、距離目標點的距離等信息。
由于geoNear是MongoDB原生支持的查詢函數,所以性能上也做到了高度的優化,完全可以應付生產環境的壓力。
基于MongoDB做附近查詢是很方便的一件事情。
MongoDB在地理位置信息方面的表現遠遠不限于此,它還支持更多更加方便的功能,如范圍查詢、距離自動計算等。
接下來,我們結合Symfony2來詳細地演示一些使用MongoDB進行地理位置信息處理的例子。
參考環境:Nginx1.2 + PHP5.4 + MongoDB2.4.3 + Symfony2.1
建立coordinate和places兩個document文件,前者是作為places內的一個embed字段. 為方便演示效果,這里同時設置了兩個索引 2d 和 2dsphere
坐標保存以longitude, latitude這個順序(沒有明確的限制和區別,但我們在此遵循官方的推薦)。
另外,為直觀顯示查詢效果,默認使用百度地圖標記查詢數據。
我們用到的代碼包是doctrine/mongodb-odm-bundle(下文稱ODM),這個代碼包提供了在Symfony2環境下的MongoDB數據庫支持,使用這個代碼包,可以讓我們更加方便的在Symfony2環境下操作MongoDB數據庫。。
ODM封裝了MongoDB中常用的一些地理位置函數,如周邊搜索和范圍搜索。
ODM中的操作默認距離單位是度,只有geoSphere支持弧度單位(必須在參數中指定spherical(true))
MongoDB地理位置索引常用的有兩種。
關于兩個坐標之間的距離,官方推薦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.
不過,只要坐標跨度不太大(比如幾百幾千公里),這兩個索引計算出的距離相差幾乎可以忽略不計。
建立索引:
查詢方式分三種情況:
而查詢坐標參數則分兩種:
坐標對(經緯度)根據查詢命令的不同,$maxDistance距離單位可能是 弧度 和 平面單位(經緯度的“度”):
GeoJson $maxDistance距離單位默認為米:
查詢當前坐標附近的目標,由近到遠排列。
可以通過$near或$nearSphere,這兩個方法類似,但默認情況下所用到的索引和距離單位不同。
查詢方式:
查詢結果:
上述查詢坐標[121.4905, 31.2646]附近的100個點,從最近到最遠排序。
默認返回100條數據,也可以用limit()指定結果數量,如
指定最大距離 $maxDistance
結合Symfony2進行演示:
這里用near,默認以度為單位,公里數除以111(關于該距離單位后文有詳細解釋)。
通過 domain.dev/near 訪問,效果如下:
longitude: xxx, latitude: xxx為當前位置,我們在地圖上顯示了周邊100條目標記錄
MongoDB中的