昨日(2010/12/12)開催された「第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyo」で、MongoDBの地理空間インデックスの紹介をLTとしてやりました。
初LTで緊張しまくってましたが(?)皆様の暖かいまなざしに救われ、なんとか無事に終われました。
勉強会全体の内容については、主催者でもある@doryokujinさんがまとめられた記事をご覧ください。
「第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyo」を開催してきました! - doryokujin's blog
で、地理空間インデックスに関するいくつかの補足と、コピペ用のテキストを書いておきたいと思います。
Geospatial Indexingの速度
昨日、Geospatial Indexingの速度はどうなのか聞かれ、数万件程度のレコードでは(普通のIndexに比べて)重くは感じない、と答えたのですが、気になったのでちゃちゃっと検証してみました。
(簡易的な検証ですので参考程度に)
検証環境
(localhost上での検証。各テストは3回計測し、その平均値を掲載しています)
保存
1,000万件のレコードをインサートした時の結果です。
名前 | 普通のIndex(ObjectId) | GeoIndex |
---|---|---|
合計時間(秒) | 942.7 | 1,162.2 (+23%) |
秒間に処理された件数(/sec) | 10,613.4 | 8,606.7 (-19%) |
※カッコ内は、ObjectIdに比べての増減の比率
GeoIndexについては、あらかじめensureIndexを実行してあります。
ObjectIdと比較すると、保存では2割程度の速度低下が見られました。
これは、内部的にGeoHash化するため、その計算処理が多くを占めていると思われます。
検索
上記で保存したデータ(1,000万件)を、ObjectIdでの検索と、GeoIndexの$near, $nearSphereで検索をそれぞれ10万回した時の結果です。
(findOne で実行)
名前 | 普通のIndex(ObjectId) | $near | $nearSphere |
---|---|---|---|
合計時間(秒) | 6.2 | 8.5 (+37%) | 9.3 (+50%) |
秒間に処理された件数(/sec) | 16,155.4 | 11,770.8 (-38%) | 10,770.8 (-51%) |
※カッコ内は、ObjectIdに比べての増減の比率
検索に関しては、ObjectIdと比較し、4〜5割程度の速度低下となりました。
これは、基点のGeoHash処理に加え、GeoHashでの前方一致検索を行った後に、距離計算とソートが行われるためかと思われます。
結論
スライド
スライド内容の補足と、コピペ用コードたち
地理空間のインデックス、Geospatial Indexingでは、二次元の地理空間情報をキーに検索を行うことが出来ます。
経度・緯度以外の情報を入れることも出来ますが、普通は経度・緯度情報を使うと思います。
Geospatial Index の作成
Indexを作るには、普通のIndexと同じで、コレクションのensureIndexメソッドを使うのですが、Geospatial Index の場合は、2dという文字列を指定します。
ただし、コレクション毎に、一つのGeospatial Indexしか作れません。
db.shopinfo.ensureIndex({ // loc フィールドに Geospatial Index を張る loc : "2d" });
もちろん、複合Indexとして普通のIndexと同時に指定する事も出来ます。
db.shopinfo.ensureIndex({ loc : "2d", // Geospatial Index を張る category: 1 // 他のフィールドに Indexを張る });
ちなみに、デフォルトでは、±180のfloat値を受け付けますが、拡張も可能です。
db.shopinfo.ensureIndex({ loc : "2d", // Geospatial Index を張る }, { // オプションで拡張もできる min: -1000, max: 1000 });
保存
var lon = 139; var lat = 35; db.shopinfo.save({ name: '喫茶もんご', category: ['喫茶店'], loc: [lon, lat] // 位置情報 (x, y の順がオススメ) // loc: [x: lon, y: lat] // 連想配列でも // loc: [lon: lon, lat: lat] // lon,latでも // loc: [foo: lon, bar:lat] // 適当な名前でもOK });
クエリー:完全一致
// ほぼ使い道なし。。。 db.shopinfo.find({ loc: [139, 35] });
クエリー:$near
db.shopinfo.find({ loc : { $near : [139.4, 35.4] // [139.4, 35.4]から近い順にソート } });
クエリー:$maxDistance
db.shopinfo.find({ loc : { $near : [139.4, 35.4], $maxDistance : 2 // 最大距離が ±2 のものを検索 } });
クエリー:複合条件
db.shopinfo.find({ loc : { $near : [139.4, 35.4], $maxDistance : 2 }, category: '喫茶店' });
クエリー:geoNearコマンド
geoNearコマンドを使うと、詳細情報を取得することが出来ます。
runCommandメソッドで、geoNearでコレクション名を指定します。
指定出来るオプションには、"near", "maxDistance", "query"なんかがあります。
db.runCommand({ geoNear: "shopinfo", near: [139.4, 35.4], maxDistance: 2, query: {category: '喫茶店'} // 普通のクエリも使える });
上記を実行すると下記のように返ってきます
{ "ns" : "test.shopinfo", // ↓基点に指定した情報をGeoHash化したビット列 "near" : "1110100101001011000011000101000010111011111111011110", "results" : [{ "dis" : 0.5656861498877501, // 距離(単位:度) "obj" : { "_id" : ObjectId("4d01f5616919cb54afce6a77"), "name" : "喫茶もんご", "category" : ["喫茶店"], "loc" : [139, 35] } }], "stats" : { "time" : 0, "btreelocs" : 0, "nscanned" : 1, "objectsLoaded" : 1, "avgDistance" : 0.5656861498877501, "maxDistance" : 0.5656861498877501 }, "ok" : 1 }
地理空間インデックスでは、内部でGeoHashを併用することで、高速性を高めています。
領域のクエリー:$within
長方形のモデルの場合、右上と左下(もちろん、右下と左上とかでもいいのだけど…)の点情報を配列で指定します。
db.shopinfo.find({ loc : { $within : { $box : [[130, 30], [140, 35]] // (130, 30) - (140, 35)の長方形内 } } });
円形のモデルの場合、中心点と半径情報を配列で指定します。
db.shopinfo.find({ loc : { $within : { $center : [[135, 35], 4] // (135, 35)から半径4の円の範囲 } } });
地球は球体:Sphere クエリ
(本当は楕円だ、とか、そういうツッコミはとりあえず置いておきます!)
経度1度の距離は赤道から離れるほど、小さくなるので、精度が落ちてしまう、
というか、ソートの順番が正しくない場合があり、使い物になりません。
Ver.1.7.0以降でサポートされたSphereクエリ使いましょう。
使い方は、$near の代わりに $nearSphere
db.shopinfo.find({ loc : { $nearSphere : [139.4, 35.4] } });
$center の代わりに $centerSphere
db.shopinfo.find({ loc : { $within : { // (135, 35)から半径 0.1ラジアンの円の範囲 $centerSphere : [[135, 35], 0.1] } } });
を使うだけ(簡単!)
ちなみに、$boxSphereというのは、仕組み上意味をなさないので無いよ!
runCommandでの球体モデル
runCommandで、球体モデルを使用したい場合は、spherical パラメータを true にします。
db.runCommand({ geoNear: "shopinfo", near: [135, 35], query: {category: '喫茶店'}, spherical: true // 球体モデルを有効に });
結果はこんな感じ(抜粋)
"results" : [{ "dis" : 0.05718377899700883, // ラジアン単位で返ります "obj" : { "loc" : [139, 35] } }],
検索基点から検索結果までの距離がdisというプロパティで返ってくる(ラジアン単位)ので、上記の場合、0.057…ラジアン×6371kmで、大体364 km になるよ。
というわけで、一緒にMongoDBで遊び(?)ましょう!