隔週連載groonga

第2回groongaをRuby On Railsでも使ってみた ~chikamap.comの事例から

mroongaを使ってchikamap.comを作った地価マップ作成者です。

筆者は趣味でプログラムなどをしています。元々はWindowsをメインで使っていましたが、数年前からRubyが気になりMacへ乗り替えてRuby On Railsで色々個人的なサイトを作っています。

今回は、素人ながらgroonga、mroongaを使ってサイトを作った事例を紹介します。

chikamap.comとは

chikamap.comは中古住宅・土地購入の際に参考になる地価履歴やその周辺の公共施設・交通機関などの基本的な情報を地図から表示するというだけの簡単なサイトです。

サイトとしてはデータを表示するだけなので構築は比較的簡単な部類だと思いますが、⁠地図への地価履歴表示」「その周辺の公共施設、交通機関を表示」部分について位置情報としてのデータベース検索が必要になります。

地図への地価履歴表示

検索の方法はデータベースに登録してある地価の位置情報を、Google マップで表示している地図の北西の緯度経度と南東の緯度経度から、四角いエリアとしてその内側にあるデータを絞り込んで表示します。

北西・南東のポイント
北西・南東のポイント

その周辺の公共施設、交通機関を表示

この地図に表示された地価のポイントをクリックすることで、指定の半径何kmの周辺施設、交通機関を検索し表示します。

周辺施設・交通機関地図例
周辺施設・交通機関地図例

データベース自体には上のような四角いエリア内のデータを検索するような機能は元々あったりしますが、ある位置から半径何kmのデータを検索するようなデータベースの機能は作成当時の自分では見つけることができませんでした。

しかし位置情報からのデータベース検索方法を色々探している内に、このククログの記事が目に留まりました。この記事ではgroongaを使って四角いエリア内、半径何km内のデータを検索する方法が記載されており、まさに自分の必要としている機能が簡単に実現できるものでした。

groongaを直接コマンドから実行しても良いのですがchikamap.comはRuby On Railsを使って作っているため、MySQL経由でgroongaが使えればデータの登録から検索までコードがすっきりするのではないかと思い今回初めてmroongaを使ってみました。

データベースへの位置情報データ登録について

地価の情報、周辺施設・交通機関のデータについては緯度経度としてデータを保存します。これにはMySQLのデータタイプとしてpointというspatialデータタイプを使うことになります。しかし素のRailsではmigrationでエラーが出るためactiverecord-mysql2spatial-adapter gemを使っています。これを使うとt.pointのようにmigrationが書けます。

# db/migrate/xxxxxxx.rb

create_table :model_names, options: "ENGINE=mroonga" do |t|
  # point データタイプ
  t.point :location, null: false
end

データはRuby On Railsでいつもやるようにモデルを経由してsaveしていけば良いので、groongaを使うための特別な変更は必要ありません。

四角いエリアのデータ検索

データを入れた後、地図に表示されている四角いエリアでのデータは次のように検索しています。

north_west = "#{north_west_longitude} #{north_west_latitude}"
south_east = "#{south_east_longitude} #{south_east_latitude}"
ModelName.where("MBRContains(GeomFromText('LineString(#{north_west}, #{south_east})'), location)")

難しいことは抜きにして、北西と南東の緯度経度さえ分かればこれだけでそのエリア内のデータが抽出できとても簡単です。

Google マップAPIのgetBounds()から表示中の南西と北東の緯度経度(south-west and north-east corners)が取得できます。これを北西・南東の緯度経度へ置き換えて使っています。

半径何kmのデータ検索

四角いエリアは先述のように検索できるのですが、クリックした地価ポイントから半径何kmの円の内側にある施設等をSQLで実行する方法がよく分かりませんでした。そこで、groongaを外部コマンドとして実行し何とか動くようにしたいと考えました。

実際にはストレージモードの特徴を使い、多少無理矢理っぽいのですが、MySQLのデータディレクトリにできているgroongaファイルをRailsから次のように外部コマンドで実行しています。

# config/initializers/mysql_datadir.rb

# MySQLのコマンドでdatadirを探す
result = ActiveRecord::Base.connection.execute 'show variables like "datadir"'
MYSQL_DATADIR = Hash[*result.first]["datadir"].freeze
# app/controllers/application_controller.rb

# mroongaデータベースファイル(*.mrn)を見つける
def mroonga_database
  Pathname(MYSQL_DATADIR).join "#{Rails.configuration.database_configuration[Rails.env]["database"]}.mrn"
end

