はじめに — 日本語検索の壁
Webアプリケーションで検索機能を実装するとき、日本語の検索は英語よりも複雑です。英語なら単語の区切りがスペースで明確ですが、日本語には単語の区切りがありません。「東京都渋谷区」という文字列から「渋谷」を検索するには、部分一致検索が必要です。
しかし、単純な LIKE '%渋谷%' クエリはパフォーマンスに問題があります。データ量が増えると、検索に数秒かかることも珍しくありません。
この記事では、PostgreSQL の pg_trgm 拡張を使って、Supabase で日本語の部分一致検索を高速化する方法を解説します。BandBridge(ミュージシャンマッチングサービス)と IdeaSpool(アイデア管理ツール)の開発で実際に適用した経験をもとに、実践的なアプローチを共有します。
pg_trgm とは — トライグラムによる類似度検索
pg_trgm は PostgreSQL の拡張機能で、文字列をトライグラム(3文字の連続した部分文字列)に分解して類似度検索を行います。
例えば、「渋谷」という文字列は以下のトライグラムに分解されます。
" 渋", " 渋谷", "渋谷 "
(先頭と末尾にスペースが付加されます)
この分解によって、LIKE クエリのようなシーケンシャルスキャンではなく、インデックスを使った効率的な検索が可能になります。
なぜ pg_trgm を選んだか
日本語の全文検索には、いくつかの選択肢があります。
| 方法 | メリット | デメリット |
|---|---|---|
LIKE '%keyword%' | 簡単、追加設定不要 | 遅い(シーケンシャルスキャン) |
| pg_trgm + GIN インデックス | 高速、Supabase 標準搭載 | 3文字未満では効果が薄い |
| pgroonga | 日本語に最適化 | Supabase では使えない |
| 外部検索エンジン(Elasticsearch等) | 高機能 | 別サービスの運用が必要 |
Supabase を使っている場合、pg_trgm は追加料金なしで利用できる最も現実的な選択肢です。日本語にも対応しており、3文字以上のキーワードであれば十分な速度が出ます。
BandBridge での実装 — ミュージシャン検索の高速化
BandBridge はミュージシャン同士をマッチングするサービスです。ユーザーはプロフィール(ニックネーム、自己紹介、音楽経歴など)を登録し、他のミュージシャンを検索できます。
2026年1月7日の開発日記には、こう書いています。
pg_trgm インデックス作成(全文検索高速化)
このとき、検索対象となるフィールドに対して GIN インデックスを作成しました。
pg_trgm の有効化
Supabase では、SQL Editor から以下のコマンドで pg_trgm 拡張を有効化できます。
CREATE EXTENSION IF NOT EXISTS pg_trgm;
GIN インデックスの作成
検索対象のカラムに対して、GIN(Generalized Inverted Index)インデックスを作成します。
-- プロフィールテーブルの検索用インデックス
CREATE INDEX idx_profiles_nickname_trgm
ON profiles
USING GIN (nickname gin_trgm_ops);
CREATE INDEX idx_profiles_bio_trgm
ON profiles
USING GIN (bio gin_trgm_ops);
gin_trgm_ops はトライグラム演算子クラスで、pg_trgm 用のインデックスを作成するために必要です。
検索クエリの実装
インデックスを作成したら、検索クエリで ILIKE または類似度演算子を使います。
-- ILIKE を使った部分一致検索
SELECT * FROM profiles
WHERE nickname ILIKE '%ギター%'
OR bio ILIKE '%ギター%';
GIN インデックスがあれば、この ILIKE クエリはインデックスを使用して高速に実行されます。
開発日記では、この判断についてこう記録しています。
pg_trgm: 日本語の部分一致検索を高速化。LIKEクエリの遅延を回避
IdeaSpool での実装 — アイデア検索の最適化
IdeaSpool はアイデア管理ツールで、ユーザーが入力したアイデアのタイトルや本文を検索できます。
2026年1月22日の開発日記には、こう書いています。
pg_trgm による日本語全文検索
このとき、GitHub ストレージから Supabase に移行するタイミングで、検索機能も刷新しました。
学んだこと
開発日記には、実装を通じて学んだことが記録されています。
pg_trgm は日本語検索にも使える(GIN インデックス必須)
このコメントが示すように、pg_trgm は日本語でも動作します。ただし、GIN インデックスがないと効果がありません。インデックスなしで ILIKE を使うと、データ量に比例して遅くなります。
複数カラムの検索
アイデアの検索では、タイトルと本文の両方を検索対象にしたいケースがあります。
SELECT * FROM ideas
WHERE title ILIKE '%キーワード%'
OR content ILIKE '%キーワード%';
このとき、title と content の両方に GIN インデックスを作成しておく必要があります。
CREATE INDEX idx_ideas_title_trgm
ON ideas
USING GIN (title gin_trgm_ops);
CREATE INDEX idx_ideas_content_trgm
ON ideas
USING GIN (content gin_trgm_ops);
pg_trgm の注意点と制限
pg_trgm は便利ですが、いくつかの制限があります。
3文字未満のキーワードでは効果が薄い
トライグラム(3文字単位)で分解するため、2文字以下のキーワードではインデックスの恩恵を受けにくくなります。
BandBridge の開発日記には、こう記録しています。
pg_trgm は3文字以上のマッチで効果を発揮
「渋谷」(2文字)よりも「渋谷区」(3文字)の方が検索効率が良いということです。
実際のアプリケーションでは、2文字のキーワードでも検索は動作しますが、パフォーマンスの改善幅は小さくなります。
インデックスサイズが大きくなる
GIN インデックスは、B-tree インデックスに比べてサイズが大きくなる傾向があります。テキストカラムが多い場合や、テキストの長さが長い場合は、データベースのストレージ使用量に注意が必要です。
類似度スコアの閾値調整
pg_trgm では、類似度を使った検索も可能です。
SELECT *, similarity(nickname, '検索キーワード') AS score
FROM profiles
WHERE nickname % '検索キーワード'
ORDER BY score DESC;
% 演算子は、類似度が閾値(デフォルト 0.3)以上のレコードを返します。この閾値は pg_trgm.similarity_threshold で調整できます。
SET pg_trgm.similarity_threshold = 0.1;
閾値を下げると、より緩い一致でもヒットするようになりますが、ノイズも増えます。アプリケーションの要件に応じて調整が必要です。
Supabase でのベストプラクティス
BandBridge と IdeaSpool の開発を通じて、Supabase で pg_trgm を使う際のベストプラクティスをいくつか整理しました。
1. インデックスはマイグレーションで管理する
Supabase の SQL Editor で直接インデックスを作成することもできますが、マイグレーションファイルで管理する方が安全です。
-- migration: 20260107_add_search_indexes.sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX IF NOT EXISTS idx_profiles_nickname_trgm
ON profiles
USING GIN (nickname gin_trgm_ops);
マイグレーションで管理することで、開発環境と本番環境のインデックス構成を一致させられます。
2. 検索対象カラムを絞る
すべてのテキストカラムにインデックスを作成するのではなく、実際に検索対象となるカラムに絞ってインデックスを作成します。
BandBridge の場合、検索対象は以下に限定しました。
- プロフィール:
nickname,bio - バンド:
name,description - 募集:
title,description
音楽経歴(music_story)のような長文フィールドは、検索対象から外しました。検索対象を増やすほどインデックスが肥大化するため、本当に必要なカラムだけに絞ることが重要です。
3. RLS との組み合わせに注意
Supabase では RLS(Row Level Security)を使ってアクセス制御を行います。検索クエリも RLS の対象になるため、インデックスが効いているかどうかを確認するときは、RLS を考慮した上で EXPLAIN ANALYZE を実行する必要があります。
-- RLS を有効にした状態での実行計画を確認
EXPLAIN ANALYZE
SELECT * FROM profiles
WHERE nickname ILIKE '%ギター%';
RLS のポリシーが複雑な場合、インデックスが効かないケースもあります。その場合は、クエリの書き方を工夫するか、RLS ポリシーを見直す必要があります。
実装のステップバイステップ
ここまでの内容をまとめると、Supabase で pg_trgm を使った日本語検索を実装する手順は以下のようになります。
ステップ 1: 拡張機能の有効化
CREATE EXTENSION IF NOT EXISTS pg_trgm;
ステップ 2: GIN インデックスの作成
検索対象のカラムに対してインデックスを作成します。
CREATE INDEX idx_your_table_column_trgm
ON your_table
USING GIN (column_name gin_trgm_ops);
ステップ 3: 検索クエリの実装
ILIKE を使った部分一致検索を実装します。
SELECT * FROM your_table
WHERE column_name ILIKE '%検索キーワード%';
ステップ 4: パフォーマンスの確認
EXPLAIN ANALYZE でインデックスが使われていることを確認します。
EXPLAIN ANALYZE
SELECT * FROM your_table
WHERE column_name ILIKE '%検索キーワード%';
出力に Bitmap Index Scan や Index Scan が含まれていれば、インデックスが効いています。Seq Scan(シーケンシャルスキャン)になっている場合は、インデックスが効いていないので原因を調査します。
まとめ — 日本語検索の現実的な解決策
Supabase で日本語の全文検索を実装する場合、pg_trgm は現実的で効果的な選択肢です。
メリット:
- Supabase 標準搭載、追加料金なし
- 日本語の部分一致検索に対応
- GIN インデックスで高速化可能
注意点:
- 3文字未満のキーワードでは効果が薄い
- インデックスサイズが大きくなる傾向
- RLS との組み合わせに注意
BandBridge と IdeaSpool の開発では、pg_trgm を導入することで、検索のレスポンスが大幅に改善しました。特に、ユーザー数やデータ量が増えてくる段階では、最初から pg_trgm を導入しておくことをお勧めします。
日本語検索は難しい問題ですが、pg_trgm を使えば、追加のインフラなしで十分な性能が得られます。Supabase を使っている方は、ぜひ試してみてください。
Zeronova(ゼロノバ)
Product Manager / AI-Native Builder
Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。