持續搞【附近】系列—聽說MongoDB是專業的(三)

  • 2019 年 11 月 13 日
  • 筆記

還是得好用才行

一直聽說MongoDB才是【專業】搞地理空間查詢的,人家才是【專業】的!相當長一段時間來,一說搞【附近】就會相當一批人的腦海里就不自主浮想到MongoDB… …

上一節中的geohash頂多能應付一下點的運算,本質上是將二維的數據一維化然後通過索引提高預算查詢效率,相比之下MongoDB最大的優勢和優點就是:

  • 效率高很多
  • 支援多點、線、多邊形
  • 球面運算

按說吧,上面劃橫線的才是榜樣模板式的回答,然而實際上對於我們這個龐大的泥腿子群體而言,MongoDB最大的優勢是:

複製粘貼一下demo程式碼,CURD就能用

MongoDB的地理空間索引分為兩種類型:

  • 2d索引,用於平面地圖之流,反正也能用
  • 2dsphere索引,用於地球兒表面的地理查詢運算,推薦用法

先說2d索引,然而實際上MongoDB的2d索引的實現底層原理依然是geohash,所以同樣其2d索引支援點的存儲運算,對於線和面就相對比較難受了(PS:由於市面上好像並沒有看到名字類似於《MongoDB內核分析》或《MongoDB設計與實現》的書籍,所以對於MongoDB的2d索引的實現結論我的印象停留在【幾年前】取自於mongo官網的一篇blog)。所以,既然你都用MongoDB了,直接一步到位走2dsphere就行了。

2dsphere的實現並不是geo-hash,我依然是從MongoDB官網的blog上了解到的一些資訊和資料。2dsphere採用的是【穀人希】公司的google S2演算法,將S2處理好的索引數據保存在了B-Tree數據結構中,B-Tree可以支援快速查詢。

如果有曾經深入研究過MongoDB這兩種地理空間索引實現的老哥們,可以公眾號發消息幫我double check一下是否正確。畢竟官方的blog資料發表的年份都是比較老的,不敢保證這些年新版本MongoDB是否依然如此。

然而,下面我並不打算剖析【穀人希】公司的S2演算法的大概原理(自己理解並不算深刻),也並不打算科普B-Tree和BTree以及B+Tree的區別(太煩人了)。後面我會抽空專門整理一篇關於標題類似於《人類關於N種地理空間索引實現方案橫向大評測》之類的文章,畢竟,當年為了搞【附近】我是曾經下過真功夫的。

實際上從上面文字中可以看出,IT人拼到最後全是演算法和數據結構

這個和桃兒說過的「 藝人拼到最後拼的是文化 」基本上是很類似的。

在MongoDB中,2dsphere或2d是描述是由一種叫做geoJSON的標準格式來描述的,從名字上就可以看出來首先它是一坨JSON,其次它定義了自己的一些標準。比如我們要用geoJSON來描述一個點、一條線,就應該用如下方式進行描述:

// 描述一個點"loc":{  "type":"Point",  "coordinates":[ 116.55944824218749,30.58827267102698 ]}// 描述一個多邊形"box":{  "type":"Polygon",  "coordinates":[                  [                     [116.40701293945311,30.454001045389525],                    [116.77505493164062,30.454001045389525],                    [116.77505493164062,30.76248901825541],                    [116.40701293945311,30.76248901825541],                    [116.40701293945311,30.454001045389525]                  ]                ]}

確切說,除了MongoDB外,還有很多支援地理空間索引的資料庫或引擎都會支援geoJSON標準。

下面進入到我們最喜歡的複製粘貼程式碼階段,正式開始前我們需要說明下世界上最好的語言和MongoDB之間不得不說的故事。MongoDB為PHP提供了兩個版本的驅動:

  • mongodb,支援PHP7,持續支援更新中
  • mongo,僅支援PHP5,目前只管修bug其他統統不管

這兩個驅動的用法完全不一樣,然而我不得不承認明顯PHP5版本的驅動使用起來更符合人類胃口,PHP7版本簡直是要人命。

