検索の「質」を極める:PostgreSQLの全文検索を実戦レベルで使い倒す
データベースエンジニアとして長く現場にいると、必ずと言っていいほど「検索機能」の限界に突き当たります。`LIKE ‘%query%’` で凌いでいた時代は終わり、ユーザーはGoogleのような「関連度順」の結果と「どこにヒットしたか」が一目でわかるスニペットを求めてくる。
PostgreSQLの全文検索機能(Tsearch2)は、この難題に対する非常に強力な回答ですが、多くのエンジニアは「とりあえず動く」状態のまま放置しがちです。今日は、`ts_rank` と `ts_headline` を軸に、その裏側にあるアーキテクチャと、パフォーマンスの落とし穴について少し深く掘り下げてみましょう。
—
ts_rank:単なるヒット数ではない、「重み」の演算アルゴリズム
検索結果の適合度を測る `ts_rank`。多くの人はこれを「単に検索語句が含まれている回数」だと思っているかもしれませんが、実はもう少し気の利いたことをしています。
`ts_rank` は `tsvector`(正規化されたトークンのリスト)と `tsquery`(検索クエリ)を受け取り、以下の計算式に基づいた重み付けを行います。
- 頻度(Frequency): 文書内に検索語が何回出現したか。
- 重み(Weights): `tsvector` の各トークンに付与されているラベル(A, B, C, D)。
- ここが盲点です。PostgreSQLでは `setweight` を使って、タイトル(A)と本文(D)に異なる重みを付与できます。検索の精度を上げるなら、検索対象のテーブル設計時に、この重み付けを戦略的に行うのが定石です。
注意すべきパフォーマンストラブル:
`ts_rank` は計算コストがそこそこ重い関数です。大規模なテーブルで `ORDER BY ts_rank(…) DESC` を実行すると、全件のスコアを計算するために一時的なソートが発生し、クエリが破綻します。
もし数百万件規模のデータを扱うなら、あらかじめ `tsvector` をカラムとして保持し、`GIN` インデックスを貼った上で、さらに「頻出するクエリ」に対してはスコアリング済みのマテリアライズド・ビューを用意するような、泥臭い最適化が必要になることもあります。
—
ts_headline:スニペット生成のコストと代償
検索結果に「どこにヒットしたか」を表示する `ts_headline` は、ユーザー体験(UX)を劇的に向上させます。しかし、エンジニアの視点で見ると、これは「非同期的な重い処理」の筆頭候補です。
`ts_headline` は、元のテキスト(あるいは `tsvector` のソース)に対して、再びトークナイズと正規化を行い、検索語句の周囲を切り出します。
- ここが危ない: `ts_headline` は検索結果の「全件」に対して実行してはいけません。
- ユーザーが1ページに表示する件数はせいぜい20〜50件でしょう。それなら問題ありませんが、ページネーションの裏側で全件に対してこの関数を適用し、結果を捨てているような実装を見かけることがあります。
- 必ず `LIMIT` 句で絞り込んだ結果セットに対してのみ、`ts_headline` を適用するようにしてください。
- Tips: パフォーマンスを稼ぎたいなら、`StartSel` や `StopSel` の引数を調整して、タグ付けの負荷を最小化するのも一つの手です。また、テキストが極端に長い場合、`MaxWords` や `MinWords` の指定を厳格にすることで、解析コストを劇的に下げることができます。
—
アーキテクチャの視点:なぜ「インデックス」だけでは足りないのか
エンジニアとして覚えておいてほしいのは、「インデックスは検索対象を見つけるためのものであり、スコアリング(順位付け)のためのものではない」という事実です。
GINインデックスは「どの行に語が含まれているか」のリストを高速に返すためのものですが、`ts_rank` や `ts_headline` は、そのリストを元に「行データそのもの」へアクセスし、さらに追加の計算を要求します。
つまり、検索の最適化は以下のステップで考えるべきです。
1. インデックスによる絞り込み: `WHERE col @@ query` で対象を数千件以下に絞る。
2. スコアリングの限定: 絞り込まれた結果に対してのみ `ts_rank` を計算する。
3. スニペットの遅延生成: 最終的に画面に表示する「表示用レコード」に対してのみ `ts_headline` を適用する。
—
最後に
PostgreSQLの全文検索は、適切に設計すれば商用の検索エンジンにも劣らない性能を発揮します。しかし、それは魔法ではありません。内部で何が起きているのか、メモリとCPUのどこがボトルネックになるのかを想像し、クエリを組み立てる。
この感覚こそが、優れたデータベースエンジニアと、単にクエリを書くプログラマの分かれ道だと私は信じています。
皆さんのシステムで `ts_rank` が悲鳴を上げていないか、一度 `EXPLAIN ANALYZE` で覗いてみてください。意外なところで、コストの無駄遣いが見つかるかもしれませんよ。
コメント