🐘

Rails×PostgreSQLでpg_bigmを試すまでにやったこと

に公開

背景

Rails×PostgreSQLのアプリケーションの検索機能のパフォーマンス改善をpg_bigmを使って行いました。
pg_bigmそのものの導入方法や検証の記事はいくつかありますが、アプリケーションへの導入方法について記載されているものはあまりなかったため本記事に残します。

対象読者

Ruby/Rails、PostgreSQLの構成でpg_bigmを使って検索パフォーマンス改善を検討している方

pg_bigmとは

PostgreSQL上で全文検索を行うためのモジュールで、2gramを使用したインデックスを提供し、全文検索を簡単に実装できます。
単語ごとの境界が曖昧な日本語での全文検索は2gramが主流だと言われているので、国内向けプロダクトでPostgreSQLを使っている場合は候補に上がってくるかと思います。
https://pgbigm.osdn.jp/pg_bigm-1-2.html
https://www.slideshare.net/hadoopxnttdata/pgbigm-39739489

まずはpg_bigmの検証から

検証準備

ローカルで検証を行った方法を記載しておきます。
下記リンクから、お使いのpostgreSQLのバージョンに合うものをダウンロードしてください。
https://github.com/pgbigm/pg_bigm/releases
私は、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オプションを使用してください。
https://dangerous-animal141.hatenablog.com/entry/2020/09/06/121232

検証

下記3点について懸念があったため、それぞれ検証を実施しました。

  1. 検索パフォーマンスが改善するか
  2. データ更新に遅延が発生しないか
  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

検証をしてみて分かったこと

  • 検索速度の大幅な改善が見込まれる

    インデックスが使われる + 再検索のコストが低ければ、ほとんどの場合で通常の検索を上回るパフォーマンスが出せることがわかりました。
    改めて本番データを使った検証が必要ですが、大幅な改善が見込まれます。
  • 検索漏れやノイズは発生しない

    検証結果には記載していませんでしたが、検索のヒット件数とヒットしたアイテムのIDは通常の中間一致検索と全く同じでした。
    Ngramでは、性質上ノイズが問題になることがありますが、上述したRecheckによりこの適合率が実現されている様です。
    (pg_bigmのオプションで、Recheckをスキップすることもできますが、検索結果の正確さは実用不可なレベルでした。)
  • 手軽に導入できる

    エンジニア歴半年の自分でも苦労せずサクッと試せるくらい簡単で、ドキュメントもわかりやすかったです。
    理解する上で、PostgreSQLの基礎知識も必要ですが、こちらもドキュメントが読みやすかったです。
  • インデックスが使用される状態を確保する必要がある

    発行するクエリによっては、pg_bigmのインデックスが正しく使われないケースもあると思います。
    そのため、EXPLAINを使用して実行計画を見ながら複合インデックスやヒント句を使用して、ある程度クエリを安定させる必要性が出てくるケースがありそうです。

Railsアプリケーションへの導入

導入の流れ

今回は既存プロジェクトの検索機能を下記のステップで修正し導入を進めました。

  1. 前述したセットアップ用のマイグレーションで、拡張機能、カラム、インデックスを追加
  2. 検索用のテキスト保存時に正規化を実施
  3. 検索処理時、検索ワードにも同様の正規化を実施
  4. リリース時、元々の検索用カラムの内容を①で追加したカラムにコピーする補正を実施

導入時に気をつけたこと

インデックスを空のカラムに貼ってからデータ投入を行った

一般的には、データが入ったカラムにインデックスを貼る方がインデックスにかかる時間は短く済みますが、検証の結果からマイグレーションの際のCREATE INDEXが数時間にわたって実行されることが予測できたので、別カラムを定義する方法をとっています。
インデックスを先に貼っておけば、データ投入時のインデックス作成は非同期で実行されるため、アプリケーションへの影響が少ないです。

正規化

pg_bigmが対応しているのはLIKE検索のみなので、ILIKE句が使えません。そのため保存と検索処理時にアプリケーション側でダウンケース処理を含む正規化を行っています。
検索処理には加えてlikequeryというpg_bigm内に用意された関数を使って、検索文字列内の特殊文字をエスケープさせるようにしました。
https://www.alibabacloud.com/help/ja/rds/apsaradb-rds-for-postgresql/use-the-pg-bigm-extension-to-perform-fuzzy-match-based-queries#section-94m-k9h-ujc

導入結果

検索APIのレスポンスタイムが80%減になり、UXの改善を達成することができました。
ただし、導入後からインデックス作成によるDBへの負荷が確認されており、更新頻度やテキストサイズなどの相性はありそうです。

最後に

pg_bigmを導入して簡単にRailsアプリケーションの全文検索のパフォーマンスアップを行うことができました。
導入コストに見合わないくらい検索パフォーマンスが上がると思うので、全文検索のパフォーマンスにこまっている方にはぜひおすすめしたいと思います。

Discussion