railsアプリ(db:mysql)での位置情報範囲検索におけるgeocoderとquadkeyの比較

# TL;DR
railsアプリにてアプリケーション層(SQL)のみにて位置情報検索を行うさいに[geocoder](https://github.com/alexreisner/geocoder)と[quadkey](https://github.com/deg84/quadkey)を検討しました。
geocoderはピンポイントな位置からの文字通り周辺半径内の情報を検索出来ますが複雑なクエリを発行しているために多少のチューニングは必要です。
quadkeyはその性質上、演算的によってピンポイントな位置を指定してそこからフレキシブルな指定範囲を検索するということは困難もしくは多少の作り込みが必要です。

# いきさつ
__後々役に立つ事間違いない！__ との無邪気で無垢な衝動から溜め込まれた位置情報をついに活用する時がやってきた。だがしかし特定位置から一定範囲内のポイントを検索する手法をDBのアップグレードや切替をせずにアプリケーションの層(SQL)でどう行なうのか。
当初はgeohashを使えばどうにかなるかなと調査していたところに[quadkey](https://github.com/deg84/quadkey)と[geocoder](https://github.com/alexreisner/geocoder)による実装を発見したので、簡単なサンプルを実装して比較してみる。

| 構成など | バージョン |
|:-:|:-:|
| ruby | 2.2.3 |
| DB  | mysql 5.6.34 |
| rails  | 4.5.2.2  |

# 検証アプリを用意

## rubyの準備

諸般の理由がありruby2.2.3, rails 4.2.5.2です。

```shell-session
rbenv shell 2.2.3
gem install rails --no-rdoc --no-ri -v 4.2.5.2
```

## rails new

検証目的は主にDBのパフォーマンスなので不要なものはskipしました。

```shell-session
rails new geo_sample \
--skip-bundle \
--skip-sprockets \
--skip-spring \
--database=mysql \
--skip-javascript \
--skip-turbolinks \
--skip-test-unit
```

## Gemfile追加

目的のgemなどを追加し、インストール。

```rb:Gemfile
gem 'puma'
gem 'geocoder'
gem 'quadkey'
```

```shell-session
bundle ins --path ./vendor/bundle
```

緯度経度情報を保持するテーブルを作ります。

```shell-session
bundle exec rails g model cordinate \
latitude:float:index \
longitude:float:index \
quadkey15:string:index \
quadkey22:string:index
```

テスト用データを用意します。
5万件ほど。今回は[埼玉県](http://www.gsi.go.jp/KOKUJYOHO/CENTER/kendata/saitama_heso.htm)をターゲットに大雑把な矩形領域を取ってそのなかに緯度経度をランダムに登録しました。ちなみに私は埼玉県民ではありません。

```rb:seeds.rb
count = Cordinate.count
(count..50000).each do | i |
  lat = Kernel.rand(Cordinate::SOUTH_EDGE..Cordinate::NORTH_EDGE)
  lng = Kernel.rand(Cordinate::WEST_EDGE..Cordinate::EAST_EDGE)
  Cordinate.create!(
  	latitude: lat,
  	longitude: lng,
  	quadkey15: Quadkey.encode(lat, lng, 15),
  	quadkey22: Quadkey.encode(lat, lng, 22)
  )
  print '.'
end
```

geocoderのデフォルトが`mi(マイル?)`だと使いにくいので変更します。

```rb:config/initializers/geocoder.rb
Geocoder.configure(units: :km)
```

## 環境をまとめて起動

docker-composeでまとめてサーバを立ち上げます。

```Dockerfile:./db-server/Dockerfile
FROM mysql:5.6.34
```

```Dockerfile:Dockerfile
FROM ruby:2.2.3

# 必要なライブラリインストール 
RUN apt-get update -qq && apt-get install -y build-essential

# ワークディレクトリ設定
ENV ROOT_PATH /app
RUN mkdir $ROOT_PATH
WORKDIR $ROOT_PATH
ADD Gemfile $ROOT_PATH/Gemfile
ADD Gemfile.lock $ROOT_PATH/Gemfile.lock
RUN bundle install
ADD . $ROOT_PATH
```

```yaml:docker-compose.yml
version: '3'
services:
  db:
    build: "./db-server"
    environment:
      MYSQL_ROOT_PASSWORD: 'app_password'
      TZ: 'Asia/Tokyo'
    ports:
      - 3306:3306
  app:
    build: .
    environment:
      - PORT=3000
    command: bundle exec pumactl start
    volumes:
      - .:/app
    ports:
      - '3000:3000'
    depends_on:
      - db
```

```shell-session
docker-compose build
docker-compose up -d db
docker-compose run --rm app bundle exec rake db:migrate db:setup
docker-compose up
```

## 実験

今回のコードは[https://github.com/chrhsmt/geo_rails_sample](https://github.com/chrhsmt/geo_rails_sample)に上げてあります。
quadkeyによる検索は指定精度による前方一致検索をSQLで投げています。

### サンプルデータを地図にプロットすると

埼玉県庁を中心に半径0.5km(quadkey的には精度15で1km四方)のデータを取得して地図に描画してました。

![https://i.gyazo.com/9eae58897cfa8c950b5003d6cca29a7f.png](https://i.gyazo.com/9eae58897cfa8c950b5003d6cca29a7f.png)

赤い矩形領域がquadkeyでの領域で赤い点がその領域内のみで判定されたポイント、緑の円領域がgeocoderでの領域で緑の点がその領域内のみで判定されたポイント、青の点は両領域で被っているポイントです。

画像を見て分かる通り、quadkeyによる単純な領域指定はその領域が演算的に決定されてしまっているのでピンポイントの位置からのフレキシブルな範囲指定はできません。quadkeyにてフレキシブルさを求めるなら精度を更に細かく設定した矩形領域を組み合わせる手法が求められるでしょう。

### パフォーマンス

geocoderとquadkeyの双方でランダムな検索をしてパフォーマンスを取ってみました。
(index最適化は取得カラムで不要なものを削りました)

| bench | quadkey | geocoder | geocoder(index最適化) |
|:-:|:-:|:-:|:-:|
| -n 50 -c 5 | Requests per second: 9.63 /sec (mean) <br>Time per request: 519.381 ms (mean)  | Requests per second: 4.49 /sec (mean) <br> Time per request: 1113.652 ms (mean)  | Requests per second: 7.25 /sec (mean) <br> Time per request: 690.015 ms (mean) |
| -n 100 -c 10 |  Requests per second: 10.45 /sec (mean) <br> Time per request: 956.520 ms (mean)  |  Requests per second: 4.15 /sec (mean) <br>Time per request: 2410.117 ms (mean) | Requests per second: 6.29 /sec (mean) <br> Time per request: 1588.721 ms (mean) |
| -n 500 -c 50 | Requests per second: 9.66 /sec (mean) <br>Time per request: 5178.409 ms (mean)  | Requests per second: 4.44 /sec (mean) <br>Time per request: 11269.104 ms (mean)  | Requests per second: 6.25 /sec (mean) <br> Time per request: 7996.864 ms (mean) |



## 参考
- [GeoHashとは違う、緯度経度のHash的なQuadKeyを作ってみた - http://d.hatena.ne.jp/maachang/20150404/1428139492](http://d.hatena.ne.jp/maachang/20150404/1428139492)