MongoDBでゆるふわDB体験

第9回MongoDBの地理空間インデックス

地理空間インデックスの概要

MongoDBでは、地理情報をデータとして登録し、地理空間インデックスを作成することにより、位置情報を利用した検索クエリが可能になります。たとえば、山手線の駅の地理情報をMongoDBに登録して、地理情報インデックスを張っておけば、自分のいる場所から近い順に山手線の駅を検索するということが可能となります。

MongoDBの地理空間インデックスは、バージョン1.4という非常に早い時期からサポートされており、人気サイトであるfoursquareで使用されているという実績を持っています。

こちらに、地理空間インデックスに関する開発の歴史をまとめました。

バージョンリリースされた機能
1.4地理空間インデックスをサポート
1.8球面空間でのクエリをサポート
2.4GeoJSONオブジェクトをサポート[1]

それではさっそく地理空間インデックスを使ってみましょう。

シンプルなデータで地理空間インデックスを使ってみる

2dインデックスの作成

地理空間インデックスを使うためには、2dインデックスという特別なインデックスの作成が必要となります。以下のクエリで、yamanotesenコレクションのlocフィールドに2dインデックスを作成します。

> db.yamanotesen.ensureIndex( { loc : "2d" } );

次にデータのinsertを行います。

データの準備

地理空間インデックスを使うため、経度・緯度の情報を含めたデータを挿入します。今回は山手線の7つの駅を使用しました。

経度緯度
五反田139.72382235.625974
恵比寿139.71007035.646685
新宿139.70046435.689729
新大久保139.70026135.700875
池袋139.71108635.730256
上野139.77704335.713790
品川139.73899935.628760

経度・緯度は配列またはハッシュとして保存します。今回はlocというフィールドに配列として保存します。yamanotesenというコレクションにinsertするクエリは、以下のようになります。

> db.yamanotesen.insert({ name:"五反田",loc: [ 139.723822, 35.625974 ] });
> db.yamanotesen.insert({ name:"恵比寿",loc: [ 139.710070, 35.646685 ] });
> db.yamanotesen.insert({ name:"新宿",loc: [ 139.700464, 35.689729 ] });
> db.yamanotesen.insert({ name:"新大久保",loc: [ 139.700261, 35.700875 ] });
> db.yamanotesen.insert({ name:"池袋",loc: [ 139.711086, 35.730256 ] });
> db.yamanotesen.insert({ name:"上野",loc: [ 139.777043, 35.713790 ] });
> db.yamanotesen.insert({ name:"品川",loc: [ 139.738999, 35.628760 ] });

準備は整いましたので、いよいよ検索してみましょう。

近傍を検索するクエリ

近傍の検索には、$nearオペレータを使用します。今回は渋谷駅[ 139.701238, 35.658871 ]から近い順に3つの駅を取得してみましょう。クエリは、このようになります。

> db.yamanotesen.find({ loc : { $near : [ 139.701238, 35.658871 ] }}).limit(3)

