Ruby on RailsでMySQLとMroongaを使って日本語全文検索を実現する方法 - 2015-11-10 - ククログ

ククログ

株式会社クリアコード > ククログ > Ruby on RailsでMySQLとMroongaを使って日本語全文検索を実現する方法

Ruby on RailsでMySQLとMroongaを使って日本語全文検索を実現する方法

MySQLは先日リリースされたMySQL 5.7から標準機能で日本語のテキストを全文検索できるようになりました。逆に言うと、現在広く使われているMySQL 5.6以前では日本語のテキストを全文検索できません。MySQLにMroonga(むるんが)というストレージエンジンを導入することで日本語のテキストを全文検索できるようになります。しかもMroongaは高速です。MySQL 5.7で導入された日本語全文検索機能よりも高速です。

MySQLと全文検索エンジンサーバーを組み合わせて日本語全文検索を実現することもできますが、管理するサーバーが増える・SQL以外に全文検索エンジンサーバーのことを覚える必要があるなど開発・運用時のコストが高くなります。MySQLだけで完結できた方が開発時も運用時も楽になります。

この記事ではRuby on Railsで作ったアプリケーションからMroongaを使って日本語全文検索機能を実現する方法を説明します。実際にドキュメント検索システムを開発する手順を示すことで説明します。ここではCentOS 7を用いますが、他の環境でも同様の手順で実現できます。

MySQLとMroongaのインストール

まずMySQLとMroongaをインストールします。CentOS 7以外の場合にどうすればよいかはMroongaのインストールドキュメントを参照してください。

% sudo -H yum install -y http://packages.groonga.org/centos/groonga-release-1.1.0-1.noarch.rpm
% sudo -H yum install -y http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm
% sudo -H yum install -y mysql-community-server
% sudo -H systemctl start mysqld
% sudo -H yum install -y mysql-community-mroonga

Rubyのインストール

CentOS 7にはRuby 2.0のパッケージがありますが、Ruby on Rails 4.2.4はRuby 2.2が必要なのでrbenvとruby-buildでRuby 2.2をインストールします。

% sudo -H yum install -y git
% git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
% git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
% echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
% echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
% exec ${SHELL} --login
% sudo -H yum install -y gcc make patch openssl-devel readline-devel zlib-devel
% rbenv install 2.2.3
% rbenv global 2.2.3

Ruby on Railsのインストール

Ruby on Railsをインストールします。

% gem install rails

ドキュメント検索システムの開発

いよいよ日本語全文検索機能を持ったドキュメント検索システムを開発します。

まずはrails newで雛形を作ります。

% sudo -H yum install -y mysql-community-devel
% rails new document_search --database=mysql
% cd document_search

Active Record 4.2.4ではmysql2 gemは0.3系でなければいけないのでGemfileでバージョンを指定します。

Gemfile:

@@ -4,7 +4,7 @@ source 'https://rubygems.org'
 # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
 gem 'rails', '4.2.4'
 # Use mysql as the database for Active Record
-gem 'mysql2'
+gem 'mysql2', '~> 0.3.20'
 # Use SCSS for stylesheets
 gem 'sass-rails', '~> 5.0'
 # Use Uglifier as compressor for JavaScript assets

バージョンを更新します。

% bundle install

therubyracer gemを有効にします。

Gemfileから次の行のコメントを外します

# gem 'therubyracer', platforms: :ruby

therubyracer gemをインストールします。

% sudo -H yum install -y gcc-c++
% bundle install

データベースを作成します。

% bin/rake db:create

ここまでは(ほぼ)Mroongaと関係ない手順です。アプリケーションがMySQLを使う場合にはよくある手順です。

ここからはMroongaを使う場合に特有の手順になります。

まず検索対象のドキュメントを格納するテーブルを作成します。

% bin/rails generate scaffold document title:text content:text

rake db:migrationする前にマイグレーションファイルを編集してストレージエンジンをMroongaに変更します。

@@ -1,6 +1,6 @@
 class CreateDocuments < ActiveRecord::Migration
   def change
-    create_table :documents do |t|
+    create_table :documents, options: "ENGINE=Mroonga" do |t|
       t.text :title
       t.text :content

このマイグレーションファイルを反映します。

% bin/rake db:migrate

