ElasticSearchでの位置情報取り扱いの性能を検証する

背景

ElasticSearchで地理情報も扱う"で、ElasticSearchで全文検索だけで無く位置情報の検索も同時に取り扱うことを試しました。
今回はそのElasticSearchでの位置情報利用が実用に足るかの検証を行います。

テストデータ

今回ベンチマークで使用するデータは、国土交通省が公開している位置参照情報ダウンロードサービスを利用させて頂きます。
今回はその中でも東京都のデータを使用しました。

東京都のデータだけでも解凍したCSVで約30MB、データは291104件あるのでテストデータとしては十分でしょう。

ちなみに、都道府県別データの中で最大なのは愛知県で、解凍したCSVが約116MB、データは1127623件あります。

ダウンロードしたCSVファイルの文字コードはShift-JISなので、後で扱いやすいようにnkf -Swなどして変換しておきます。
また、ヘッダーが日本語なので適当にアルファベットに変更しました。

データサンプル

"pref","city","town","number","ref","x","y","lat","lon","house","rep","before_update","after_update"
"東京都","千代田区","麹町六丁目","5","9","-34965.0","-9246.0","35.684800","139.731181","1","1","0","0"
"東京都","千代田区","神田神保町一丁目","58","9","-33443.9","-6898.1","35.698530","139.757108","1","1","0","0"
"東京都","千代田区","神田神保町一丁目","60","9","-33427.9","-6863.2","35.698675","139.757493","1","1","0","0"
"東京都","千代田区","神田神保町二丁目","42","9","-33505.0","-7006.9","35.697979","139.755906","1","1","0","0"
"東京都","千代田区","神田神保町二丁目","44","9","-33493.4","-6981.9","35.698084","139.756182","1","1","0","0"

テストデータはrailsルートのdata/test_data.csvに配置しました。

データ取り込み

今回、取り扱うCSVのファイルサイズが大きめなので、smarter_csvのgemを使います。
Gemfilegem 'smarter_csv'を追加してbundle installします。

次に、データの取り込み用にrakeタスクを作成してみます。

bundle exec rails g task data

lib/tasks/data.rake

namespace :data do
  desc "テストデータの取り込み"
  task :import => :environment do
    smarter_csv_options = {
      convert_values_to_numeric: true,
      headers_in_file: true,
      col_sep: ",",
      skip_blanks: true,
      chunk_size: 100,
      header_converters: :symbol
    }

    file = "data/test_data.csv"
    SmarterCSV.process file, smarter_csv_options do |chunk|
      chunk.each do |c|
        hash = {
          address: "#{c[:pref]}#{c[:city]}#{c[:town]}#{c[:number]}",
          latitude: c[:lat],
          longitude: c[:lon]
        }
        Pin.create hash
      end
    end
  end
end

これで以下のコマンドで全データをインポートします。

bundle exec rake data:import

ページネーション

29万件のデータを取り込んだので、そのままindexにアクセスすると大変なことになりそうです。
そこでページネーション機能を追加します。

ページネーションにはkaminariを使います。

tireがkaminariと互換性があるため、tire.searchpage: (params[:page] || 1)を追加するだけです。

app/models/pin.rb

def self.search(params)
  tire.search(load: true, page: (params[:page] || 1)) do
    query {
      string "name:#{params[:search]} address:#{params[:search]}"
    } if params[:search].present?
    filter :geo_distance, {
      distance: "#{params[:distance].present? ? params[:distance].to_f : 10}km",
      location: {lat: params[:lat].to_f, lon: params[:lon].to_f}
    } if params[:lat].present? && params[:lon].present?
  end
end

viewもページネーションに対応させるので、app/views/pins/index.html.hamlの下部に以下のコードを追加します。

app/views/pins/index.html.haml

= paginate @pins

これでページネーション機能が追加できました。

ベンチマーク

東京駅を中心として、半径1km以内で、住所に「八重洲」を含むピンを検索してみました。
検証の環境はMacBook Air 13-inch Mid 2011, プロセッサ:1.7GHz Intel Core i5, メモリ 4GBです。

Processing by PinsController#index as HTML
  Parameters: {"utf8"=>"✓", "search"=>"八重洲", "lat"=>"35.680685897019096", "lon"=>"139.76755142211914", "distance"=>"1"}
  Pin Load (0.5ms)  SELECT `pins`.* FROM `pins` WHERE `pins`.`id` IN (1994, 2872, 2973, 4049, 4051, 4056, 2627, 2691, 2797, 2831)
  Rendered pins/index.html.haml within layouts/application (32.1ms)
Completed 200 OK in 106ms (Views: 89.9ms | ActiveRecord: 0.5ms)

100msで返ってくるなら十分ではないでしょうか。
半径の指定や検索キーワードをいろいろ試してみても十分な速度でした。