ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja nl en ja ja ja ja ja ja ja ja zh ja ja ja ja ja ja ja ja ja ja en ja en ja en ja en ja en ja ja ja ja en ja en ja ja en ja ja ja ja ja ja eu ja ja ja ja ja ja ja ja ja ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
Zulip & PGroonga Night - PGroonga & Zulip #zpnight - 2017-09-07 - ククログ
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を使えるようにしました。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の最新情報を紹介する予定です。
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
Groongaについて開発者に質問できる「グルカイ!」ほぼ毎週火曜日12:15-12:45 Groongaチャンネルで開催中です。 Groongaについて知りたい人、ぜひご参加ください。
Firefoxで外部アプリケーションを起動するだけのアドオンをGo言語で作る方法と注意点
db tech showcase Tokyo 2017 - MySQL・PostgreSQLだけで作る高速でリッチな全文検索システム #dbts2017
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。