全文検索用のインデックスを作成します。

まずマイグレーションファイルを作成します。

% bin/rails generate migration AddFullTextSearchIndexToDocuments
      invoke  active_record
      create    db/migrate/20151109143515_add_full_text_search_index_to_documents.rb

db/migrate/20151109143515_add_full_text_search_index_to_documents.rb次のような内容にします。ここでtype: :fulltextを指定してインデックスを追加することがポイントです。

class AddFullTextSearchIndexToDocuments < ActiveRecord::Migration
  def change
    add_index :documents, :content, type: :fulltext
  end
end

このマイグレーションファイルを反映します。

% bin/rake db:migrate

MySQL側の準備はできたのでアプリケーション側に全文検索機能を実装します。

モデルに全文検索用のスコープを定義します。MySQLで全文検索を実行するときはMATCH(...) AGAINST('...')を使います。Web検索エンジンのように「キーワード1 OR キーワード2」とORを使ったクエリーを指定できるMATCH(...) AGAINST('*D+ ...' IN BOOLEAN MODE')がオススメです。

class Document < ActiveRecord::Base
  scope :full_text_search, -> (query) {
    where("MATCH(content) AGAINST(? IN BOOLEAN MODE)", "*D+ #{query}")
  }
end

ビューにヒット件数表示機能と検索フォームをつけます。検索フォームではqueryというパラメーターに検索クエリーを指定することにします。

app/views/documents/index.html.erb:

@@ -2,6 +2,13 @@
 
 <h1>Listing Documents</h1>
 
+<p><%= @documents.count %> records</p>
+
+<%= form_tag(documents_path, method: "get") do %>
+  <%= search_field_tag "query", params["query"] %>
+  <%= submit_tag "Search" %>
+<% end %>
+
 <table>
   <thead>
     <tr>

最後に、コントローラーで全文検索を使うようにします

@@ -5,6 +5,10 @@ class DocumentsController < ApplicationController
   # GET /documents.json
   def index
     @documents = Document.all
+    query = params[:query]
+    if query.present?
+      @documents = @documents.full_text_search(query)
+    end
   end
 
   # GET /documents/1

これで日本語全文検索機能は実現できました。簡単ですね。

動作を確認するためにQiitaから検索対象のドキュメントを取得するRakeタスクを作ります

lib/tasks/data.rake:

require "open-uri"
require "json"

namespace :data do
  namespace :load do
    desc "Load data from Qiita"
    task :qiita => :environment do
      tag = "groonga"
      url = "https://qiita.com/api/v2/items?page=1&per_page=100&query=tag:#{tag}"
      open(url) do |entries_json|
        entries = JSON.parse(entries_json.read)
        entries.each do |entry|
          Document.create(title:   entry["title"],
                          content: entry["body"])
        end
      end
    end
  end
end

実行して検索対象のドキュメントを作成します。

% bin/rake data:load:qiita

http://localhost:3000/documentsにアクセスし、フォームに「オブジェクト」と日本語のクエリーを入力します。元のドキュメントは100件あり、「オブジェクト」で絞り込んで16件になっています。日本語で全文検索できていますね。

「オブジェクト」で検索

次のようにOR検索もできます。「オブジェクト」単体で検索したときの16件よりも件数が増えているのでORが効いていることがわかります。

「オブジェクト OR API」で検索

まとめ

MySQLとMroonga(むるんが)を使ってRuby on Railsアプリケーションで日本語全文検索機能を実現する方法を説明しました。

ポイントは次の通りです。

  • create_table(options: "ENGINE=Mroonga")

  • add_index(type: :fulltext)

  • where("MATCH(content) AGAINST(? IN BOOLEAN MODE)", "*D+ #{query}")

開発時・運用時のことを考えてMySQLベースの日本語全文検索機能の実現を検討してみてはいかがでしょうか。

おしらせ

今月の29日(11月29日)にMroongaのイベントがあります。Mroongaに興味がでてきた方は↓のイベントページからお申し込みください。発表内容から有益な情報を得られますし、開発者に直接質問することもできます。

Groonga Meatup 2015 - Groonga | Doorkeeper

関連:Ruby on RailsでPostgreSQLとPGroongaを使って日本語全文検索を実現する方法