mongodb - Geospatial Indexing | サイバーエージェント 公式エンジニアブログ
こんにちは。Amebaでアプリケーションエンジニアをしています、宍戸です。
今回は、mongodbの機能であるGeospatial Indexingについて、v1.6系、v1.8系、v2.0系について特徴的な点の比較検証をおこなってみた結果についてまとめたいと思います。


■地理空間インデックスについて
地理空間のインデックスは以下のような機能になります。(日本語ドキュメントから引用)
MongoDBでは、二次元の地理空間のインデックス(geospatial index)を持っています。これは位置をベースにしたクエリーのためのもです。たとえば、"自分の場所から近いNアイテムを取得"といったことです。また"自分の場所から近いN個のミュージアムを取得"と言った追加のフィルターを追加することも効果的にできます。

位置情報の保持方法については、公式ドキュメントにあるように、緯度と経度を配列などで保持し、インデックスを作成することで、地理空間インデックスを利用することができます。



基本的な操作方法、特長などについては公式ドキュメントや多くのmongodbを利用されている方々のblogなどにまとめられていますので今回は割愛させていただきます。ここでは実際に利用・検証を行った以下の項目についてまとめました。

---------------------------------
1. $nearコマンドの精度(Spherical Modelとの比較)
2. Sharding環境での利用
3. Multi-location Documents
---------------------------------

1. $nearコマンドの精度(Spherical Modelとの比較)
「自分の場所から近いN個のミュージアム」といった情報を検索する際には$nearオペレータを利用します。
(今回、検証用に山手線各駅のデータ(駅名、緯度経度)を利用して検証を行いました。)

サイバーエージェントビル(緯度:35.657694, 経度:139.696195)から、近い順に3件のデータを取得した結果は以下のようになります。
$サイバーエージェント 公式エンジニアブログ


地理空間インデックスを用いた検索についてはfind()とおなじような機能でgeoNearというコマンドも用意されています。こちらは、特定の位置からの距離やトラブルシューティング用の診断情報なども返してくれる便利なコマンドです。
$サイバーエージェント 公式エンジニアブログ


geoNearコマンドで返されるdisというフィールドが、基準点と、対象の緯度経度との距離になります。
この例では、渋谷駅まで、0.00517881705643294とありますが、実際の距離(メートル)に変換するには、以下の式に当てはめればOKです。
0.00517881705643294(ラジアン) × 6378(地球の半径(km)) = 33.0304952(km)

この結果を見ると、ちょっと現実離れした数値になっています。
この精度問題はv1.8以降のsphericalオプションを追加することで解決します。
sphericalオプションを追加した場合($nearSphere)のfind()クエリは以下のようになります。
$サイバーエージェント 公式エンジニアブログ


geoNearコマンドもこのオプションに対応して使うことができます。
$サイバーエージェント 公式エンジニアブログ

先ほど同様、距離を計算してみると、かなり精度に違いがあることがわかります。
0.00007441565604614601(ラジアン) × 6378(地球の半径(km)) = 0.474623054(km)

この誤差を許容できるか、は用途次第かと思いますが、誤差に関しては
どの程度なのか、きちんと把握しておく必要がありそうです。



2. Sharding環境での利用
公式ドキュメントに書かれているとおり、MongoDBのv1.7.1以前では地理空間インデックスを利用したCollectionはShard設定をすることができませんでしたが、改善されています。
実際に、先程のCollectionをShardingするよう設定して、検索クエリを投げてみると失敗してしまいます。
$サイバーエージェント 公式エンジニアブログ


現在、$near($nearSphere)オペレータは、Sharding環境において利用することができません。
(参考リンク)
[#SERVER-1981] Convert $near queries to geoNear command in mongos

代わりに、geoNearコマンドを使うことで、現状はこの問題を解決できます。
$サイバーエージェント 公式エンジニアブログ
(結果としては、statsフィールドにshardsの値が追加された以外、違いはありません)

JavaのODM(Object Document Mapper)はまだこのgeoNearコマンドに対応していないものが多く、独自で対応せざるを得ない状況でしたが、先日リリースになったSpring Data MongoDB 1.0.0 M4がこのクエリをサポートしています。今後、その他のODMの対応も進んでいくことになるかと思いますので注目しています。



3. Multi-location Documents
v2.0で地理空間インデックスに新たな機能としてMulti-location Documentsが追加されています。

以下のようなデータを用意して、試してみます。
ユーザーの位置情報のブックマークのようなデータ構造を用意します。
$サイバーエージェント 公式エンジニアブログ

まずは今まで通り検索してみます。(以下の結果は抜粋です)
$サイバーエージェント 公式エンジニアブログ

検索結果はデフォルトでドキュメント毎のため、複数ある位置情報のうち「どの位置が」近いのかわかりません。
Multi-location Documentsにあわせて追加された、includeLocsオプションを使うと、「どのデータが、どれくらい近いか」を得ることができます。(白枠で囲まれた部分が、「該当する緯度経度」と「検索の基準点からの距離」となります。)
$サイバーエージェント 公式エンジニアブログ


また、例として「東京駅から近い位置情報を持つドキュメントを3件」取得したいケースにおいて、保持しているデータが「東京駅、有楽町駅、神田駅」のような場合には、最寄りの3件がそれにあたるため、以下のような戻り値になってしまいます。(先ほどと同じクエリを発行した結果の一部を抜粋しています。)「userId=4」のドキュメントが重複してしまっています。disとlocの値を見ると、返している値が「東京」と「有楽町」を指していることがわかります。
$サイバーエージェント 公式エンジニアブログ

重複するドキュメントをまとめるには、uniqueDocsオプションを利用します。
$サイバーエージェント 公式エンジニアブログ

距離も同じ場合には、該当するドキュメントは時系列の昇順で並ぶようです。
(今回の例では、userId=2のデータの「東京」とuserId=4のデータの「東京」は位置情報として同じ距離ですが、保存した順がuserId順のため、このような順番で出力されています。(_id順である、とも言えます))
例として、uniqueDocsを利用して「特定地点の周辺spotをお気に入り登録している人を5人取得」といった使い方ができそうです。



■まとめ
mongodbの地理空間インデックスについて、いくつかの機能をピックアップして使ってみました。
現状、v1.6系では距離に関する精度に難がありそうですので、その点についてはv1.8以降を利用する必要があること、またSharding環境での利用については、geoNearコマンドを利用する必要があることなどを実際に利用することで確認できました。
また、v2.0以降のMulti-location Documentsは、オブジェクトをembedすることができるmongodbの特徴を活かすことができる機能になっていると思います。ですが現状では、ドキュメントの重複、位置情報の特定など、注意点があるため、解決策として、uniqueDocsオプション、includeLocsオプションが用意されていることと、その挙動を確認できました。使いどころ次第ではクエリを何分の1かに軽減できる機能かと思うので、よく検討して使いたい機能ではないかと思います。
今回確認したのは一部の機能であり、現状の使い勝手が悩ましいものについても、日々更新されているので、定期的にキャッチアップ、検証を行い、引き続き利用していければと思います。