- データベース
PostgreSQL日本語全文検索|pg_trgmとpg_bigmの速度比較と選び方
PostgreSQLで日本語の全文検索を手軽に速くする方法とは、LIKE '%検索語%' の中間一致に GIN インデックスを効かせる拡張 pg_trgm(3文字単位)か pg_bigm(2文字単位)を使うことです。 標準の全文検索(tsvector)は空白で単語を区切るため、そのままでは日本語を分割できません。pg_trgm は PostgreSQL に同梱されていてすぐ使えますが、3文字に満たない検索語(「猫」「電池」など)ではインデックスが効きません。pg_bigm は別途インストールが必要な代わりに、1〜2文字の語でも速く検索できます。
この記事では、PostgreSQL 16.13 に日本語の文章10万件(平均108文字)を入れ、インデックスなし/pg_trgm/pg_bigm の3通りで、1文字・2文字・3文字以上の検索語の実行時間を EXPLAIN ANALYZE で実測 しました。あわせて、インデックスサイズ、作成時間、1万件INSERTの更新コスト、データベースの lc_ctype が C だと pg_trgm が日本語を扱えない ことの確認、similarity・word_similarity・% 演算子の出力、コピペで使える導入SQL一式まで、選ぶための材料をまとめています。
日本語全文検索の方式早見表
| 方式 | 仕組み | 導入 | 1〜2文字の語 | 3文字以上の語 | あいまい検索 | 向いている場面 |
|---|---|---|---|---|---|---|
| LIKE(インデックスなし) | 全行を順に読む | 不要 | 全件走査 | 全件走査 | なし | 数万件までの小さな表 |
| pg_trgm + GIN | 3文字ずつ(トライグラム)で索引 | 同梱(CREATE EXTENSION) |
索引が効かない | 速い | similarity / % |
3文字以上の語が中心、ILIKE・正規表現も使う |
| pg_bigm + GIN | 2文字ずつ(バイグラム)で索引 | 別途インストール | 速い | 速い | bigm_similarity / =% |
日本語の短い語も検索する |
| tsvector(標準の全文検索) | 単語に分割して索引 | 同梱 | 日本語は分割されない | 日本語は分割されない | ランキング | 英語など空白区切りの言語 |
| PGroonga などの形態素・N-gram 系 | 専用エンジンで分割・索引 | 別途インストール | 速い | 速い | スコア・同義語など | 大量の文書、検索機能を作り込む場合 |
「同梱でまず試すなら pg_trgm、1〜2文字の日本語を検索するなら pg_bigm」 が基本の選び方です。今回の実測の要点は次の表です(詳細は後述)。
| 検索語(ヒット件数) | インデックスなし | pg_trgm GIN | pg_bigm GIN |
|---|---|---|---|
| 猫(1文字・246件) | 62〜120ms | 66ms(Seq Scan) | 1.2ms |
| 電池(2文字・326件) | 61〜96ms | 59ms(Seq Scan) | 1.3ms |
| 東京(2文字・32,539件) | 64〜65ms | 64ms(Seq Scan) | 15ms |
| プリンター(5文字・278件) | 115〜129ms | 2.5ms | 1.5ms |
| バックアップ(6文字・24,746件) | 99〜103ms | 63ms | 32ms |
| インデックスサイズ/作成時間 | ― | 22MB/5.9秒 | 17MB/4.3秒 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PostgreSQL日本語全文検索|pg_trgmとpg_bigmの速度比較と選び方」
方式の選び方フロー
flowchart TD
A[日本語の文章を検索したい] --> B{データ件数は数万件以下で<br>応答も十分速い?}
B -- はい --> C[LIKE のままでよい]
B -- いいえ --> D{1〜2文字の語でも<br>検索する?}
D -- いいえ --> E{拡張を追加<br>インストールできる?}
E -- できない --> F[pg_trgm + GIN]
E -- できる --> G[pg_trgm か pg_bigm]
D -- はい --> H{拡張を追加<br>インストールできる?}
H -- できる --> I[pg_bigm + GIN]
H -- できない --> J[pg_trgm + 3文字未満は別の工夫<br>例: 候補を絞ってから LIKE]
I --> K{表記ゆれ・品詞・<br>スコアも必要?}
K -- はい --> L[PGroonga などの<br>専用エンジンを検討]
マネージドサービスの PostgreSQL では、使える拡張が決まっています。pg_bigm や PGroonga を使う前に、利用中のサービスの「対応拡張一覧」で名前を確認 しておくと手戻りがありません。
検証環境とテストデータ
| 項目 | 内容 |
|---|---|
| PostgreSQL | 16.13(Ubuntu 24.04 のパッケージ) |
| pg_trgm | 1.6(PostgreSQL 16 同梱) |
| pg_bigm | 1.2(GitHub の pgbigm/pg_bigm をソースからビルド。最新リリースタグは v1.2-20250903) |
| データベース | ENCODING UTF8、LC_CTYPE C.UTF-8 |
| データ | 架空の備品・在庫に関する日本語文章 10万件、本文の平均108文字、テーブル 42MB |
| 計測 | EXPLAIN (ANALYZE) の Execution Time を7回計測した中央値(共有コンテナのため数十msの揺れあり) |
pg_bigm は Ubuntu 標準の apt リポジトリに postgresql-16-pg-bigm が見つからなかったため(PostgreSQL 公式の apt リポジトリ PGDG を追加すると入れられます)、ソースからビルドしました。手順は次のとおりです。
sudo apt-get install -y postgresql-server-dev-16 git build-essential
git clone --depth 1 https://github.com/pgbigm/pg_bigm.git
cd pg_bigm
make USE_PGXS=1 PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config
sudo make USE_PGXS=1 PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config install
テーブルは次の形です。データはPythonで生成したCSVを \copy で読み込みました。
CREATE TABLE docs (
id int PRIMARY KEY,
title text NOT NULL,
body text NOT NULL
);
\copy docs FROM 'docs.csv' CSV
VACUUM ANALYZE docs;
本文の例:
ホチキスのバックアップを整理するときは金沢のバックアップも整理する。マーカーの棚卸をまとめるときは金沢の検索もまとめる。便利なデータベースのはさみについて大阪で登録する方法。…
検索語は、文字数(1・2・5・6文字)とヒット件数(少ない・多い) の組み合わせになるよう選びました。
| 検索語 | 文字数 | ヒット件数 |
|---|---|---|
| 猫 | 1 | 246 |
| 電池 | 2 | 326 |
| 東京 | 2 | 32,539 |
| プリンター | 5 | 278 |
| バックアップ | 6 | 24,746 |
pg_trgmでLIKE中間一致を速くする
導入とインデックス作成
CREATE EXTENSION IF NOT EXISTS pg_trgm;
\timing on
CREATE INDEX docs_body_trgm ON docs USING gin (body gin_trgm_ops);
SELECT pg_size_pretty(pg_relation_size('docs_body_trgm'));
CREATE INDEX
Time: 5875.378 ms (00:05.875)
pg_size_pretty
----------------
22 MB
(1 row)
3文字以上の語は速くなる
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM docs WHERE body LIKE '%プリンター%';
Aggregate (actual time=5.277..5.279 rows=1 loops=1)
-> Bitmap Heap Scan on docs (actual time=0.144..5.252 rows=278 loops=1)
Recheck Cond: (body ~~ '%プリンター%'::text)
Heap Blocks: exact=270
-> Bitmap Index Scan on docs_body_trgm (actual time=0.098..0.099 rows=278 loops=1)
Index Cond: (body ~~ '%プリンター%'::text)
Execution Time: 5.387 ms
インデックスで278件に絞り込み、インデックスなし(115〜129ms)から中央値2.5msへ 短くなりました。ヒットが多い「バックアップ」(24,746件)も Bitmap Index Scan になり、27〜63msでした。ヒット件数が多いと、絞り込んだ後に本体の行を読む量が増えるため、短縮の幅は小さくなります。
1〜2文字の語ではインデックスが効かない
「電池」「東京」「猫」では、プランナーはインデックスを使わず Seq Scan を選びました。理由を確かめるため、enable_seqscan = off でインデックスを強制してみます。
SET enable_seqscan = off;
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM docs WHERE body LIKE '%電池%';
Aggregate (actual time=257.976..257.979 rows=1 loops=1)
-> Bitmap Heap Scan on docs (actual time=196.054..257.910 rows=326 loops=1)
Recheck Cond: (body ~~ '%電池%'::text)
Rows Removed by Index Recheck: 99674
Heap Blocks: exact=5427
-> Bitmap Index Scan on docs_body_trgm (actual time=195.074..195.075 rows=100000 loops=1)
Index Cond: (body ~~ '%電池%'::text)
Execution Time: 258.118 ms
インデックスが10万件すべてを候補として返し(rows=100000)、99,674件を再チェックで捨てています。 '%電池%' からは3文字の組(トライグラム)が1つも作れないため、インデックスで絞り込めないのです。結果として、Seq Scan(約60ms)より遅い258msになりました。プランナーが Seq Scan を選んだのは妥当な判断です。
| 検索語の長さ | pg_trgm の動き |
|---|---|
| 1〜2文字 | トライグラムが作れず、インデックスで絞り込めない |
| 3文字以上 | トライグラムで候補を絞り込める(長いほど絞り込みが強い) |
pg_trgmの日本語対応は lc_ctype で決まる
pg_trgm は、データベースの LC_CTYPE(文字の種類の判定に使うロケール)で「単語を構成する文字」と認めた文字だけ からトライグラムを作ります。同じ文字列を、LC_CTYPE の違う3つのデータベースで調べました。
CREATE DATABASE t87_c TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C';
CREATE DATABASE t87_ja TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'ja_JP.UTF-8' LC_CTYPE 'ja_JP.UTF-8';
-- 各DBで
CREATE EXTENSION pg_trgm;
SELECT show_trgm('東京タワー');
| LC_CTYPE | show_trgm('東京タワー') |
similarity('東京タワー','東京スカイツリー') |
|---|---|---|
| C.UTF-8 | {0xb4cae8,0x160599,0x1b0ad0,0x4325b1,NVD,0x5ae818} |
0.15384616 |
| ja_JP.UTF-8 | {0xb4cae8,0x160599,0x1b0ad0,0x4325b1,NVD,0x5ae818} |
0.15384616 |
| C | {}(空) |
0 |
LC_CTYPE が C のデータベースでは、日本語からトライグラムが1つも作られません。 この状態で同じ10万件に pg_trgm のインデックスを作ると、サイズは512kBしかなく、'%プリンター%' でもインデックスが10万件すべてを返して144msかかりました(C.UTF-8 のDBでは5ms)。
-- LC_CTYPE = C のデータベース
-> Bitmap Index Scan on docs_body_trgm (actual time=11.205..11.206 rows=100000 loops=1)
Index Cond: (body ~~ '%プリンター%'::text)
Execution Time: 144.054 ms
PostgreSQL 16 では SHOW lc_ctype は使えず(ERROR: unrecognized configuration parameter "lc_ctype")、次のSQLで確認します。
SELECT datname, datcollate, datctype, pg_encoding_to_char(encoding)
FROM pg_database WHERE datname = current_database();
pg_trgm で日本語を扱うなら、LC_CTYPE が C.UTF-8 や ja_JP.UTF-8 のデータベースを使う のが前提です。LC_CTYPE はデータベース作成時に決まるので、C の場合は新しいデータベースを作って移す必要があります。pg_bigm はロケールに依存せず日本語を2文字ずつ扱います。
pg_bigmで1〜2文字の語も速くする
導入とインデックス作成
CREATE EXTENSION IF NOT EXISTS pg_bigm;
SELECT extversion FROM pg_extension WHERE extname = 'pg_bigm';
\timing on
CREATE INDEX docs_body_bigm ON docs USING gin (body gin_bigm_ops);
SELECT pg_size_pretty(pg_relation_size('docs_body_bigm'));
extversion
------------
1.2
(1 row)
CREATE INDEX
Time: 4317.081 ms (00:04.317)
pg_size_pretty
----------------
17 MB
(1 row)
CREATE EXTENSION だけで使えます。設定パラメータ(pg_bigm.enable_recheck など)を postgresql.conf で変えたいときは、shared_preload_libraries か session_preload_libraries に pg_bigm を加えます。
1文字・2文字でもインデックスが効く
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM docs WHERE body LIKE '%猫%';
Aggregate (actual time=1.200..1.202 rows=1 loops=1)
-> Bitmap Heap Scan on docs (actual time=0.176..1.174 rows=246 loops=1)
Recheck Cond: (body ~~ '%猫%'::text)
Heap Blocks: exact=241
-> Bitmap Index Scan on docs_body_bigm (actual time=0.141..0.141 rows=246 loops=1)
Index Cond: (body ~~ '%猫%'::text)
Execution Time: 1.306 ms
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM docs WHERE body LIKE '%東京%';
Aggregate (actual time=14.676..14.678 rows=1 loops=1)
-> Bitmap Heap Scan on docs (actual time=3.345..12.951 rows=32539 loops=1)
Recheck Cond: (body ~~ '%東京%'::text)
Heap Blocks: exact=5424
-> Bitmap Index Scan on docs_body_bigm (actual time=2.582..2.582 rows=32539 loops=1)
Index Cond: (body ~~ '%東京%'::text)
Execution Time: 14.735 ms
1文字の「猫」で約1.3ms、ヒットが3万件を超える「東京」でも約15ms でした。pg_trgm で Seq Scan になっていた語がすべてインデックスで検索できています。
pg_bigmで使えない書き方
pg_bigm の GIN インデックスは LIKE 用です。ILIKE と正規表現では使われませんでした(前方一致の LIKE 'プリンター%' は使われます)。
-- body ILIKE '%プリンター%'
-> Seq Scan on docs (cost=0.00..6677.00 rows=10 width=0)
Filter: (body ~~* '%プリンター%'::text)
-- body ~ 'プリンター'
-> Seq Scan on docs (cost=0.00..6677.00 rows=10 width=0)
Filter: (body ~ 'プリンター'::text)
大文字・小文字を区別しない検索や正規表現検索もインデックスで速くしたいときは pg_trgm が向いています(pg_trgm は ILIKE・~・~* に対応しています)。英数字の大文字小文字をそろえたいだけなら、lower(body) の式インデックスと lower(body) LIKE … の組み合わせも使えます。
likequery() で記号を安全に検索する
ユーザーの入力に % や _ が含まれると、LIKE のワイルドカードとして解釈されます。pg_bigm の likequery() は、記号をエスケープして前後に % を付けてくれます。
SELECT likequery('100%_OFF');
SELECT count(*) FROM docs WHERE body LIKE likequery('電池');
likequery
--------------
%100\%\_OFF%
(1 row)
count
-------
326
(1 row)
インデックスサイズと更新コスト
10万件の本文に対するインデックスの比較と、同じ10,000行を INSERT したときの時間(2回計測)です。
| 項目 | インデックスなし | pg_trgm GIN | pg_bigm GIN |
|---|---|---|---|
| インデックスサイズ | ― | 22MB | 17MB |
| 作成時間(10万件) | ― | 5.9秒 | 4.3秒 |
| 10,000行 INSERT | 33〜66ms | 861〜934ms | 739〜841ms |
-- 計測に使ったSQL(実行後に追加分は削除)
\timing on
INSERT INTO docs SELECT id + 100000, title, body FROM docs WHERE id <= 10000;
DELETE FROM docs WHERE id > 100000;
VACUUM docs;
全文検索用の GIN インデックスがあると、INSERT は今回の条件で十数倍の時間 になりました。1行の文章から数十〜百個以上のキー(トライグラム・バイグラム)を作って索引に登録するためです。GIN には新しいキーをいったん「保留リスト」にためて後からまとめて反映する仕組み(fastupdate、既定で有効)があり、まとめて取り込む場合は次の手順にすると全体の時間を短くできます。
| 場面 | おすすめ |
|---|---|
| 初回に大量のデータを入れる | データを入れ終えてから CREATE INDEX |
| 定期的に大量に入れ替える | インデックスを削除 → 投入 → 再作成、を比較してみる |
| 1件ずつの登録が中心 | そのままで可(1行あたり約0.1ms弱の増加) |
| 検索結果に反映が遅れることを気にする | 保留リストは検索時にも読まれるため結果は正しい。サイズは gin_pending_list_limit で調整 |
類似度検索:similarity・word_similarity・%演算子
pg_trgm には、文字列がどれだけ似ているかを0〜1で返す関数があります。あいまい検索(入力の揺れを許す検索)に使えます。
show_trgm で分割結果を見る
SELECT show_trgm('PostgreSQL');
SELECT show_trgm('東京タワー');
SELECT show_trgm('猫');
show_trgm
-----------------------------------------------------
{" p"," po",esq,gre,ost,pos,"ql ",res,sql,stg,tgr}
show_trgm
----------------------------------------------------
{0xb4cae8,0x160599,0x1b0ad0,0x4325b1,NVD,0x5ae818}
show_trgm
---------------------
{0x2c4429,0x336c3f}
英字は小文字にしたうえで、単語の前に空白2つ、後ろに空白1つを補って3文字ずつ に分けます。日本語などのマルチバイト文字のトライグラムは、ハッシュ値(0x…)で表示されます。「猫」は前後の空白を補った組だけになるため、LIKE '%猫%'(空白を補わない検索)では使えるトライグラムがありません。pg_bigm の show_bigm は2文字ずつで、そのまま読めます。
SELECT show_bigm('東京タワー');
show_bigm
-----------------------------------
{タワ,ワー,"ー ",京タ,東京," 東"}
similarity と % 演算子
SELECT similarity('東京タワー', '東京スカイツリー') AS sim,
word_similarity('タワー', '東京タワーの夜景') AS word_sim,
strict_word_similarity('タワー', '東京タワーの夜景') AS strict_sim;
SELECT 'ボールペン黒' % 'ボールペン赤' AS similar_op,
similarity('ボールペン黒', 'ボールペン赤');
SHOW pg_trgm.similarity_threshold;
sim | word_sim | strict_sim
------------+----------+-------------
0.15384616 | 0.25 | 0.083333336
similar_op | similarity
------------+------------
t | 0.5555556
pg_trgm.similarity_threshold
------------------------------
0.3
| 関数・演算子 | 意味 | しきい値の設定 |
|---|---|---|
similarity(a, b) |
2つの文字列全体のトライグラムの共通割合 | ― |
a % b |
similarity がしきい値以上なら true | pg_trgm.similarity_threshold(既定0.3) |
word_similarity(a, b) |
a と、b の中の連続した一部分との最大の類似度 | ― |
a <% b |
word_similarity がしきい値以上なら true | pg_trgm.word_similarity_threshold(既定0.6) |
strict_word_similarity(a, b) |
b の単語の区切りに合わせて比べる類似度 | pg_trgm.strict_word_similarity_threshold(既定0.5) |
a <-> b |
距離(1 - similarity)。ORDER BY で近い順 |
― |
bigm_similarity(a, b)(pg_bigm) |
バイグラムでの類似度 | pg_bigm.similarity_limit(=% 演算子用) |
word_similarity('タワー', '東京タワーの夜景') が 0.25 と小さいのは、日本語に空白がないため です。'タワー' 側には前後の空白を補ったトライグラムができますが、'東京タワーの夜景' の中では「タワー」の前後に空白がないので一致しません。空白で区切った word_similarity('タワー', '東京 タワー の夜景') は 1 になりました。日本語の部分一致は LIKE、表記の揺れの許容は similarity と役割を分けると扱いやすくなります。
類似度の高い順に取り出す(インデックスつき)
CREATE INDEX docs_title_trgm ON docs USING gin (title gin_trgm_ops);
SELECT title,
round(similarity(title, '東京の在庫でボールペンを確認')::numeric, 3) AS score
FROM docs
WHERE title % '東京の在庫でボールペンを確認'
ORDER BY score DESC, id
LIMIT 3;
title | score
----------------------------------------+-------
東京の在庫で便利なボールペンを見直す | 0.417
東京の在庫で静かなボールペンを更新する | 0.400
東京の在庫で小さなボールペンを比較する | 0.400
(3 rows)
title % '…' の件数を数えるクエリは、インデックスなしの Seq Scan で 640ms、GIN インデックスありの Bitmap Index Scan で 7.8ms でした。similarity() は real 型を返すため、round(similarity(…), 3) と書くと ERROR: function round(real, integer) does not exist になります。 ::numeric を付けてから丸めてください。
tsvector(標準の全文検索)が日本語に向かない理由
PostgreSQL の標準の全文検索は、文章を単語(トークン)に分けて索引を作ります。区切りは空白などで判断するため、日本語の文章は1つの大きなトークンになります。
SELECT to_tsvector('simple', '東京タワーの夜景を撮影しました'),
to_tsvector('simple', 'PostgreSQL full text search');
to_tsvector | to_tsvector
------------------------------------+---------------------------------------------
'東京タワーの夜景を撮影しました':1 | 'full':2 'postgresql':1 'search':4 'text':3
日本語を tsvector で扱うには、形態素解析(文を単語に分ける処理)で分かち書きしてから渡す などの準備が必要です。それを拡張側でまとめて行うのが PGroonga などの専用エンジンです。PGroonga は本記事の環境では apt で見つからなかったため未検証で、導入は公式サイトの手順を参照してください。
コピペ用:導入SQL一式
pg_trgm(同梱)
-- 1. 前提:LC_CTYPE が C でないこと
SELECT datctype FROM pg_database WHERE datname = current_database();
-- 2. 拡張とインデックス
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX CONCURRENTLY docs_body_trgm ON docs USING gin (body gin_trgm_ops);
-- 3. 検索(3文字以上の語で効く。ILIKE・正規表現も可)
SELECT id, title FROM docs WHERE body LIKE '%プリンター%';
SELECT id, title FROM docs WHERE body ILIKE '%postgresql%';
-- 4. あいまい検索
SET pg_trgm.similarity_threshold = 0.3;
SELECT id, title, similarity(title, '在庫の確認') AS score
FROM docs WHERE title % '在庫の確認'
ORDER BY score DESC LIMIT 10;
pg_bigm(要インストール)
-- 1. 拡張とインデックス
CREATE EXTENSION IF NOT EXISTS pg_bigm;
CREATE INDEX CONCURRENTLY docs_body_bigm ON docs USING gin (body gin_bigm_ops);
-- 2. 検索(1文字から効く。入力値は likequery() でエスケープ)
SELECT id, title FROM docs WHERE body LIKE likequery('猫');
-- 3. 類似度
SELECT bigm_similarity('東京タワー', '東京スカイツリー');
CREATE INDEX CONCURRENTLY は、テーブルへの書き込みを止めずにインデックスを作る書き方です(時間は通常より長くなり、トランザクションブロックの中では使えません)。
インデックスの確認と削除
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS size, idx_scan
FROM pg_stat_user_indexes WHERE relname = 'docs';
DROP INDEX CONCURRENTLY IF EXISTS docs_body_trgm;
idx_scan が増えていればインデックスが使われています。GIN インデックスの仕組みそのものや B-tree との違いは、別記事「PostgreSQLインデックスの種類」で解説しています。
よくあるつまずきと対処
| 症状 | 原因 | 対処 |
|---|---|---|
pg_trgm を入れたのに LIKE '%電池%' が速くならない |
2文字以下はトライグラムが作れない | pg_bigm を使う、または3文字以上で検索する |
| pg_trgm のインデックスが極端に小さく、日本語で効かない | DB の LC_CTYPE が C |
C.UTF-8 / ja_JP.UTF-8 の DB を作って移す |
SHOW lc_ctype がエラー |
PostgreSQL 16 で設定パラメータから外れた | pg_database.datctype を参照 |
| pg_bigm で ILIKE が遅い | gin_bigm_ops は LIKE 用 | lower() の式インデックスか pg_trgm |
function round(real, integer) does not exist |
similarity は real 型 | similarity(…)::numeric |
| 大量 INSERT が遅くなった | GIN のキー登録コスト | 投入後にインデックス作成、まとめて投入 |
ユーザー入力の % がワイルドカードになる |
LIKE の特殊文字 | pg_bigm の likequery()、または \ でエスケープ |
よくある質問
Q. pg_trgm と pg_bigm はどちらを選べばよいですか? A. 1〜2文字の日本語(「猫」「電池」「東京」など)も検索するなら pg_bigm です。今回の実測では、pg_trgm がインデックスを使えなかった1〜2文字の語を、pg_bigm は1〜15msで検索できました。拡張を追加できない環境や、ILIKE・正規表現も使う場合は pg_trgm が向いています。
Q. pg_trgm で日本語が使えないと聞きました。
A. pg_trgm は現在、マルチバイト文字も扱えます。ただし データベースの LC_CTYPE が C だと日本語からトライグラムが作られない ため、使えないように見えます。今回の環境でも、C.UTF-8 では show_trgm('東京タワー') に値が出て、C では空でした。
Q. pg_bigm と pg_trgm のインデックスを両方作ってもよいですか? A. 作れますが、どちらも INSERT・UPDATE のたびに更新されるので、更新コストとディスク使用量が足し算で増えます。検索のパターン(LIKE だけか、ILIKE・類似度も使うか)を決めて、どちらか一方に絞る のがおすすめです。
Q. 何件くらいからインデックスが必要ですか? A. 今回の10万件・平均108文字では、インデックスなしでも60〜130msでした。応答時間の目標と同時アクセス数で判断し、EXPLAIN ANALYZE で Seq Scan の時間を見てから導入 を決めると無駄がありません。
まとめ
- 日本語の中間一致検索は、pg_trgm か pg_bigm の GIN インデックスで速くできる。tsvector はそのままでは日本語を分割しない。
- pg_trgm は3文字以上の語で効き、「プリンター」は115〜129ms → 2.5ms になった。1〜2文字の語ではインデックスで絞り込めない。
- pg_bigm は1〜2文字の語でも効き、「猫」1.2ms、「電池」1.3ms、「東京」15ms だった。サイズも pg_trgm より小さかった(17MB と 22MB)。
- pg_trgm の日本語対応は LC_CTYPE 次第。
Cではshow_trgmが空になり、インデックスが効かなかった。 - GIN インデックスは INSERT のコストを増やす(1万行で約0.05秒 → 約0.8秒)。大量投入は投入後に作成する。
- あいまい検索は similarity・
%演算子。日本語は空白がないため word_similarity の値は小さく出る。 - 表記ゆれや品詞を扱う本格的な検索には、PGroonga などの専用エンジン も検討する。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PostgreSQL日本語全文検索|pg_trgmとpg_bigmの速度比較と選び方」
株式会社RJCでは、PostgreSQLの検索の仕組みとインデックス設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。