C Sharpens you up

http://qiita.com/yuba に移しつつあります

Misskeyのノート検索をpg_bigmで爆速化する

Misskeyのノート検索は、Misskey単体だとPostgreSQL上でLIKE演算子による全文部分一致検索という大変重たい方式になっており、ある程度の大規模サーバーになるとタイムアウトして動かなくなります。そこで高速全文検索のために外部サービスとして昔はelasticsearch、最近*1はmeilisearchといったインデックスサービスを別立てして利用できるようになっています。

最近ですと、インデックスサービスを別に立てるのはやはり運用上も大変ということで、別サービスを立てるのではなくpgroongaをPostgreSQLに組み込んで全文検索させるという手法も広がってきています。ただpgroongaはpgroongaで、運用的には楽でも導入が大変。なにしろ独自定義のSQL演算子を使って全文検索をリクエストしないといけないのでMisskey本体のプログラムを改変する必要があります。そしてその改変版は、pgroongaの入っていないPostgreSQLでは動きません。

この痛しかゆしな状況、解消できるソフトウェアがあります。PostgreSQL用の全文検索拡張としてはわりと古株、NTT DATA生まれのOSSpg_bigmというのがそれです。

  • 日本語にも対応している(PostgreSQLに標準添付されている全文検索拡張のpg_trgmは日本語に非対応なのです)
  • LIKE '%検索ワード%' 式で使える。つまりMisskey側を専用版に改変する必要がない

この2条件を満たしているので導入も運用も比較的楽だってわけです。私の運営しているサーバーの https://reax.work/ 「リアクションするだけのお仕事」ではこのpg_bigmを組み込んで高速検索を実現できました。そこで、導入方法についてまとめておこうと思います。

全体像

  1. Misskey本体の微修正
  2. PostgreSQLへのpg_bigm導入
  3. pg_bigm有効化と、インデックス作成

1. Misskey本体の微修正

Misskey本体の専用版化なしで、という風に冒頭で書いたわけですが、実は素のMisskey 2024.11.0 ですとpg_bigmは使えませんでした。それは、このバージョンまではLIKE演算子と言っても厳密には大小文字を同一視するILIKE演算子を使っており、pg_bigmはILIKE演算子では動かないからです。

ただ、LIKE演算子でも大小文字を同一視した全文検索は書けますよね。こう書くだけです。

   LOWER(note.text) LIKE '%小文字化した検索ワード%'

そして、ちょっと意外なことになのですがこのように書き換えた場合、素のPostgreSQLでも検索パフォーマンスは条件次第だがやや向上するのだそうです。SO諸賢いわく。 performance - LOWER LIKE vs iLIKE - Stack Overflow

というわけで、pg_bigmを使いたい人にもそうでない人にも損じゃないこの変更、Misskey本家へのプルリクにしておりまして、本日ありがたくもマージしていただきました。 https://github.com/misskey-dev/misskey/pull/15205

そういうわけで2024.11.0の次のバージョンのMisskeyからですと無改変でpg_bigmが使えるというわけになるのですが、そのリリースを待てないという方ですとか、あとはMisskeyフォークを使っているのでいつ取り込まれるかわからないという方のために取り込み用のブランチを用意いたしました。

pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように by yuba · Pull Request #1 · yuba/misskey · GitHub

cherry-pickしていただいてもいいですし、あと分岐元を相当古く(13.12.0)しておいたので、大方のフォークでしたら単にmergeするだけでこの変更だけ取り込めます。

$ git remote add yuba-misskey https://github.com/yuba/misskey.git
$ git fetch yuba-misskey
$ git merge yuba-misskey/feature/like-lower

あとは、2024.11.0にこの修正だけ入れたバージョンのDockerイメージも公開しております。

https://hub.docker.com/repository/docker/yuba/misskey/tags/2024.11.0-like-lower/sha256-e4f03b1e5c4934f1e0f8a3c3312a64613c98ea2bad4c6da9dda38b0933c49adc

2. PostgreSQLへのpg_bigm導入

まず、pg_bigmはクラウドDBの二大巨頭、Amazon AuroraGoogle Cloud SQLでは最初からインストール済みです。ですのでこのステップはいきなりスキップです。うれしいですね。

さてオンプレ派。拡張は、PostgreSQLのextensionディレクトリとlibディレクトリに所定のファイルを置き、postgresql.confに shared_preload_libraries='pg_bigm' と書き込むことでインストールできます。さてその所定のファイルを用意する方法なのですが、pg_bigm本家では2通りの方法を用意しています。

  • tar.gzを展開してmake && make install
  • rpmパッケージ

前者は「所定の位置」を自力で検知して成果物の配置までしてくれます。具体的には、pg_configコマンドにPATHが通っていればそれを呼び出すことで、通っていなければPG_CONFIG環境変数でpg_configコマンドのフルパスを教えてもらうことでpg_config経由でディレクトリ構成を勝手に取得してくれるという仕組み。

