ZulipにPGroongaサポートを実装した須藤です。PyCon JP 2017に参加するためにZulipの開発者の1人であるGregさんが来ていたので、日本PostgreSQLユーザ会(JPUG)さんに主催してもらってZulipとPGroongaのイベント「Zulip & PGroonga Night」を開催しました。
なお、GregさんのPyConJP 2017でのトーク「Clearer Code at Scale: Static Types at Zulip and Dropbox」(動画)はPyConJP 2017のベストトークに選ばれました。すごい!
Zulip & PGroonga NightではPGroongaの紹介とZulipの全文検索インデックスの更新方法の紹介をしました。
関連リンク:
背景
ZulipではPostgreSQL標準のtextsearchを使って全文検索を実現しています。textsearchは言語特化型のインデックスを作るため、同時に複数の言語をサポートすることができません。また、日本語を含むアジア圏の言語のサポートが不十分なため、日本語を全文検索できないという問題もあります。
そこで、私はZulipでPGroongaを使えるようにしました。PGroongaは言語特化型のインデックスも言語非依存のインデックスも作れます。言語非依存のインデックスを作れば同時に日本語も英語もいい感じに全文検索できます。
クリアコードがZulipを選んだ理由
私がZulipにPGroongaサポートパッチを送ったのは自分たちが必要だからです。クリアコードではチャットツールとしてZulipを使っています。その前はSkypeを使っていました。お客さんとの連絡でSkypeを使う必要があったため、その流れでなんとなく使っていました。Skypeはあまり活用していませんでした。
クリアコードはフリーソフトウェアを推進したい会社なのでフリーソフトウェアではないSkypeを使っていることをどうにかしたいと考えていました。そこで、いくつかフリーソフトウェアなチャットツールを検討しました。そのうちの1つがZulipでした。Zulipのネックは日本語全文検索できないことでした。ネックがあるので選択肢から外すという考え方もあると思いますが、私たちは、自分たちで日本語全文検索できるようにして使うことにしました。フリーソフトウェアのよいところは自分たちで改良できることだからです。
Zulipの全文検索インデックスの更新方法
Zulipは書き込み時のレイテンシーを小さくしておくために工夫をしています。チャットアプリケーションでは書き込みがすぐに終わることはよい使い勝手に直結するからです。
書き込み時はデータをPostgreSQLに書き込むだけで、全文検索インデックスの更新は別途バックグラウンドで実行します。このためにトリガーとNOTIFY
とLISTEN
を使っています。
具体的な実装を簡単に紹介します。興味のある人はZulipのコードを見てください。既存のコードから学習することができることもフリーソフトウェアのよいところです。
zerver_message
がメッセージ(チャットの書き込み)を保存しているテーブルです。この中に全文検索対象のデータを入れるカラムを定義します。メッセージのテキストそのものとは別に定義することがポイントです。search_tsvector
がそのカラムです。(PGroongaを使うときの実装ではなくtextsearchを使うときの実装です。)
CREATE TABLE zerver_message (
rendered_content text,
-- ... ↓Column for full text search
search_tsvector tsvector
);
この全文検索対象のデータを入れるカラムには全文検索用のインデックスを作ります。(この例ではPGroongaのインデックスではなくtextsearchのインデックスを作っています。)
CREATE INDEX zerver_message_search_tsvector
ON zerver_message
USING gin (search_tsvector);
このメッセージ用のテーブルにトリガーを設定します。このトリガーはメッセージが追加・更新されたときに「更新ログ」テーブル(fts_update_log
テーブル)にメッセージのIDを追加します。
-- Execute append_to_fts_update_log() on change
CREATE TRIGGER
zerver_message_update_search_tsvector_async
BEFORE INSERT OR UPDATE OF rendered_content
ON zerver_message
FOR EACH ROW
EXECUTE PROCEDURE append_to_fts_update_log();
メッセージのIDを追加する関数の実装は次のようになります
-- Insert ID to fts_update_log table
CREATE FUNCTION append_to_fts_update_log()
RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO fts_update_log (message_id)
VALUES (NEW.id);
RETURN NEW;
END
$$;
「更新ログ」テーブルの定義は次の通りです。全文検索インデックスを更新するべきメッセージのIDを入れているだけです。
-- Keep ID to be updated
CREATE TABLE fts_update_log (
id SERIAL PRIMARY KEY,
message_id INTEGER NOT NULL
);
これで後から全文検索インデックスを更新するための情報を保存する仕組みができました。通常通りメッセージテーブルを操作するだけで実現できていることがポイントです。こうすることでアプリケーション側をシンプルにしておけます。残りの処理は、後から全文検索インデックスを更新する、です。
この処理のためにNOTIFY
とLISTEN
を使います。NOTIFY
はLISTEN
している接続に通知する仕組みです。LISTEN
している接続はNOTIFY
されるまでブロックします。NOTIFY
とLISTEN
を組み合わせることで、ポーリングしなくてもイベントが発生したことに気づくことができます。
今回のケースでは「更新ログが増えた」というイベントに気づきたいです。このイベントが来たら全文検索インデックスを更新したいからです。
そのために、「更新ログ」テーブルにトリガーを追加します。「更新ログ」テーブルにレコードが追加されたらNOTIFY
するトリガーです。
-- Execute do_notify_fts_update_log() on INSERT
CREATE TRIGGER fts_update_log_notify
AFTER INSERT ON fts_update_log
FOR EACH STATEMENT
EXECUTE PROCEDURE
do_notify_fts_update_log();
NOTIFY
する関数の実装は次の通りです。この関数を実行すると、fts_update_log
というイベントをLISTEN
している接続のブロックが解除されます。
-- NOTIFY to fts_update_log channel!
CREATE FUNCTION do_notify_fts_update_log()
RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
NOTIFY fts_update_log;
RETURN NEW;
END
$$;
全文検索のインデックスを更新するSQLはPythonから発行しています。全文検索のインデックスの更新処理は必要なときだけ(更新ログがあるときだけ)実行したいです。必要がないときも更新処理を実行し続けるとムダにCPUを使ってしまうからです。
必要なときだけ処理を実行するために、LISTEN
でブロックします。ブロックが解除されたら(NOTIFY
されたら)必ず更新ログがあるので、処理を実行します。↓には入っていませんが、処理が終わったら次のNOTIFY
があるまでまたブロックする実装になっています。こうすることで必要なときだけ処理を実行できるためムダにCPUを使わずにすみます。
cursor.execute("LISTEN ftp_update_log") # Wait
cursor.execute("SELECT id, message_id FROM fts_update_log")
ids = []
for (id, message_id) in cursor.fetchall():
cursor.execute("UPDATE zerver_message SET search_tsvector = "
"to_tsvector('zulip.english_us_search', "
"rendered_content) "
"WHERE id = %s", (message_id,))
ids.append(id)
cursor.execute("DELETE FROM fts_update_log WHERE id = ANY(%s)",
(ids,))
このような複数プロセスでの待ち合わせを実現するためにRDBMSとは別の仕組みを使うことも多いでしょう。たとえば、RedisのPub/Subを使えるでしょう。別の仕組みを使うと運用が面倒になります。PostgreSQLにはNOTIFY
/LISTEN
があるので、PostgreSQLを使っていて待ち合わせを実現しなければいけないときはNOTIFY
/LISTEN
を使うことを検討してみてください。
まとめ
ZulipとPGroongaのイベントでZulipとPGroongaの情報を紹介しました。クリアコードはZulipを使っていて、今ではなくてはならないツールになっています。ぜひみなさんもZulipを使ってみてください。
Zulipは基本的に自分たちで運用しますが、運用を任せる選択肢もあります。Zulipの開発チームがクラウドサービスでの提供を進めているのです。オープンソースコミュニティは無料で使えるそうです。興味のある人はzulipchat.comを確認してください。
PGroongaが気になる人は、11月3日開催のPostgreSQL Conference Japan 2017に来てください。日本PostgreSQLユーザ会(JPUG)が主催のPostgreSQLのカンファレンスです。ここでPGroongaの最新情報を紹介する予定です。