# table_name  | mroongaデータベース登録のテーブル名 = :model_namesなどmigrationでのテーブル名(複数形)と同じ
def radius_search(table_name, longitude, latitude, km)
  filter = Shellwords.escape 'geo_in_circle(location,"#{latitude.to_f},#{longitude.to_f}",#{km * 1000})'
  # --limit -1を指定しないとデフォルトでは最大10件のみで返される(当初ハマりました)
  results = JSON.parse `groonga #{mroonga_database} select #{table_name} --filter '#{filter}' --limit -1`

  # 結果配列から_keyインデックス位置の値を取って集める
  data = results[1].first
  attributes = Hash[*data[1].flatten]
  key_index = attributes.keys.index "_key"
  stations = data[2..-1]
  ids = stations.collect do |station|
    station[key_index]
  end

  # ActiveRecordから対象のデータを検索
  relation = table_name.to_s.camelize.singularize.constantize.where(id: ids)
end

色々やっていますが、要はククログの記事と全く同じです。mroongaで作ったgroongaのデータファイルを探すところが増えているぐらいで、後はJSONで返ってくる結果を分解して、テーブルのプライマリキーとなる値を_keyから集めていき、それをまとめてActiveRecordから再度検索しています。

まとめ

今回groongaで全文検索の機能は使っていませんが、Ruby On RailsでもMySQLとmroongaで全文検索をつかうことが簡単に実現できます。

一つ問題があるとすれば、mroongaをRuby On Railsで使い、かつMySQL5.5以降を使っている場合には、全文検索用のインデックスにparserの指定をインデックスのコメントとして記述する必要があることが挙げられます。

特にmigrationファイルではインデックスにコメントをつけることができず、エラーになるため、筆者は次のようなモンキーパッチでインデックスにコメントをつけられるように変更して使っています。

# config/initializer/mroonga_index.rb

module MroongaIndex
  def mroonga_index(table_name, column_name, options = {})
    index_type = if options[:parser].present?
      MYSQL_INDEX_FULLTEXT
    end
    index_sql = %|CREATE #{index_type} INDEX index_#{table_name}_on_#{column_name} ON #{table_name}(#{column_name})|
    index_sql = mroonga_fulltext_index(index_sql, options) if index_type == MYSQL_INDEX_FULLTEXT

    execute index_sql
  end

  private
    MYSQL_INDEX_FULLTEXT = "FULLTEXT"

    def index_comment?
      @mysql_version = `mysql --version` rescue nil
      @mysql_version =~ /Distrib ([\d\.]+?),/
      major, minor, patch = $1.to_s.split(".")
      5 <= major.to_i and 5 <= minor.to_i
    end

    def mroonga_fulltext_index(index_sql, options)
      raise <<-ERROR unless index_comment?
  mysql version needs to be 5.5 and later
  #{@mysql_version || "mysql not installed"}
      ERROR

      parsers = %w[TokenBigram
                   TokenMecab
                   TokenBigramSplitSymbol
                   TokenBigramSplitSymbolAlpha
                   TokenBigramSplitSymbolAlphaDigit
                   TokenBigramIgnoreBlank
                   TokenBigramIgnoreBlankSplitSymbol
                   TokenBigramIgnoreBlankSplitSymbolAlpha
                   TokenBigramIgnoreBlankSplitSymbolAlphaDigit
                   TokenDelimit
                   TokenDelimitNull
                   TokenUnigram
                   TokenTrigram]
      raise "parser not defined" unless parsers.include?(options.fetch(:parser){nil})

      parser = options.fetch(:parser)
      %|#{index_sql} COMMENT 'parser "#{parser}"'|
    end
end

module ActiveRecord
  class Migration
    include MroongaIndex
  end
end

この変更は次のように使います。

# db/migrate/xxxxxxx.rb

create_table :model_names, options: "ENGINE=mroonga" do |t|
  t.string :address, null: false
end

# 上のモンキーパッチをこんな感じで使い全文検索のインデックスコメントを入れる
mroonga_index(:model_names, :address, parser: "TokenBigram")

全文検索をするときはMATCH AGAINSTを使います。

# app/controllers/xxx_controller.rb

def search
  # 全文検索する時はMATCH AGAINSTで日本語もOK
  ModelName.where("MATCH(address) AGAINST(?)", params[:keywords])
end

WindowsからMacに乗り替えて、Ruby On Railsにもまだまだついて行けないことが多いのですが、筆者でもgroongaを使うことで位置情報のエリア内検索をしたり、全文検索をしたりがいとも簡単にできてしまいます。

すこし前まではインストールはソースからなどと、筆者のような初心者にはとても敷居が高かったのですが、最近ではHomebrewがとても便利でgroongamroongaもコマンド一発でインストールできてしまいます(Windows、Linuxでもインストールは簡単です⁠⁠。

全文検索はどうも難しそうだと思われていた方、筆者のような初心者でも簡単に導入することができるため、この機会にgroongaを一度インストールして体感してみてください。

おすすめ記事

記事・ニュース一覧