ゼロから始めるRails位置ゲーサーバ(その1)

こんにちは。クライアントチームサーバサイドエンジニア、コウです。最近、 Rails で位置ゲーのサーバサイドを実装しました。
なので、 Rails で位置ゲーサーバの実装方法について、3つの Part に分けて紹介させていただきます。

ポーリング API で、スマホのリアルタイム位置情報にもとづいて、何キロ範囲内の全てのマーカーデータをデータベースからとるには、いくつの実装方法があります。

Geohash

最初におすすめされたのは Geohash です。
Geohash は経緯度に基づくジオコーディング方法の一つです。
Geohash の gem を使えば、データベースは MySQL のままでも、実装できます。
でも、 Geohash の gem が大分古かったので、使えるかどうか試しませんでした。
なぜなら、もっと簡単な方法があるからです!

Geohashに興味のある方は、杉山さんの記事「サーバーで付近の情報を通知するサービスのつくり方」がおすすめです。

ElasticSearch

ElasticSearch はオープンソース全文検索エンジンです。
ElasticSearch はリアルタイム位置情報検索が得意だそうです。
あまり詳しくないので、今回はスルーさせてください。
なので、ElasticSearch Geolocation のドキュメント URL だけを貼りますね。

RedisとMongoDB

Redis 3.2 から Geo メソッドがいくつか追加されました。 Sorted Set で Geo 検索できます。2016年にリリースされたばかりなので、使った例がおおくないです。
リアルタイム位置情報のアップデートとセレクトが多い場合、MongoDB の Geospatial がよく使われています。スピードがかなり速いです。
でも今回のマーカーデータは全部事前に登録できる上に、リレーションデータもあります。
MongoDB を使うメリットはそんなに大きくないです。

MySQL Spatial Data

そういうわけで、もう既に MySQL をインストールしたので、 MySQL のSpatial Data についても調べました。
最初は、 MySQL の Spatial Data がいいと思いました。
インデックスも付けられるし、点と点の間の距離計算できます。
だけど!
「ユーザーから N キロメートル範囲内の全てのマーカーを取り出す」という簡単なことができるけど!
SELECT文が(私にとって)かなりわかりづらいです。

SELECT
    *, (
      6371 * acos (
      cos ( radians(78.3232) )
      * cos( radians( lat ) )
      * cos( radians( lng ) - radians(65.3234) )
      + sin ( radians(78.3232) )
      * sin( radians( lat ) )
    )
) AS distance
FROM markers
HAVING distance < 10
ORDER BY distance;

ref: stackoverflow (英語, published at 2014/02/18)

SET @user = ST_GeomFromText('POINT(139.777254 35.713768)');
SELECT *, ST_Distance_Sphere(@user, lonlat) AS distance
    FROM markers
    WHERE ST_Distance_Sphere(@user, lonlat) <= 10000
    AND ST_Within(
      lonlat,
      ST_Buffer(@user, DEGREES(300/(6370986*COS(RADIANS(ST_Y(@user))))),
      ST_Buffer_Strategy('point_square'))
    )
    ORDER BY distance;

ref: Shogo’s Blog (日本語, published at 2017/03/28)

sin cos acos radians が多くないでしょうか?
6371 は何の数字でしょうか?
ST_Distance_Sphere は遅い、Geometry 型しか使えないことも記事に書いてあります。
そんなこと思いつつ、別の解決方法を探り始めました。

PostgreSQL の PostGIS

「世界で最も人気なオープンソースデータベース」の MySQL から離れて、 ようやく、「世界で最も先進的なオープンソースのデータベース」の PostgreSQL までたどり着きました。 簡単に PostgreSQL + PostGIS のメリットを紹介しましょう。

  • リレーションデータベース
  • ドキュメント ( HTML / PDF / EPUB ) が完璧
  • PostGIS の使いやすさ (Geography 対応)
  • RDS の PostgreSQL に PostGIS のイクステンションがあらかじめインストールされている
  • ActiveRecord 専用 gem activerecord-postgis-adapter あり、その上、ずっとメンテナンスされている
  • 何よりセレクトが速い!!!

ベンチマークの数字からPostGISの性能のよさを証明しましょう。
下記は「railsでユーザーの緯度経度から10キロ以内のレコードを取り出して、距離でオーダーする」のセレクト文のベンチマークです:

User
  .select("users.*, st_distance(location, 'point(116.458104 39.966293)') as distance")
  .where("st_dwithin(location, 'point(116.458104 39.966293)', 10000)")
  .order("distance")
レコード件数 10w 20w 50w 100w 260w 1000w
かかった秒数 1.2ms 1.4ms 1.8ms 2.5ms 4.2ms 15.3ms

ref: Ruby China #22059 (中国語, published at 2014/10/15)

もう一度いいます!
何よりセレクトが速いです!

結論

なので、今回の仕様に一番ぴったりな解決方法は、 PostgreSQL の PostGIS で実装することでした。
めでたし〜めでたし〜

次回の Part 2 で、 上記にも出てた謎な英単語 「 Geography 」、 と「 Geometry 」について、簡単に話します。
お楽しみにしてください〜(<ゝω・)☆


(/・ω・)/ ★☆★☆★ サーバサイドエンジニア大絶賛募集中 ★☆★☆★ \(・ω・\)