RubyやRailsも使えるPaaSであるHerokuでRroongaを使えるようにしました。これにより、高速な全文検索機能を提供するRubyによるWebアプリケーションをHeroku上で動かすことができるようになりました。
ここでは、HerokuでRroongaを使う方法と、どのように動いているかを簡単に説明します。
サンプルアプリケーション
Heroku上でRroongaを使えることを示すサンプルアプリケーションとして、Rroongaで全文検索できるブログを作成しました。
Railsでscaffoldしたものに、全文検索関連の機能を追加して見た目を整えた1だけの簡単なアプリケーションです。
全文検索機能はページ上部の検索ボックスにキーワードを入力してサブミットすると確認できます。キーワードにマッチするとキーワードがハイライトするようになっていますが、これもRroongaの機能です。
Rroongaを使ったアプリケーションの作り方
それでは、Herokuで動くRroongaを使ったアプリケーションの作り方を説明します。
Heroku用Railsアプリケーションの作成
まず、Railsアプリケーションを作ります。Heroku用のRailsアプリケーションの作り方の詳細はHerokuのドキュメント(英語)を参考にしてください。
% rails new rroonga-blog --database=postgresql --skip-bundle
% cd rroonga-blog
% git init
Gemfileに次の内容を追加します。rails_12factorはHeroku用で、rroongaはRroongaを使うためです。
gem 'rails_12factor', group: :production
gem 'rroonga'
config/database.ymlはproduction
用の設定を次のように変えるだけでよいです。
production:
url: <%= ENV['DATABASE_URL'] %>
Debian GNU/Linuxではデータベース周りの初期設定は次の通りです。
% sudo -H apt-get install -V -y postgresql postgresql-server-dev-all
% sudo -u postgres -H createuser --createdb $USER
% bundle install
% rake db:create
scaffold
でベースの機能を作ります。
% rails generate scaffold post title:string content:text
% rake db:migrate
これでブログができました。
Rroongaの組み込み
それでは、ここにRroongaを組み込んでいきます。
まず、次の内容でconfig/initilizers/groonga.rbを作ります。やっていることはGroongaデータベースの作成またはオープンです。
require 'fileutils'
require 'groonga'
database_path = ENV['GROONGA_DATABASE_PATH'] || 'groonga/database'
if File.exist?(database_path)
Groonga::Database.open(database_path)
else
FileUtils.mkdir_p(File.dirname(database_path))
Groonga::Database.create(path: database_path)
end
Herokuで動くときはGroongaのデータベースのパスは環境変数GROONGA_DATABASE_PATH
で渡ってきます2。しかし、テストのためにローカルで動かすときはこの環境変数が設定されていないため、デフォルト値として'grooonga/detabase'
を使っています。
Groongaのデータベースはリポジトリーに入れる必要はないので無視するようにします。
.gitignore
:
# ...
/groonga/database
config/initializers/
に書いたので、RailsアプリケーションはどこでもGroongaのデータベースにアクセスできるようになりました。
このブログは次のデータをデータベースに格納します。
- title: タイトル
- content: 内容
- created_at: 作成時間
- updated_at: 更新時間
今回はすべてにインデックスを張って検索できるようにします。そのためのGroongaのスキーマをgroonga/init.rb
3で定義します。後述する通り、このGroongaのデータベースは永続的なものではなく、何度でも作り直すものなので、マイグレーションのような仕組みは不要です。
groonga/init.rb
:
require_relative '../config/environment'
# データを保存するテーブルを定義。カラムはPostgreSQLと同じ。
Groonga::Schema.define do |schema|
schema.create_table('Posts',
type: :hash,
key_type: :uint32) do |table|
table.short_text('title')
table.text('content')
table.time('created_at')
table.time('updated_at')
end
end
# 後でここにPostgreSQLのデータをインポートするコードを入れる
# インデックスを定義。通常はこのパラメーターで十分。
Groonga::Schema.define do |schema|
schema.create_table('Terms',
type: :patricia_trie,
key_type: :short_text,
normalizer: 'NormalizerAuto',
default_tokenizer: 'TokenBigram') do |table|
table.index('Posts.title')
table.index('Posts.content')
end
schema.create_table('Times',
type: :patricia_trie,
key_type: :time) do |table|
table.index('Posts.created_at')
table.index('Posts.updated_at')
end
end
Groonga側にテーブルができたので、PostgreSQLにデータを追加・更新・削除するときにGroongaのデータベースの中身も更新するようにします。
まず、Groongaのインデックスを更新するクラスを作ります。
lib/post_indexer.rb
:
class PostIndexer
def initialize
@posts = Groonga['Posts']
end
def add(post)
attributes = post.attributes
id = attributes.delete('id')
@posts.add(id, attributes)
end
def remove(post)
@posts[post.id].delete
end
end
lib/
に置いたのでconfig.autoload_paths
にlib/
を追加します。
config/application.rb
:
# ...
module RroongaBlog
class Application < Rails::Application
# ...
config.autoload_paths += ["#{config.root}/lib"]
end
end
Post
クラスにコールバックを設定します。
app/models/post.rb
:
class Post < ActiveRecord::Base
after_save do |post|
indexer = PostIndexer.new
indexer.add(post)
end
after_destroy do |post|
indexer = PostIndexer.new
indexer.remove(post)
end
end
これで、データが変わるとGroongaのデータベースと同期するようになりました。
それでは全文検索機能を組み込みましょう。query
というパラメーターが指定されたらタイトルと内容を全文検索します。
app/controllers/posts_controller.rb
:
class PostsController < ApplicationController
# ...
def index
query = params[:query]
if query
@posts = search(query)
else
@posts = Post.all
end
end
# ...
private
# ...
def search(query)
# Groongaを使って全文検索
groonga_posts = Groonga['Posts']
matched_groonga_posts = groonga_posts.select do |record|
# titleかcontentにqueryがマッチ、という検索パターンを指定
record.match(query) do |match_target|
match_target.title | match_target.content
end
end
# Groongaのデータベースでは各レコードのキーに
# PostgreSQLのレコードのIDが入っているので、
# それを使って対象レコードを取得。
post_ids = matched_groonga_posts.collect(&:_key)
Post.where(id: post_ids)
end
end
検索フォームを追加します。
app/views/posts/index.html.erb
:
<h1>Listing posts</h1>
<%= form_tag posts_path, method: "get" do %>
<%= search_field_tag :query, params[:query] %>
<%= submit_tag 'Search' %>
<% end %>
<!-- ... -->
http://localhost:3000/postsをWebブラウザーで開いてテストデータを投入し、検索してみてください。AND検索だけではなく、OR検索やNOT検索もできます。もちろん、日本語も使えます。
- AND検索:キーワードをスペースで区切る。
- 「hello」と「world」を両方含んでいたらヒットする例:「hello world」
- OR検索:キーワードを「 OR 」で区切る。
- 「hello」または「world」をどちらか含んでいたらヒットする例:「hello OR world」
- NOT検索:キーワードの前に「-」を付ける。
- 「hello」は含むが「world」を含まなかったらヒットする例:「hello -world」
ローカルで動作することを確認できたのでコミットします。Herokuにデプロイするときはgit push
する必要があるのでコミットしておかないといけません。
% git add .
% git commit --message 'Import'
Herokuで動かす
ローカルで動作することを確認できたので、いよいよHerokuで動かします。
Heroku Toolbeltをインストール済みであるという前提で説明します。
まず、Herokuアプリケーションを作ります。Rroonga用のビルドパックを指定することがポイントです。Rroonga用のビルドパックはGroongaを追加でインストールすること以外はRuby用のビルドパックと同じです。そのため、Rroognaを使っていないHeroku用のRailsアプリケーションと同じように開発できます。
% heroku apps:create --buildpack https://codon-buildpacks.s3.amazonaws.com/buildpacks/groonga/rroonga.tgz
PostgreSQLデータベースの初期設定をします。
% heroku run rake db:migrate
これでHeroku上でブログを使えるようになりました。Webブラウザーで開いて動作を確認してみてください。
% heroku apps:open
HerokuでRroongaを使って全文検索をするアプリケーションができましたね。Herokuでも全文検索したい人は試してみてください。
HerokuでRroongaを動かすということ
ここからはHerokuでRroongaを動かすということは、どのようなメリット、どのような制約があるのかについて説明します。このあたりにあまり興味がなく、単に使えれば十分という人は次の2点だけ覚えておけば十分です。
- マスターデータはどこかに持っておき、
groonga/init.rb
でGroongaデータベースにマスターデータを投入すること4 - 扱えるデータの量は多くても100MB程度
揮発性ローカルストレージのHerokuとローカルストレージに保存するRroonga
Herokuはgit push
したり、dynoが再起動する毎にローカルストレージの内容が消えます。
Rroongaはローカルストレージにデータベースを作成し、そこに対して読み書きします。もちろん、dynoが再起動するとRroongaが作ったデータベースも消えます。相性が悪いですね。
この相性の悪さは毎回Groongaのデータベースを1から作成することで解決します。Immutable InfrastructureとかDisposable Componentsのような考え方です。デプロイする毎にGroongaのデータベースが破棄されることを前提にします。そのため、毎回データベースを1から作成します。そのための仕組みがgroonga/init.rb
です。
Herokuはアプリケーションが動くまで次のような流れになります。
git push heroku master
すると、slugを作成する。- slugを元にdyno(アプリケーション)を起動する。
- なにかあったらslugを元にdynoを再起動する。
ポイントは、1つのslugを使ってN回dynoを起動するということです。
groonga/init.rb
はslugを作るときに動きます。groonga/init.rb
が作ったGroongaのデータベースはslugの中に含まれるため、dynoを起動したときはGroongaのデータベースがセットアップされた状態になります。ただし、dynoを起動した後にマスターデータが更新されないアプリケーションの場合は、という条件がつきます。
簡単に言うと、更新機能がないアプリケーションならgroonga/init.rb
で作ったGroongaデータベースを使い続けられます。例えば、るりまサーチはそのようなタイプのアプリケーションです5。
この記事で作成したブログはこの条件には当てはまりません。ブログに記事を投稿したり、既存の記事を削除したりできるからです。これはマスターデータを変更しているため、slugを作成するときに作ったGroongaのデータベースは古くなっています。
このようなアプリケーションの場合はdynoを起動する毎にGroongaのデータベースを1から作成します。この記事で作成したブログでは次のようになります。
groonga/init.rb
:
# ...
# データを保存するテーブルを定義。カラムはPostgreSQLと同じ。
# ...
# 後でここにPostgreSQLのデータをインポートするコードを入れる
if Post.table_exists?
indexer = PostIndexer.new
Post.all.each do |post|
indexer.add(post)
end
end
# インデックスを定義。通常はこのパラメーターで十分。
# ...
PostgreSQLからデータを持ってきてGroongaのデータベースを更新しているだけです6。
これをdynoが起動するタイミングでも実行します。
Procfile
:
web: ruby groonga/init.rb && bin/rails server -p $PORT -e $RAILS_ENV
dynoが起動するたびに実行するとdynoの起動が非常に遅くならないか心配になると思いますが、次の理由から問題にはならないでしょう。
- Groongaの更新速度は速い
- データ量は多くない
slugの最大サイズは300MB
データ量はどうして多くないのか説明します。
それは、dynoが使えるローカルストレージのサイズにそんなに大きくない上限があるだろうからです。実際に上限がいくつかはわかりませんが、1GB強くらいでしょう。
予想してみましょう。
slugの最大サイズは300MBです。slugはgzで圧縮されています。Rubyのビルドパックでできるファイルがだいたい100MBで、それをtar.gzにすると25MBくらいです。そのため、ここでは1/4くらいに圧縮できると考えます。dynoではslugを展開して利用します。1/4に圧縮されているとすると、展開後は1.2GBになります。そのため、1GB強くらいが上限になっていると考えられます。
しかし、展開後で1.2GBになるdynoは想定外でしょうから、実際に使えるのはもっと少ないと考えるべきです。半分の500MBくらいとしましょう。そのうち、Ruby関連のファイルで100MBくらい使います。残りは400MBです。Groongaのデータベースはインデックスの張り方にもよりますが、少なくとも検索対象のデータ(入力データ)の3倍以上の大きさになります。実際にはいくつかインデックスを張るでしょうから、4倍以上などもっとサイズが増えます。よって、入力データは多くても100MBより少なくしなければいけません。
つまり、それほど大きなデータを扱うことはできないということです。そのため、Groongaのデータベースの作成にかかる時間も短くなり、dynoを起動する毎にGroongaのデータベースを作ることも現実的になります。
HerokuでRroongaを動かすということはなんだったのか
HerokuでRroongaを使うということは、ちょっとした全文検索機能つきWebアプリケーションをRubyで簡単に開発・運用できるということです。大規模システムには向きませんが、手軽にやりたいことを試せます。
まとめ
この記事で説明したことをまとめます。
- HerokuでRroongaを使えるようになった
- Rroonga用のビルドパックを使う
groonga/init.rb
を用意する- あとはRuby用のビルドパックのときと同じ
- Groongaのデータベースは毎回1から作る
- Herokuの特性に合わせた
- Disposable Database
- 全文検索機能つきのちょとしたWebアプリケーションをRubyで簡単に開発・運用できる
- 大規模データは対象外
ビルドパックの作り方も説明しようとしましたが、力尽きました。またの機会に。。。