ElasticSearchで地理情報も扱う
背景
昨今のスマートフォンの普及は目覚ましく、位置情報を利用したサービスも増えてきました。
位置情報サービスを構築するとき、データベースは何を使うでしょうか。
mongoDBには地理空間インデックスが用意されているため、有効な選択肢になり得ます。
位置情報と全文検索を組み合わせて検索したいときはどうでしょうか。
mongoDB + ElasticSearchなどの複数のデータベースという選択肢もありますが、条件の組み合わせ処理が煩雑になってしまいます。
今回はElasticSearchで全文検索と位置情報の両方を扱ってみます。
準備
railsからElasticSearchを利用する準備は“railsから全文検索エンジンelasticsearchを利用する"を参考にしてください。
今回作成するサンプルの要点をまとめます。
- 保存する単位は単一の位置情報を持つ「ピン」とする
- ピンは「ピンのタイトル」と「住所の文字列」、「位置情報」を持つ
- ピンはタイトルや住所などで全文検索することが出来る
- ピンは特定の位置情報を中心として半径N km以内の条件で検索することが出来る
- 全文検索と位置情報の検索は組み合わせることが出来る
テーブル定義
まずテーブルの定義をします。ピンに自由につける名前をname
、住所の情報をaddress
とします。
位置情報はlatitude
、longitude
です。
Migration
class CreatePins < ActiveRecord::Migration
def change
create_table :pins do |t|
t.string :name
t.string :address
t.decimal :latitude, precision: 20, scale: 15
t.decimal :longitude, precision: 20, scale: 15
t.timestamps
end
end
end
次にmodelの設定を行います。
今回もtireのgemを使用するので、Tire::Model
をincludeします。
include Tire::Model::Search
include Tire::Model::Callbacks
マッピングの設定に関して、name
、address
は日本語での検索を行いたいので、analyzer
にkuromoji
を指定します。
ここでのポイントはindexes :location, type: :geo_point, lat_lon: true
です。
これはlocation
というフィールドに地理情報を扱うためのgeo-point-typeを指定しています。
ただし、位置情報はlatitude
とlongitude
に格納していました。
そのためElasticSearchに渡す情報を加工することにします。
tire do
mapping do
indexes :name, analyzer: :kuromoji
indexes :address, analyzer: :kuromoji
indexes :location, type: :geo_point, lat_lon: true
end
end
ElasticSearchに渡す情報をカスタマイズするには.to_indexed_json
を定義します。
def to_indexed_json
{
name: name,
address: address,
location: location
}.to_json
end
def location
{lat: latitude.to_f, lon: longitude.to_f}
end
これでElasticSearchに渡るJSONを以下のように設定することが出来ます。
{
"name": "テスト",
"address": "日本, 東京都新宿区戸山3丁目19−1",
"location": {
"lat": 35.70675638058483,
"lon": 139.71004486083984
}
}
最後に検索ためのメソッドを用意します。
def self.search(params)
tire.search(load: true) 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
検索はPinsController
のindex
で行うことにしましょう。
class PinsController < ApplicationController
before_action :set_pin, only: [:show, :edit, :update, :destroy]
def index
# @pins = Pin.all
@pins = Pin.search params
end
~以下略~
views/pins/index.html.haml
に検索フォームをつけました。
= form_tag pins_path, method: :get do
search query
= text_field_tag :search, params[:search]
lat
= text_field_tag :lat, params[:lat]
lon
= text_field_tag :lon, params[:lon]
distance
= text_field_tag :distance, params[:distance]
= submit_tag "Search", name: nil
これで位置情報と全文検索を組み合わせて利用可能です。