MongoDB面白いですね。
ドキュメントが親切なのでありがたいのですが、なかなか日本語の記事が無かったりするので、Geospatial Indexingについて試してみた事を投稿しますよ!
マニュアル:MongoDB internal corp site
Geospatial Indexing は MongoDB ver1.3.3以上で使えます。
地理空間のインデックスとは
読んで字のごとくなのですが、MongoDBでは二次元地理空間情報(多くの場合、緯度経度情報)をインデックスとして検索をかけることが出来ます。
最近のガラケーやスマートフォンからは簡単にGPS機能を利用することもできますし、PCブラウザなどでも位置情報を取得出来るようになったり、html5のGeolocation API(厳密にはHTML5に含まれてはいない)の登場などで、今後、ますます位置情報の利用頻度は高くなるんじゃないかと思います。
でも、位置情報って検索が面倒だったりして、そのままで使いやすい情報ではないです。
GeoHashやGeoHexなどというエリア(面)情報として文字列で保存する方法などもありますが、一長一短。
例えば、GeoHashは、文字数が長いほど詳細なエリアを表し、検索なども非常に使い勝手がいい(文字列比較で済む)のですが、その特徴上、距離でのソートが難しかったり、検索エリアの範囲を細かく指定しづらかったりと、デメリットもあります。
どんな技術も、制約と利便性のトレードオフなので、上手く使い分けてやる必要があります。
今回は、MongoDB の Geospatial Index を用いて「近い店検索」のようなものを実験してみます。
データの追加
今回は、テスト用なので渋谷付近にランダムなデータを100件ほど追加します。
以下はmongoシェルでの操作です。
// geotest というDBを使います use geotest // 渋谷付近の経度緯度 var baseLng = 139.65534000898916; var baseLat = 35.63588652257869; var diffLng = 139.75730686689930 - baseLng; var diffLat = 35.67661462940957 - baseLat; // カテゴリのリスト var categories = ['カフェ', 'バー', 'レストラン']; // ランダムに100件のデータを追加 for (var i=1; i<=100; i++) { var myLng = baseLng + (Math.random() * diffLng); var myLat = baseLat + (Math.random() * diffLat); var myCategory = categories[ Math.floor(Math.random() * categories.length) ]; // ランダムなデータを保存 db.places.save({ location: [myLng, myLat], // 位置情報 category: myCategory // カテゴリ }); }
shell で JavaScript が利用できるのは幸せですね。
ランダムデータの生成も楽ちんです。
Index を作成
次に、地理空間インデックスを作成します。
MongoDB では複合インデックスもOKなので、位置情報と共に、カテゴリ情報にもインデックスを張ります。
// インデックスを作成 db.places.ensureIndex({ location : "2d", // location に Geospatial Index を張る category : 1 // category に Index を張る });
検索してみる
完全一致で検索
db.places.find({ location : [139.66, 35.65] });
普通のインデックスで検索
db.places.find({ category : "レストラン" });
$near で検索
これだと、あまり意味がないのですが、$nearを使う事で、近いロケーションを検索することができます。
(実際には近い順に並び替えられる)
// [139.66, 35.65] の近くにある店を5件検索 db.places.find({ location : { $near : [139.66, 35.65] } }).limit(5);
もちろん、複合インデックスでの指定もOKです。
// [139.66, 35.65] の近くにあるカフェを5件検索 db.places.find({ location : { $near : [139.66, 35.65] }, category : "カフェ" }).limit(5);
$nearの注意点
$near は経度と緯度の1度が同じ距離を指すことを前提として計算が行われます。
例えば、
【基点】 経度:10° 緯度:-84°
から、
【点A】 経度:12° 緯度:-84° 【点B】 経度:10° 緯度:-85°
への距離を計算した場合、経度と緯度の1度が同じ距離である平面とすると、下図のような感じになるため、点Bの方が近いように見えます。
実験してみましょう。
// 点A の追加 db.places.save({ location: [12, -84], name: 'A' }); // 点B の追加 db.places.save({ location: [10, -85], name: 'B' }); // 基点から近いポイントの検索 db.places.findOne({ location: { '$near': [10, -84] } });
結果は下記のように点Aが返ります。
{ "_id" : ObjectId("4cef9fb0a22c7137b609561a"), "location" : [ 10, -85 ], "name" : "B" }
ですが。。。。
実際には、地球は球体なので場所によって1度は変わります。
今回の経度・緯度の場合、下記のような間隔になると予想出来ます。
※実際の縮尺ではありません。
下記サイトで、地球を球体として二点間の距離を計算してみましょう。
2地点間の距離と方位角 - 高精度計算サイト
すると、
【基点-点A間の距離】23.270942 km 【基点-点B間の距離】111.319491 km
となり、実際には点Aの方が近い事がわかります。
この問題に解決するため、新しい球体のモデルがバージョン1.7.0で追加されました。
球体モデルを使用して求める
この先の球体モデルは MongoDB ver1.7.0 以上で使えます。
利用は簡単です。$near を $nearSphere に変更するだけです。
実際にやってみましょう。
db.places.findOne({ location: { '$nearSphere': [10, -84] } });
結果は下記のように正しく点Aが返るようになりました。
{ "_id" : ObjectId("4cef9fada22c7137b6095619"), "location" : [ 12, -84 ], "name" : "A" }
なお、球体モデルを使用する場合には、位置情報を X(経度), Y(緯度) の順で使っていると仮定されるため、その順序を守る必要があります。
サンプルプログラム
今回は、GoogleMaps APIを使用してサンプルプログラムを作ってみました。
mongodbとのやり取りは下記のような形でPHPにて行っています。
<?php $geo = new getGeoInfo(); // 見つける! $geo->find(array( 'location' => array( '$nearSphere' => array( (float) $lng, (float) $lat, ), ), )); class getGeoInfo { private $mongo; private $collection; public function __construct() { // mongodb に接続 $this->mongo = new Mongo('mongodb://localhost:27077', array('persist' => 'myPersistId')); // collection を選択 $this->collection = $this->mongo->selectDB('geotest201011')->selectCollection('places'); } public function find($query = array(), $limit = null) { // findを実行して、カーソルを取得 $cursor = $this->collection->find($query); // JSON形式で返す echo '['; $isFirst = true; $i = 1; while ($cursor->hasNext()) { if (!is_null($limit) && $limit < $i) break; $item = $cursor->getNext(); $exportItem = array( 'location' => $item['location'], 'category' => $item['category'], ); echo ($isFirst) ? '' : ', '; echo json_encode($exportItem); if ($isFirst) { $isFirst = false; } ++$i; } echo ']'; } }
以上、駆け足ですが、MongoDBでの地理空間インデックスのサンプルでした!