普及一下,像這種驅動提供的API都是low-level API,為了更加方便地協助我們搞花式CRUD,我從github上找了一個基於PHP7 MongoDB low-level-API包裝了一層的high-level-API library。這個東西同樣也是MongoDB官方出品,地址如下:

https://github.com/mongodb/mongo-php-library

下面我們將使用上面這個庫演示如何使用MongoDB的2dsphere搞【附近】

第一步:創建2dphere索引

因為MongoDB是帶有KV性質的文檔型資料庫,所以有一點兒和MySQL非常不一樣的就是:不需要提前定義資料庫欄位。在正式使用2dsphere索引之前,我們要做的就是首先在【某個欄位】上創建一個2dsphere索引,大概就是下面這樣:

<?php// 加入composer支援require_once __DIR__ . "/vendor/autoload.php";// 這句相當於使用momo資料庫,然後使用user表$collection = ( new MongoDBClient )->momo->user;// 在loc欄位上創建2dsphere索引// 儘管還沒有loc欄位,不過這並不重要$result = $collection->createIndex( array('loc' => '2dsphere',) );var_dump( $result );

上面程式碼保存為index.php,然後php index.php執行,結果你們感受一下:

第二步:搞一坨測試數據

然後我們圍繞在北京附近創建一坨測試數據,程式碼如下:(我知道肯定有人看到下面這一坨程式碼後會想到BulkWrite,你就當我是個蠢貨)

<?php// composerrequire_once __DIR__ . "/vendor/autoload.php";// 這句相當於使用momo資料庫,然後使用user表$collection = ( new MongoDBClient )->momo->user;// 然後插入10000數據即可for ( $i = 1; $i <= 10000; $i++ ) {// 生成一個維度  $latitude  = mt_rand( 38, 40 ).'.'.mt_rand( 100000, 999999 );// 生成一個經度  $longitude = mt_rand( 115, 116 ).'.'.mt_rand( 100000, 999999 );  $insert = $collection->insertOne( array(// 你可以粗暴認為:_id就是mongodb的主鍵,如果你不顯式為_id賦值// 那麼mongodb將會自動會_id生成一坨類似於uuid的值'_id' => $i,// 注意這個loc欄位對應上面創建索引的欄位名'loc' => array(// type為point,表示是點,除此之外還有Line和Polygon兩種類型'type'        => 'Point',// 經度、維度'coordinates' => array( floatval( $longitude ), floatval( $latitude ) ),    ),  ) );  var_dump( $insert );}

php index.php執行,結果你們感受一下:

第三步:開始搞【附近】

我們將圍繞經緯度(116.2092590332,40.0444375846)進行查找,為了對演示結果心裡有譜,請你將(116.2092590332,40.0444375846)也插入到MongoDB中,這樣查詢結果中排名離你最近的一定會是該用戶咯~

<?phprequire_once __DIR__ . "/vendor/autoload.php";// 這句相當於使用momo資料庫$database = ( new MongoDBClient )->momo;// command方法相當於直接執行mongodb原生語句// 因為我懶的看這個mongodb-library的庫語法了$cursor = $database->command( array('geoNear' => 'user','near'    => array('type'        => 'Point', 'coordinates' => array(116.2092590332,40.0444375846    ),    ),  'num' => 5,  ) );$rets = $cursor->toArray()[0];foreach( $rets->results as $r ) { echo $r->obj['_id'].'號用戶距離您 : '.$r->dis.'米'.PHP_EOL;}

php index.php執行,結果你們感受一下:

距離0米,這說明老子的程式碼不是TM瞎寫的,絕對能用!

實際上,如果說我們把【xxxx號用戶】當作是【牌照xxxx的計程車】的話,一般最粗暴版本的【搞附近的車】業務就基本上得到實現了。

好了,本節暫時到此。周六日碼文章,真的是好睏!

本文關鍵詞

MongoDB、2dsphere,Google S2、B-Tree、geoJSON

本文程式碼github

https://github.com/elarity/wechat-official-accounts-demo-code