NearMe Tech Blog

NearMeの技術ブログです

RustでOpenStreetMapを触ってみる

f:id:nearme-jp:20210809224836p:plain

今回はRustという注目のプログラミング言語を用いて、OpenStreetMapというオープンな地理情報データを触ってみます。

背景

Rustに注目した背景としては、大規模かつ複雑な交通データに対して解像度をもっと上げた課題解決が必要になってきたからです。NearMeのシステムは現状、メインはTypeScriptで書いていて、最適化アルゴリズムに関連する部分をPythonで書いています。スタートアップとして、初動としてはこの構成でよかったと思いますが、更なるチューニングを求めて検討したのがRustです。

Rustの特徴としては、C/C++並の高速な処理速度、安全で効率的なメモリ管理、生産性を高める様々な機能があげられます。Pythonでも、定型の低レイヤーの処理をC/C++で行わせて、高レイヤーの処理をPythonで書くという形で、パフォーマンスと生産性を両立させることができますが、課題によっては限界があります。逆に、全部をC/C++で書くのも大変です。Rustは、流石にPythonよりは簡潔には書けないものの、C++よりは少ない努力で速さと安全性を手に入れることができると思います。別途、Goなどの他の言語との比較をすると込み入りますが、端的に言うと、今回はアルゴリズム周りでよく使われているC++のポジションにより近いということでRustを検討しました。

なお、NearMeのシステムはマイクロサービスで構築しているので、Rustで低レイヤーを書いてバインディングするという方法の他、Rustで一つマイクロサービスを構築するといことも可能です。もちろん、言語の種類が増える弊害もあるので慎重に検討する必要がありますが、その中でRustはユニークで有望な候補になっています。

ここでは、Rustを始めるとっかかりとして、OpenStreetMapのデータを読み込んで可視化したいと思います。OpenStreetMapは、"自由に利用でき、なおかつ編集機能のある世界地図を作る共同作業プロジェクト"です。世界地図は巨大なので、関東などエリアを絞ってデータをダウンロードすることができます。それでも数百Mくらいのサイズで、素のPythonで扱うにはしんどい大きさです。NearMeのシステムでもOpenStreetMapを一部利用しているところがあるのですが、コアな部分はC++で書かれたライブラリを利用しているのでカスタマイズしづらい状況です。OpenStreetMapをRustで扱えるようになれば可能性が拡がると考えています。

Jupyter NotebookでRustを扱う

インタラクティブにプログラミングを試せる環境として、Jupyter Notebookは最適です。利用できる言語はPythonがデフォルトですが、他の言語もJupyter kernelをインストールして利用することができます。

ここではDocker/Docker Composeを用いて、RustのkernelがインストールされたJupyter Notebookの環境を用意します。次のように、こちらのリポジトリをダウンロードしてDockerでコンテナを建ち上げてください。

git clone git@github.com:kenji4569/jupyter-rust.git
cd jupyter-rust
docker-compose up

建ち上げたコンテナのログの最後の方に、

http://127.0.0.1:8888/?token=xxx

という記載があるので、このURLをブラウザで開いてください。

そして、ブラウザ上で、notebooks/evcxr_jupyter_tour.ipynb というノートブックを開いて、Rustのプログラムを実行してみてください。

OpenStreetMapのデータを扱う

データをダウンロード

まずは、https://download.geofabrik.de/asia/japan.html から、kanto-latest.osm.pbf というファイルをダウンロードしてください。これは関東エリアの交通データになります。データはProtocol Buffersで符号化されています。

このファイルをjupyter-rust/notobooksディレクトリに置き、同ディレクトリにて新たなNotebookファイルをkenelとしてRustを選択して作成してください。

データの読み込み

osmpbfreaderというライブラリを用いて、先ほどダウンロードしたデータを読み込んでみます。 以下は、そのコードです。

1行目の、

extern crate osmpbfreader;

にて、osmpbfreaderライブラリがダウンロードされ、コンパイルされます。

