[Groonga] MySQLとPostgreSQLと日本語全文検索3:MroongaとPGroongaの導入方法例 mypgft¶ ↑
2016年9月29日(肉の日!)に「MySQLとPostgreSQLと日本語全文検索3}[https://groonga.doorkeeper.jp/events/50541]」というイベントを開催しました。その名の通りMySQLとPostgreSQLでの日本語全文検索についての話題を扱うイベントです。今回も{DMM.comラボさんに会場を提供してもらいました。
2月9日に開催した1回目のイベントでは{Mroonga}[http://mroonga.org/ja/]・{PGroonga}[https://pgroonga.github.io/ja/]については次の2つのことについて紹介しました。
-
Mroonga・PGroongaが速いということ
-
Mroonga・PGroongaの使い方
6月9日に開催した2回目のイベントではMroonga・PGroongaについては次の2つのことについて紹介しました。
-
Mroonga・PGroongaのオススメの使い方
-
レプリケーションまわり
今回はMroonga・PGroongaについては次のことについて紹介しました。
-
{Redmine}[http://www.redmine.org/]・{Zulip}[https://zulip.org/]にMroonga・PGroongaを導入する方法
関連リンク:
Redmineへの導入方法¶ ↑
RedmineへのMroonga・PGroongaの導入方法を説明します。RedmineはRuby on Railsを利用しているのでRuby on Railsを使っているアプリケーションに導入する例ということになります。
redmine_full_text_searchプラグインを使うとRedmineでMroongaまたはPGroongaを使って全文検索できるようになります。
このプラグインを使うとRedmineの全文検索が高速になります。たとえば、クリアコードで使っているRedmineには3000件くらいのチケットがありますが、その環境では次のように高速になりました。
プラグイン | 時間 |
---|---|
なし | 467ms |
あり | 93ms |
200万件のチケットがある環境でも約380msで検索できているという報告もあります。
200万チケット@MySQLでやってみたよ。検索時間は約380ms。 #Redmine の未来が広がって嬉しいな。ありがたいな。/Redmineで高速に全文検索する方法 - ククログ(2016-04-11) https://t.co/s7FA4gSThu @_clear_code
— Kuniharu AKAHANE (@akahane92) 2016年5月21日
Mroongaを導入する方法¶ ↑
Mroongaはトランザクションに対応していないのでトランザクションが必須のRedmineに組み込む場合はひと工夫必要になります。単純に、ALTER TABLE table ENGINE=Mroonga ADD FULLTEXT INDEX (column)
とするわけにはいきません。
ではどうするかというと別途全文検索用のテーブルを作成して元のテーブルとはJOIN
できるようにします。(他にもレプリケーションしてレプリケーション先をMroongaにするという2回目のイベントで紹介した方法もありますが、プラグインでやるには大掛かりなのでこの方法を使っています。)
マイグレーションファイルでいうと次のようにします。ここではissues
テーブル用の全文検索用のテーブルを作成しています。
def up create_table(:fts_issues, # 全文検索用テーブル作成 id: false, # idは有効・無効どっちでも可 options: "ENGINE=Mroonga") do |t| t.belongs_to :issue, index: true, null: false t.string :subject, default: "", null: false t.text :description, limit: 65535, null: false t.index [:subject, :description], type: "fulltext" end end
全文検索用のテーブルには元のデータをコピーする必要があります。マイグレーション時には既存のデータを一気にコピーします。そのため、本当のマイグレーションの内容は次のようになります。データコピー後にインデックスを追加するようにしているのはそっちの方が速いからです。
def up create_table(:fts_issues, # 全文検索用テーブル作成 id: false, # idは有効・無効どっちでも可 options: "ENGINE=Mroonga") do |t| t.belongs_to :issue, index: true, null: false t.string :subject, default: "", null: false t.text :description, limit: 65535, null: false end execute("INSERT INTO " + # データをコピー "fts_issues(issue_id, subject, description) " + "SELECT id, subject, description FROM issues;") add_index(:fts_issues, [:subject, :description], type: "fulltext") # 静的インデックス構築(速い) end
このテーブルのモデルは次のようになります。
class FtsIssue < ActiveRecord::Base # 実際はissue_idカラムは主キーではない。 # 主キーなしのテーブルなので # Active Recordをごまかしているだけ。 self.primary_key = :issue_id belongs_to :issue end
Mroonga導入後に更新されたデータはアプリケーション(Redmine)側でデータをコピーします。Active Recordのafter_save
フックを利用します。Mroongaがトランザクションをサポートしていないため、ロールバックのタイミングによってはデータに不整合が発生することがありますが、再度保存すれば復旧できることとそれほどロールバックは発生しないため、実運用時には問題になることはないでしょう。
class Issue # この後にロールバックされることがあるのでカンペキではない # 再度同じチケットを更新するかデータを入れ直せば直る after_save do |record| fts_record = FtsIssue.find_or_initialize_by(issue_id: record.id) fts_record.subject = record.subject fts_record.description = record.description fts_record.save! end end
全文検索時は全文検索用のテーブルをJOIN
してMATCH AGAINST
を使います。
issue. joins(:fts_issue). where(["MATCH(fts_issues.subject, " + "fts_issues.description) " + "AGAINST (? IN BOOLEAN MODE)", # ↓デフォルトANDで全文検索 "*D+ #{keywords.join(', ')}"])
この説明はわかりやすさのために実際の実装を単純化しています。詳細が知りたい方は実装を確認してください。
PGroongaを導入する方法¶ ↑
PGroongaはトランザクションに対応しているので別途全文検索用のテーブルを作成する必要はありません。既存のテーブルに全文検索用のインデックスを作成します。
マイグレーションファイルでいうと次のようにします。ここではissues
テーブルに全文検索用のインデックスを作成しています。enable_extension("pgroonga")
はPGroongaを使えるようにするためのSQLです。
def up enable_extension("pgroonga") add_index(:issues, [:id, :subject, :description], using: "pgroonga") end
あとは検索時に全文検索条件をつけるだけです。
issue. # 検索対象のカラムごとに # クエリーを指定 where(["subject @@ ? OR " + "description @@ ?", keywords.join(", "), keywords.join(", ")])
この説明もわかりやすさのために実際の実装を単純化しています。詳細が知りたい方は実装を確認してください。
Zulipへの導入方法¶ ↑
ZulipへのPGroongaの導入方法を説明します。ZulipはPostgreSQLを使っているので、導入するのはPGroongaだけです。ZulipはDjangoを使っているのでDjangoを使っているアプリケーションに導入する例ということになります。
Zulipはチャットツールです。チャットツールなので小さなテキストの書き込みが頻繁に発生する傾向があります。各書き込みは十分速く完了する必要があります。書き込みが遅いとユーザーの不満が溜まりやすいからです。
Zulipは書き込みをできるだけ速くするためにインデックスの更新を遅延させています。インデックスの更新はデータの追加よりも重い処理なので、その処理を後回しにしているということです。(PGroongaは検索だけでなく更新も速いので遅延させずにリアルタイムで更新しても十分速いかもしれません。アプリケーションの要件次第でどのような実装にするか検討する必要があります。)
Zulipは、インデックスの更新を遅延させるため、カラムの値を直接全文検索対象にせずに、別途全文検索用のカラムを用意しています。その全文検索用のカラムの更新を後回しにすることでインデックスの更新を遅延させています。
マイグレーションファイルでいうと次のようにします。最初のALTER ROLE
はPGroongaが提供する@@
という全文検索用のオペレーターの優先順位を調整するためのものです。本質ではないのでここでは気にしなくて構いません。
migrations.RunSQL(""" ALTER ROLE zulip SET search_path TO zulip,public,pgroonga,pg_catalog; ALTER TABLE zerver_message ADD COLUMN search_pgroonga text; UPDATE zerver_message SET search_pgroonga = subject || ' ' || rendered_content; CREATE INDEX pgrn_index ON zerver_message USING pgroonga(search_pg$roonga); """, "...")