DockerfileでPG15向けに書くとこんな感じになります。

FROM postgres:15

RUN apt update
RUN apt install -y curl make gcc postgresql-server-dev-15

RUN curl https://github.com/pgbigm/pg_bigm/archive/refs/tags/v1.2-20240606.tar.gz -L -o pg_bigm.tar.gz  # -L は301リダイレクト対応の指示
RUN tar zxf pg_bigm.tar.gz
RUN cd pg_bigm-1.2-20240606 && make USE_PGXS=1 && make USE_PGXS=1 install

RUN echo shared_preload_libraries='pg_bigm' >> /etc/postgresql/postgresql.conf

rpmパッケージを使った場合はこの配置先自動検知まではやってくれず、とりあえずで /usr/pgsql-15/ なるディレクトリに置かれてしまいます。そこから先は自力でコピーすることになります。これもDockerfileで書くとこんな感じに。

FROM postgres:15 AS pg_bigm

RUN apt update
RUN apt install -y curl alien

RUN curl https://github.com/pgbigm/pg_bigm/releases/download/v1.2-20240606/pg_bigm-1.2.20240606-1.pg15.el9.x86_64.rpm -L -o pg_bigm.rpm
RUN alien -i pg_bigm.rpm


FROM postgres:15

COPY --from=pg_bigm /usr/pgsql-15/share/extension/* /usr/share/postgresql/15/extension/
COPY --from=pg_bigm /usr/pgsql-15/lib/* /usr/lib/postgresql/15/lib

RUN echo shared_preload_libraries='pg_bigm' >> /etc/postgresql/postgresql.conf

docker使わずPostgreSQL動かしている皆様も、これらのコマンド列見ていただければだいたいインストールに必要な操作はわかるのではと思います。インストールが終わったらPostgreSQLは再起動してください。ここでどうしてもサービス中断が発生しますね。バックアップを取ることも含めてメンテナンス計画を立ててやっていきましょう。

3. pg_bigm有効化と、インデックス作成

CREATE EXTENSION pg_bigm SCHEMA public;

コマンドでpg_bigmは有効化できます。ここで、CREATE EXTENSIONコマンドはデータベースのスキーマ単位を対象にしています。SCHEMA public と指定したとおりカレントデータベースのpublicスキーマに魔法がかかりますので、Misskeyの使うデータベースをカレントデータベースにした状態でこのコマンドを打ってください。

そしてここからがキモのインデックスの作成です。

noteテーブルのtextカラムを検索対象にしたいのですが、note.text に対してインデックスを張るんではいけません。LIKEで大小文字同一視検索する話のところで出てきましたが、LIKEで検索する対象は LOWER(note.text) なのです。だからLOWER(note.text)に対してインデックスを張ります。

CREATE INDEX CONCURRENTLY note_lower_text_bigm ON note USING GIN(LOWER(text) gin_bigm_ops);

インデックスを作成しろという命令なのですが、ちょっと解説しておきますと、

  • CONCURRENTLY ⋯ テーブルロックをかけるなという意味。インデックス作成には長時間かかる(700万ノートで4分強かかりました)ので、何も指定しないとテーブルロックがかかりこの間Misskeyサービスはほぼ無応答になってしまいます。それを避けるための指定です。当然、インデックス作成にさらに時間がかかるというデメリットもあるはずなのですが、noteテーブルの行の大部分は更新のかからない古い行ですので実際には体感できるほどの時間差は付きませんでした。
  • USING GIN ⋯ 浅い理解しかしていないのですが、転置インデックスを実装するためのインターフェースってことになるんですかね、GIN。インデックス方式としてGINを使え、それに実際に使わせる実装(「演算子クラス」と呼ぶよう)にはgin_bigm_opsを使えと指示しています。gin_bigm_opsは、CREATE EXTENSION命令によって組み込まれたもの。

実際に動くところを見てみましょう

ここまででpg_bigmによる高速検索はもう動く状態になっています。実際に動くところを見てみましょう。

youtu.be

この通り、検索しはじめも追加ロードもほとんど待たされずに出てくるようになっています。

ディスク容量ですが、導入前はDB全体で7.2GBだったのが導入後7.3GBと微増にとどまっています。メモリ使用は導入前後とも4GB強で目に見える変化はありませんでした。

まとめ

pg_bigmは導入も運用も楽でサーバーリソースへの負荷も軽いという、どなたにもおすすめしやすい全文検索ソフトウェアです。meilisearchがいいかpgroongaがいいかというMisskey鯖缶界での論争を終わらせてくれる最終兵器と言えるのではないかなと思っております。お試しください。

*1:2023年前半、13.12あたりから