kernelのセッションごとに、/tmp以下にディレクトリが作られて、そこでファイルが展開されています。kernelをリスタートすると、別のディレクトリになるので、ライブラリを再度ダウンロードしてコンパイルすることになります。ここは遅いのですが、sccacheを利用して多少改善することができます。cargo install sccacheをDockerfileに書いてビルドし直して、:sccache 1をNotebook上で実行すると適用されます。

レコードの1行1行読み込みは、

for obj in pbf.par_iter().map(Result::unwrap) {

にて行われます。今回は全部で約4000万レコードを走査しています。

手元のMacBook(仮想マシン上でCPU: 2.9G x 2, Memory: 16G)では、この走査は8秒程度でした(正確にはtimeitで測りますが、:timingをNotebook上で実行するとセル単位の実行時間を測ることができます)。.par_iter().iter()にすると、CPUの並列処理が行われず16秒程度でした。なお、C++をバインドしたPythonのライブラリpyosmiumで読み込むと50秒程度、素のPythonで書かれたライブラリosmreadだと10分くらいかかりました(ただしこちらは素のPythonのprotobufの実装が遅いという要因もあるようです)。ちなみに、GoのライブラリosmpbfはRustと同程度の速度でした。

各レコードは、Node、Way、Relationのいずれかの型になっていて、以下のように場合分けして処理しています。

match obj {
    osmpbfreader::OsmObj::Node(node) => {
        ...
    }
    osmpbfreader::OsmObj::Way(way) => {
        ...
    }
    osmpbfreader::OsmObj::Relation(rel) => {
        ...
    }
}

今回は、それぞれ初回に読み取ったレコードを出力しています。例えば、初回のNodeレコードは、

Node { id: NodeId(31236558), tags: Tags({}), decimicro_lat: 356350730, decimicro_lon: 1397681010 }

となっています。

Nodeは一つの地点を表していて、緯度経度の情報を保持しています。

Wayは複数のNodeからなり境界線を表現しています。NodeとWayからグラフ構造が導かれます。

Relationは、NodeやWayや他のRelationからなり、一括りの有名な道路といった少し大きな構造を表します。

タグの表示

Node、Way、Relationそれぞれの各オブジェクトに対して、複数のTagが付与されています。Tagはkey-valueの形式で、例えば、道の特徴として、 Road Features Mapping Guide | Mapbox にあるようなTagが付与されています。以下のコードでタグを集計して表示します。

一番目のセルでは、Node、Way、Relationそれぞれにおいて、Tagのkey別の集計をとり、回数が多いものを上位から表示しています。まだ一部ですが、様々なタグが存在するのが見て取れます。

二番目のセルでは、"highway"をkeyとするTagにおいてvalue別の集計を行い、回数が多いものを上位から表示しています。特に、Wayにおいて、"primary"や"secondary"といった値があるのに着目してください。これらは道の種類を表していて、経路探索において重要なマーカーになります(参考1参考2)。

グラフ構造の可視化

最後に、NodeとWayからなるグラフ構造を可視化します。以下のコードでは、東京駅付近にあるNodeとそれに紐づくWayを選択して描画しています。

こちらがその出力です。 f:id:nearme-jp:20210810205351p:plain

ここではNodeを緯度経度を座標とする点、Wayを道の種類で色付けした線として描画しています。 赤の線は大通り、青の線は一般的な車道、緑の線は歩行者用の道、灰色の線はその他の境界線となっています。

おわりに

RustをJupyter Notebookで動かせるように環境構築し、OpenStreetMapのデータを読み込んで、そのデータ構造を眺めてきました。Rustは高速に動作する一方、様々なデータ操作を比較的簡潔に書くことができたのではないかと思います。多少独特な部分はありますが、結果的に怪しい書き方が矯正される感覚になります。ここから先もっと色々なアルゴリズムを試せると思いますので、当記事がそのきっかけになれば幸いです(OpenStreetMapに関する有用なRustのライブラリはこちらが参考になりました)。

最後になりますが、NearMeではエンジニアを募集しています!まだまだ多くの可能性が潜んでいる領域です。興味を持った方はぜひ以下から応募いただければと思います。

Author: Kenji Hosoda