2026年7月3日
2026年7月3日
PostgreSQLの全文検索設定方法(tsvector・GINインデックスと日本語対応)
PostgreSQL の全文検索は、文章を tsvector(検索用に分解した単語の集まり)に変換して GIN インデックスを作り、tsquery(検索条件)と @@ 演算子で照合する、という仕組みで動きます。英語ならこの標準機能だけで実用になりますが、日本語は単語の区切りに空白がないため、標準機能のままでは思うように検索できません。この記事では、標準の全文検索の設定手順、検索結果の並べ替えと抜粋表示、日本語を検索するための拡張機能の選び方までを説明します。
WordPress のコアは MySQL・MariaDB を対象にしており、PostgreSQL は公式にはサポートされていません。この記事は、WordPress とは別に動かすアプリケーションや、記事データを書き出して検索に使うデータベースで PostgreSQL を使う場面を想定しています。
全文検索の仕組みと LIKE との違い
WHERE body LIKE '%replication%' のような部分一致検索は、先頭が % のため通常の B-tree インデックスが使えず、行数が増えるほど遅くなります。また、「replicate」と「replication」のような語形の違いも区別できません。
全文検索では、保存する側と検索する側の両方を同じ規則で単語に分解します。英語の設定(english)なら、語形を基本形にそろえ(ステミング)、「the」「is」のような頻出語(ストップワード)を除きます。
SELECT to_tsvector('english', 'PostgreSQL replication is replicating data');
-- 'data':5 'postgresql':1 'replic':2,4 のように、基本形と出現位置が並ぶ
SELECT to_tsvector('english', 'replication') @@ to_tsquery('english', 'replicate');
-- true(どちらも 'replic' にそろうため一致する)
どの言語設定が使えるかは \dF(psql)で確認できます。設定名を省略すると default_text_search_config の値が使われますが、環境によって既定値が違うため、SQL では設定名を明示しておくほうが安全です。
検索用の列とインデックスを作る
PostgreSQL 12 以降なら、生成列で tsvector を自動的に作るのが簡単です。タイトルと本文に重みを付けて、タイトルに一致したほうを上位にできます。
CREATE TABLE articles (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title text NOT NULL,
body text NOT NULL DEFAULT '',
search tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(body, '')), 'B')
) STORED
);
CREATE INDEX idx_articles_search ON articles USING GIN (search);
生成列にしておけば、INSERT や UPDATE のたびに PostgreSQL が search を更新するため、アプリケーション側で作り直す処理が要りません。既存のテーブルに追加するときは、ALTER TABLE ... ADD COLUMN で全行の再計算が走り、その間テーブルがロックされます。大きなテーブルではメンテナンス時間に行うか、ステージング環境で所要時間を測ってから実施してください。
生成列を使わず、式インデックスで済ませる方法もあります。
CREATE INDEX idx_articles_body_fts ON articles USING GIN (to_tsvector('english', body));
-- クエリ側も同じ式で書く必要がある
SELECT id FROM articles WHERE to_tsvector('english', body) @@ to_tsquery('english', 'backup');
式インデックスでは、to_tsvector に言語設定を明示した2引数の形を使います。設定名を省いた形はインデックスに使えません。また、クエリの式がインデックスの式と一致しないとインデックスが使われないため、書き間違えやすい点が欠点です。
検索して関連度順に並べる
利用者が入力した文字列を検索条件にするなら、websearch_to_tsquery(PostgreSQL 11 以降)が扱いやすい関数です。検索エンジンに近い書き方を受け付け、"..." でフレーズ、- で除外、or でいずれかを表せます。to_tsquery は & や | を使う専用の文法で、利用者の入力をそのまま渡すと文法エラーになることがあります。
SELECT id, title, ts_rank(search, q) AS rank
FROM articles, websearch_to_tsquery('english', 'postgres replication -mysql') AS q
WHERE search @@ q
ORDER BY rank DESC
LIMIT 20;
アプリケーションから使うときは、検索語を SQL 文字列に埋め込まず、プレースホルダーで渡してください。PHP なら PDO のプリペアドステートメントで websearch_to_tsquery('english', :q) のように書きます。
一致した部分を抜粋表示する
検索結果に一致箇所を強調した抜粋を出すには ts_headline を使います。
SELECT id, title,
ts_headline('english', body, q, 'MaxWords=30, MinWords=10') AS snippet
FROM (
SELECT id, title, body, q
FROM articles, websearch_to_tsquery('english', 'postgres replication') AS q
WHERE search @@ q
ORDER BY ts_rank(search, q) DESC
LIMIT 20
) AS hits;
ts_headline は元の本文を毎回解析するため重い処理です。この例のように、先に上位20件に絞ってから適用します。また、既定では一致した語が <b> タグで囲まれます。本文に利用者が投稿した文字列が含まれる場合、そのまま HTML として出力するとスクリプトが埋め込まれる危険があるため、出力側でエスケープし、強調だけを安全に付け直す処理を用意してください。
日本語を検索する場合
標準のパーサーは空白や記号で単語を区切るため、空白のない日本語の文章は、ひと続きの長い「1語」として扱われます。その結果、「レプリケーション設定」と書かれた本文を「設定」で検索しても一致しない、といったことが起きます。日本語を検索するには、次のいずれかを検討します。
pg_trgm(3文字単位の部分一致)
PostgreSQL に同梱されている拡張機能で、文字列を3文字ずつに区切った「トライグラム」でインデックスを作ります。全文検索というより、LIKE '%...%' を速くする仕組みです。
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_articles_body_trgm ON articles USING GIN (body gin_trgm_ops);
SELECT id, title FROM articles WHERE body LIKE '%レプリケーション%';
検索語が2文字以下のときはインデックスがほとんど役に立たず、日本語では「設定」「障害」のような2文字の語がよく使われる点が弱点です。また、データベースのロケール設定によっては日本語の文字がトライグラムの対象にならないことがあります。導入後に EXPLAIN でインデックスが使われているか、実際の検索語で確かめてください。
pg_bigm(2文字単位)
日本語での利用を想定して作られた拡張機能で、2文字単位でインデックスを作るため、2文字の検索語にも対応できます。PostgreSQL 本体には同梱されていないため、別途インストールが要ります。
PGroonga(日本語向けの全文検索エンジン)
全文検索エンジン Groonga を PostgreSQL から使う拡張機能です。日本語を含む多言語の全文検索、関連度順の並べ替え、専用の演算子(&@~ など)を備えています。
CREATE EXTENSION IF NOT EXISTS pgroonga;
CREATE INDEX idx_articles_pgroonga ON articles USING pgroonga (body);
SELECT id, title FROM articles WHERE body &@~ 'レプリケーション 設定';
どれを選ぶかは、検索の精度、データ量、運用環境で決まります。特に Amazon RDS などのマネージドサービスでは、利用できる拡張機能がサービスとバージョンごとに決まっており、pg_bigm や PGroonga が使えない場合があります(2026年10月現在)。採用前に、使っているサービスの対応拡張機能の一覧を確認してください。拡張機能の対応バージョンも更新されていくため、各プロジェクトの公式ドキュメントで、使う PostgreSQL のバージョンに対応しているかを確かめます。
検索の要件が高度な場合(表記ゆれの吸収、同義語、検索ログに基づく調整など)は、Elasticsearch や OpenSearch のような専用の検索エンジンを別に立てる選択肢もあります。ただし、データの同期という運用の手間が増えるため、まずはデータベース内の仕組みで足りるかを試すのが現実的です。
設定後に確認すること
EXPLAIN (ANALYZE)で、検索クエリが Bitmap Index Scan などでインデックスを使っているか- 実際の利用者が入力しそうな検索語(短い語、記号を含む語、英数字と日本語が混ざった語)で、期待どおりの結果が返るか
- 大量の更新があるテーブルで、GIN インデックスの更新が書き込み性能に影響していないか
GIN インデックスは更新をまとめて反映する仕組み(fastupdate)を持つため、書き込み直後の一時的な遅れや、まとめて反映するときの負荷が出ることがあります。本番に入れる前に、ステージング環境で実データに近い量を使って検索と更新の両方を測ってください。
まとめ
- 全文検索は
tsvectorとtsqueryを@@で照合し、GIN インデックスで速くする - PostgreSQL 12 以降は生成列で
tsvectorを自動更新するのが簡単。言語設定は明示する - 利用者の入力は
websearch_to_tsqueryにプレースホルダーで渡す ts_headlineは重いので上位の行に絞ってから使い、出力はエスケープする- 日本語は標準機能だけでは不十分。pg_trgm・pg_bigm・PGroonga から、精度と運用環境で選ぶ
- マネージドサービスでは使える拡張機能が限られるため、採用前に確認する