Rails×PostgreSQLでpg_bigmを試すまでにやったこと
背景
Rails×PostgreSQLのアプリケーションの検索機能のパフォーマンス改善をpg_bigmを使って行いました。
pg_bigmそのものの導入方法や検証の記事はいくつかありますが、アプリケーションへの導入方法について記載されているものはあまりなかったため本記事に残します。
対象読者
Ruby/Rails、PostgreSQLの構成でpg_bigmを使って検索パフォーマンス改善を検討している方
pg_bigmとは
PostgreSQL上で全文検索を行うためのモジュールで、2gramを使用したインデックスを提供し、全文検索を簡単に実装できます。
単語ごとの境界が曖昧な日本語での全文検索は2gramが主流だと言われているので、国内向けプロダクトでPostgreSQLを使っている場合は候補に上がってくるかと思います。
まずはpg_bigmの検証から
検証準備
ローカルで検証を行った方法を記載しておきます。
下記リンクから、お使いの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
インストールまで完了したら、拡張機能の有効化を行います。
下記のマイグレーションを実行します。
class AddColumnForFullTextSearchToBooks < ActiveRecord::Migration[7.0]
def change
# 拡張機能の有効化
enable_extension 'pg_bigm'
add_column :books, :text_for_bigm, :text
# 検索用カラムにpg_bigmのインデックスを作成
add_index :books, :text_for_bigm, opclass: :gin_bigm_ops, using: :gin
end
end
これだけで、books.text_for_bigmに対するpg_bigmによる全文検索が可能になります。
※ 既存のカラムにインデックスを貼る場合は、concurrentlyオプションを使用してください。
検証
下記3点について懸念があったため、それぞれ検証を実施しました。
- 検索パフォーマンスが改善するか
- データ更新に遅延が発生しないか
- インデックスサイズがDB容量を超えないか
本記事では検索パフォーマンスにのみ触れます。
下記の環境で、複数のケースで検索のヒット数と検索にかかった時間を記録しています。
- 環境:
- M2 MacBook Pro, メモリ16GB, Ventura13.2 10コア
- 検証方法:
- データ投入後はVACUUM ANALYZEを実行して統計情報を取り直し
- 複数のケースで検索を実行
- 各ケースで10回の平均実行時間と検索にヒットした件数を記録
- 使用データ:
- ランダムな文字列をランダム個連結してテキストデータを作成
- レコード数 16,500件(インデックス作成にかかった時間: 159.4s)
合計 | 1レコード平均 (min ~ max) | |
---|---|---|
バイト数 | 758636060 | 45977(19 ~ 92666) |
文字数 | 305542320 | 18517(7 ~ 38037) |
検証結果
ケース① 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 |
ケース② ヒット数が少ない検索
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 |
検証をしてみて分かったこと
-
改めて本番データを使った検証が必要ですが、大幅な改善が見込まれます。 -
Ngramでは、性質上ノイズが問題になることがありますが、上述したRecheckによりこの適合率が実現されている様です。
(pg_bigmのオプションで、Recheckをスキップすることもできますが、検索結果の正確さは実用不可なレベルでした。) -
理解する上で、PostgreSQLの基礎知識も必要ですが、こちらもドキュメントが読みやすかったです。 -
そのため、EXPLAINを使用して実行計画を見ながら複合インデックスやヒント句を使用して、ある程度クエリを安定させる必要性が出てくるケースがありそうです。
Railsアプリケーションへの導入
導入の流れ
今回は既存プロジェクトの検索機能を下記のステップで修正し導入を進めました。
- 前述したセットアップ用のマイグレーションで、拡張機能、カラム、インデックスを追加
- 検索用のテキスト保存時に正規化を実施
- 検索処理時、検索ワードにも同様の正規化を実施
- リリース時、元々の検索用カラムの内容を①で追加したカラムにコピーする補正を実施
導入時に気をつけたこと
インデックスを空のカラムに貼ってからデータ投入を行った
一般的には、データが入ったカラムにインデックスを貼る方がインデックスにかかる時間は短く済みますが、検証の結果からマイグレーションの際のCREATE INDEXが数時間にわたって実行されることが予測できたので、別カラムを定義する方法をとっています。
インデックスを先に貼っておけば、データ投入時のインデックス作成は非同期で実行されるため、アプリケーションへの影響が少ないです。
正規化
pg_bigmが対応しているのはLIKE検索のみなので、ILIKE句が使えません。そのため保存と検索処理時にアプリケーション側でダウンケース処理を含む正規化を行っています。
検索処理には加えてlikequeryというpg_bigm内に用意された関数を使って、検索文字列内の特殊文字をエスケープさせるようにしました。
導入結果
検索APIのレスポンスタイムが80%減になり、UXの改善を達成することができました。
ただし、導入後からインデックス作成によるDBへの負荷が確認されており、更新頻度やテキストサイズなどの相性はありそうです。
最後に
pg_bigmを導入して簡単にRailsアプリケーションの全文検索のパフォーマンスアップを行うことができました。
導入コストに見合わないくらい検索パフォーマンスが上がると思うので、全文検索のパフォーマンスにこまっている方にはぜひおすすめしたいと思います。
Discussion