今回準備した7つの駅で、渋谷駅から近い駅上位3件の結果はこのようになりました(_idの表示は省略しています⁠⁠。

> db.yamanotesen.find({ loc : { $near : [ 139.701238, 35.658871 ] }},{"_id":0}).limit(3)
{ "name" : "恵比寿", "loc" : [ 139.71007, 35.646685 ] }
{ "name" : "新宿", "loc" : [ 139.700464, 35.689729 ] }
{ "name" : "五反田", "loc" : [ 139.723822, 35.625974 ] }

検索結果の詳細情報取得

geoNearコマンドを使用することで、オブジェクトまでの距離と、いくつかの統計情報を取得することができます。

> db.runCommand({'geoNear':'yamanotesen', near : [ 139.701238, 35.658871 ], num: 1})
{
        "ns" : "test.yamanotesen",
        "near" : "1110100101001011001100110110111110110111011101111101",
        "results" : [
                {
                        "dis" : 0.015050010631232593,
                        "obj" : {
                                "_id" : ObjectId("514a653517ca6b06a3266e26"),
                                "name" : "恵比寿",
                                "loc" : [
                                        139.71007,
                                        35.646685
                                ]
                        }
                }
        ],
        "stats" : {
                "time" : 0,
                "btreelocs" : 0,
                "nscanned" : 5,
                "objectsLoaded" : 3,
                "avgDistance" : 0.015050010631232593,
                "maxDistance" : 0.015066109444883333
        },
        "ok" : 1
}
resultsフィールドで返される配列内のdisフィールドが距離になります。今回は渋谷駅から恵比寿駅までの距離が0.015050010631232593となっています。これは、平面空間上における、座標[ 139.701238, 35.658871 ]と 座標[ 139.710070, 35.646685 ]の2点間の距離です。

経度・緯度の1度の距離を乗することで、実際の距離を計算することが可能です。正確な値ではありません[3]が、今回は経度・緯度の1度を全て111.262283kmとして計算してみますと、

0.015050010631232593×111.262283=約1.7km

となります。

地球は球面なので平面空間で計算すると誤差が出てしまいます。MongoDBは球面空間でのクエリもサポートしていますので、次は球面空間で計算してみましょう。

地球の球面を考慮したクエリ

球面空間で近傍の検索には、$nearSphereオペレータを使用します。使い方は$nearオペレータと同じです。

> db.yamanotesen.find({ loc : { $nearSphere : [ 139.701238, 35.658871 ] } }).limit(3)

球面空間でgeoNearコマンドを使用するには、runCommandの中にspherical:trueというオプションを追加します。

> db.runCommand({'geoNear':'yamanotesen', near : [ 139.701238, 35.658871 ], num: 1, spherical:true})
{
        "ns" : "test.yamanotesen",
        "near" : "1110100101001011001100110110111110110111011101111101",
        "results" : [
                {
                        "dis" : 0.0002468278845577094,
                        "obj" : {
                                "_id" : ObjectId("514a653517ca6b06a3266e26"),
                                "name" : "恵比寿",
                                "loc" : [
                                        139.71007,
                                        35.646685
                                ]
                        }
                }
        ],
        "stats" : {
                "time" : 0,
                "btreelocs" : 0,
                "nscanned" : 5,
                "objectsLoaded" : 3,
                "avgDistance" : 0.0002468278845577094,
                "maxDistance" : 0.0002471235288170513
        },
        "ok" : 1
}

spherical:trueとしてgeoNearコマンドを使用すると、disフィールドは0.0002468278845577094となりました。単位はラジアンなので、地球の半径距離6378kmを乗することで実際の距離になります。

0.0002468278845577094×6378=約1.5km

渋谷駅と恵比寿駅の直線距離として、かなり現実に近い数値が取得できることがわかりました。

範囲内の検索

$geoWithinオペレータ[4]と以下のオペレータを使用することで、指定範囲内のドキュメントや、指定点から円形範囲のドキュメントを取得できます。

オペレータ用途
$box指定した四角形内のドキュメントを取得
$polygon指定した多角形内のドキュメントを取得
$center中心と半径を指定した円内のドキュメントを取得
$centerSphere球面空間での$center

GeoJSONで地理空間インデックスを使ってみる

バージョン2.4からGeoJSONがサポートされました。GeoJSONは、空間上で点や直線や多角形という図形を扱うためのフォーマットです。GeoJSONを使用することで、交差する図形の検索ができるようになりました。また、これまでに紹介した近傍の検索も可能です。概要に関しては、この連載の前回第8回「リリース間近! MongoDB 2.4の新機能」で紹介しましたので、今回はインデックスの作成と、交差する図形の検索クエリを紹介します。

2dsphereインデックスの作成

GeoJSONを使用するには、これまで使用していた2dインデックスではなく、2dsphereインデックスを使用します。geojsonコレクションのgeoフィールドに2dsphereインデックスを作成するクエリは、このようになります。

> db.geojson.ensureIndex( { geo : "2dsphere" } );

GeoJSONオブジェクトのinsert

今回はGeoJSONオブジェクトの一つである、LineStringを使用します。これは空間に直線を表現するオブジェクトです。3つのLineStringをinsertしてみます。

> db.geojson.insert({"name": "tate05", geo:{ "type": "LineString", "coordinates": [ [ 5, 0 ], [ 5, 10 ] ] }});
> db.geojson.insert({"name": "tate10", geo:{ "type": "LineString", "coordinates": [ [ 10, 0 ], [ 10, 10 ] ] }});
> db.geojson.insert({"name": "tate15", geo:{ "type": "LineString", "coordinates": [ [ 15, 0 ], [ 15, 10 ] ] }});

上記のクエリにより、図1のように垂直な直線が3本ある状態となりました。

図1 3本の垂直な直線
図1 3本の垂直な直線

交差する図形の検索

交差するGeoJSONオブジェクトを検索するには$geoIntersectsオペレータを使用します。上の3本の直線に対して、水平な直線[ [ 0, 5 ], [ 12, 5 ] ]を引き、交差するオブジェクトを検索してみます。

図2 水平な直線を追加
図2 水平な直線を追加

クエリはこのようになります。

> db.geojson.find({geo:{ "$geoIntersects": { "$geometry": { "type": "LineString", "coordinates": [ [ 0, 5 ], [ 12, 5 ] ]}} }}, {"_id":0});
{ "name" : "tate05", "geo" : { "type" : "LineString", "coordinates" : [ [ 5, 0 ], [ 5, 10 ] ] } }
{ "name" : "tate10", "geo" : { "type" : "LineString", "coordinates" : [ [ 10, 0 ], [ 10, 10 ] ] } }

検索結果には、横の直線と交差しているオブジェクトであるtate05とtate10の2つが返ってきました。

次回のテーマ

今回はMongoDBの特徴的な機能の一つである地理空間インデックスについて紹介いたしました。すでに多くの位置情報を利用したサービスで使用されていましたが、バージョン2.4でGeoJSONをサポートしたことにより、さらに使用範囲が広くなりました。今後も、位置情報を利用する際の選択肢として、MongoDBは有力な候補となりそうです。

次回はMongoDBでのMap/Reduceについて紹介する予定です。

おすすめ記事

記事・ニュース一覧