隔週連載groonga

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

この記事を読むのに必要な時間:およそ 4 分

半径何kmのデータ検索

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

mroongaのストレージモードとラッパーモード

mroongaはストレージモード,ラッパーモードと2通りの使い方があり,ストレージモードでは表面上はMySQLを使っているようにSQLでデータを操作できるのに,裏はすべてgroongaでできているという特徴があります。

次回はmroongaについて紹介します。ストレージモードとラッパーモードについても説明するのでお楽しみに!

実際にはストレージモードの特徴を使い,多少無理矢理っぽいのですが,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を一度インストールして体感してみてください。

groongaの利用事例を寄稿しませんか

連載の目的は「読者の皆さんがgroongaを使いたくなる!」ことです。そこで,すでにgroongaを使っており,groongaの利用事例を本連載で紹介していただける人を募集します。募集要項を参考にご連絡ください。お待ちしています!

著者プロフィール

地価マップ作成者(ちかまっぷさくせいしゃ)

趣味プログラマ。株式会社クリアコード須藤さんがgroongaメーリングリスト,twitterで「groonga利用事例をWebの記事にしてもいいよと言う方は連絡してください」と呼びかけられていたのが目に留まり,今回記事の執筆に参加させてもらいました。