Rails×PostgreSQLでpg_bigmを試すまでにやったこと
概要
Rails×PostgreSQLのアプリケーションの検索機能をpg_bigmを使って改善を行いました。
アプリケーション側の実装について記載されている記事があまりなかったので、導入と検証を行なった際の記録を記事に残しています。
※ 不適切な箇所があればご指摘いただけますと大変ありがたいです🙇♂️
対象読者
Ruby/Rails、PostgreSQLの構成でpg_bigmを使って検索パフォーマンス改善を検討している方
pg_bigmとは
PostgreSQL上で全文検索を行うためのモジュールで、2gramを使用したインデックスを提供し、全文検索を簡単に実装できます。
単語ごとの境界が曖昧な日本語での全文検索は2gramが主流だと言われているので、国内向けプロダクトでPostgreSQLを使っている場合は候補に上がってくるかと思います。
ローカルでの検証準備
ローカルで検証を行った方法を記載しておきます。
下記リンクから、お使いのpostgreSQLのバージョンに合うものをダウンロードしてください。
私は、Ruby3.1.3/Rails7.0.4 PostgreSQL 13.9/pg_bigm1.2で検証を行いました。
# 解凍
tar zxf pg_bigm-1.2-20200228.tar.gz
cd pg_bigm-1.2-20200228
# pg_configのパスを取得
which pg_config
=> /opt/homebrew/bin/pg_config
# 上記結果をPG_CONFIGに指定して、ビルド&インストール
make USE_PGXS=1 PG_CONFIG=/opt/homebrew/bin/pg_config
make USE_PGXS=1 PG_CONFIG=/opt/homebrew/bin/pg_config install
インストールまで完了したら、拡張機能の有効化を行います。
DBで直接実行する場合は、DBに接続してCREATE EXTENSION pg_bigm;
を実行するだけなのですが、Railsアプリケーションからは、migrationファイルに以下のように記述して、rails db:migrate
を実行します。
class AddColumnForFullTextSearchToBooks < ActiveRecord::Migration[7.0]
def change
# 拡張機能の有効化
enable_extension 'pg_bigm'
# 検索用カラムと、pg_bigmのインデックスを作成
add_column :books, :text_for_bigm, :text
add_index :books, :text_for_bigm, opclass: :gin_bigm_ops, using: :gin
end
end
これだけで、books.text_for_bigmに対するpg_bigmによる全文検索が可能になります。
既存のカラムにインデックスを貼るでも問題ありませんが、pg_bigm用のインデックス作成にかなり時間がかかるため、concurrentlyオプションを使うようにしないと、トランザクションが切れずテーブルロックが発生して障害発生の原因となります。(体験談あり。別記事で反省文を書く予定です😢)
検証
諸事情があり、ローカルで検証したかったため、テストデータのサイズは小さめですが、下記の方法でで行いました。
検証方法
- 環境:
- M2 MacBook Pro, メモリ16GB, Ventura13.2 コア数10
- 使用データ:
- ランダムな文字列をランダム個連結してテキストデータを作成
- レコード数 16500 件
- インデックス作成にかかった時間: 159.4395s
- 10回の検索結果の平均実行時間を記録
- ※ データ投入後はVACUUM ANALYZEを実行しておくこと
| | 合計 | 1レコード平均 (min ~ max)|
| ---- | ---- | ---- |
| バイト数 | 758636060 | 45977(19 ~ 92666) |
| 文字数 | 305542320 | 18517(7 ~ 38037) |
- 検証内容
①2文字、②1件のみヒットする検索条件、③8割以上のレコードにヒットする検索条件の、3つの条件で検索を行う。
検証結果
①2文字検索
表の通り1/100以下の実行時間となりました。
これは2gramの1トークンの最大文字数が2文字なので、Recheck処理が不要なためです。
Recheckとは
pg_bigmでは検索ワードを2文字ずつのトークンに分けてそれぞれのBitmapIndexを突合する形で絞り込みを行う。
その結果を、トークン分割する前の検索ワードで再検索(Recheck)することで検索もれ、ノイズを防いでいる。
※ RecheckはSeqスキャンゆえに、再検索の対象レコード数が多いとパフォーマンス改善につながらないケースもあるため注意
ヒット件数 | 実行時間 | |
---|---|---|
通常のLIKE '%%' | 13635/16500 | 1904.223 ms |
pg_bigm | 13635/16500 | 6.532 ms |
②1件のみヒットするように検索
Seqスキャンと、Indexスキャンの差が出てこちらもかなり短縮されました。検証データが小さいのもありますが、十分パフォーマンス改善に刺さることがわかります。
ヒット件数 | 実行時間 | |
---|---|---|
通常のLIKE '%%' | 1/16500 | 3012.187 ms |
pg_bigm | 1/16500 | 2.136ms |
③ヒット数が多くなるよう検索
これはpostgreSQLのオプティマイザの都合上、インデックスが使用されないケースです。
通常の検索同様、pg_bigm導入後もインデックスが使われるようなクエリを組む必要があります。
ヒット件数 | 実行時間 | |
---|---|---|
通常のLIKE '%%' | 14606/16500 | 2115.026 ms |
pg_bigm | 14606/16500 | 1959.660 ms |
検証をしてみて分かったこと(感想含む)
- 導入が本当に簡単
エンジニア歴半年の自分でも苦労せずサクッと試せるくらい簡単で、ドキュメントもわかりやすかったです。
理解する上で、PostgreSQLの基礎知識も必要ですが、こちらもドキュメントが読みやすかったです。 - pg_bigmで検索速度の大幅な改善が見込まれる
インデックスが使われる + 再検索のコストが低ければ、ほとんどの場合で通常の検索を上回るパフォーマンスが出せることがわかりました。
改めて本番データを使った検証が必要ですが、かなりの確率で改善が見込まれることがわかりました。 - 検索漏れは発生しない
記載していませんでしたが、検索へのヒット件数も通常の中間一致検索と全く同じで、取得したIDも全て等しいことが確認できました。
これは上述したRecheckによるもので、この処理の必要性がよくわかります。
(pg_bigmのオプションで、Recheckをスキップすることもできますが、検索結果の正確さは実用不可なレベルでした。) - インデックスが使用される状態を確保する必要がある
発行するクエリによっては、pg_bigmのインデックスが正しく使われないケースもあると思います。
そのため、EXPLAINを使用して実行計画を見ながら複合インデックスやヒント句を使用して、ある程度クエリを安定させる必要性が出てくるケースがありそうです。
Railsアプリケーションに導入する
導入の流れ
環境によりまちまちな部分もあると思いますが、大まかには下記の通りです。
①前述したセットアップ用のマイグレーションで、拡張機能、カラム、インデックスを追加
②検索用のテキスト保存時に、正規化 + ダウンケース処理を実施
③検索時に、検索ワードにも 正規化 + ダウンケース処理を実施
④リリース時に元々の検索用テキストカラムの内容を①で追加したカラムにコピーするデータ補正
導入時の注意点
インデックスを空のカラムに貼ってからデータ投入を行った
一般的には、データが入ったカラムにインデックスを貼る方が処理時間は短く済みますが、CREATE INDEXが数時間にわたって処理されることが予測できたので、その影響を考えて安全策をとっています。
インデックスを先に貼っておけば、データ投入時のインデックス作成は裏でqueueに積まれ、順次処理されていきます。
ダウンケースの必要性
pg_bigmが対応しているのはLIKE検索のみなので、ILIKEが使えません。そのため保存と検索ワードにアプリケーション側で変換処理を挟んでいます。
検索処理には加えてlikequeryというpg_bigmの関数を使って、検索文字列内の特殊文字をエスケープさせるようにしました。
最後に
気にすべきポイントはいくつかあったものの、pg_bigmを使用して簡単にRailsアプリケーションの全文検索のパフォーマンスアップを行うことができました。
導入コストに見合わないくらい検索パフォーマンスが上がると思うので、全文検索のパフォーマンスにこまっている方にはぜひおすすめしたいと思います。
Discussion