PostgreSQL: PostgreSQL全文検索エンジン
最終更新:2026-08-26
1. 学習内容
- 全文検索の中心的概念(ドキュメント、語彙素、辞書、設定)を理解する
tsvectorとtsqueryデータ型を使用するto_tsvector/to_tsquery/plainto_tsquery/phraseto_tsquery/websearch_to_tsqueryによる変換を使用する@@演算子で全文検索を実行するts_rankによるランキングとts_headlineによるハイライト表示を使用する- GINインデックスを作成して全文検索を高速化する
- 多言語全文検索(english/simple/zhpinyin)を設定する
ts_stat統計とシステムカタログクエリを使用する
2. ストーリー
CharlieのEコマースプラットフォームには50万件の商品レコードがあり、ユーザーは商品を検索する必要があります。彼は最初 LIKE '%red shoes%' を使いましたが、「red shoes」を検索しても説明文にたまたま連続した文字列「red shoes」が含まれる商品しか返らず、クエリには3秒かかりました。PostgreSQLの全文検索に切り替えた後:
- 「red shoes」は「red」と「shoes」の両方を含む商品に一致(順不同)
- ストップワードが自動的に無視される(the/a/an)
- 自動ステミング(shoes → shoe)
- GINインデックスによる高速化で、クエリはわずか30ミリ秒 — 100倍高速
3. 概念:全文検索の中心的概念
(1) 4つの中心的概念
| 概念 | 説明 | PostgreSQLでの相当物 |
|---|---|---|
| ドキュメント | 検索対象のテキスト内容 | tsvector |
| クエリ | ユーザーの検索語 | tsquery |
| 辞書 | 単語の処理方法(ストップワード、ステミング)を定義 | pg_ts_dict システムカタログ |
| 設定 | パーサーと辞書を組み合わせる | pg_ts_config システムカタログ |
(2) 全文検索処理パイプライン
flowchart LR
A[生テキスト] --> B[パーサー<br/>トークン化]
B --> C[辞書<br/>ステミング+ストップワード]
C --> D[tsvector<br/>ソート済み語彙素]
D --> E[GINインデックス<br/>高速検索]
F[ユーザークエリ] --> G[to_tsquery<br/>解析&正規化]
G --> H[tsquery<br/>語彙素+演算子]
E --> I["@@ 演算子<br/>一致"]
H --> I
I --> J[ts_rank<br/>スコアリング]
J --> K[ts_headline<br/>ハイライト表示]
K --> L[結果]
▶ サンプル:テキストがどのように処理されるかを確認
SELECT * FROM ts_debug('english', 'The red shoes are beautiful');
alias | descriptor | token | dictionaries | dictionary | lexemes
-------+------------+--------+----------------+------------+---------
ascii | word | The | {english_stem} | english_stem| {}
ascii | word | red | {english_stem} | english_stem| {red}
ascii | word | shoes | {english_stem} | english_stem| {shoe}
ascii | word | are | {english_stem} | english_stem| {}
ascii | word | beautiful | {english_stem}| english_stem| {beauti}
4. 概念:tsvectorとtsquery
(1) tsvector — ドキュメントの前処理結果
tsvector は、ソートされ重複除去された語彙素のリストで、各語彙素は位置情報を持ちます。
▶ サンプル:テキストをtsvectorに変換
SELECT to_tsvector('english', 'The red shoes are beautiful shoes');
'beauti':5 'red':2 'shoe':3,6
| ステップ | 入力 | 出力 |
|---|---|---|
| トークン化 | The red shoes are beautiful shoes | [The, red, shoes, are, beautiful, shoes] |
| ストップワード除去 | [The, red, shoes, are, beautiful, shoes] | [red, shoes, beautiful, shoes] |
| ステミング | [red, shoes, beautiful, shoes] | [red, shoe, beauti, shoe] |
| 重複除去+位置 | [red, shoe, beauti, shoe] | 'beauti':5 'red':2 'shoe':3,6 |
(2) tsquery — クエリの前処理結果
tsquery は語彙素とブーリアン演算子の組み合わせです。
| 演算子 | 意味 | 例 |
|---|---|---|
& |
AND | red & shoe |
| |
OR | red | blue |
! |
NOT | !broken |
<-> |
FOLLOWED BY(隣接) | red <-> shoe |
`<N>` |
距離N | red <2> shoe |
▶ サンプル:tsqueryを構築
SELECT to_tsquery('english', 'red & shoe');
-- 'red' & 'shoe'
SELECT to_tsquery('english', 'red | blue');
-- 'red' | 'blue'
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
5. 概念:全文検索クエリ関数
(1) 5つのクエリ構築関数
| 関数 | PGバージョン | 入力形式 | 説明 |
|---|---|---|---|
to_tsquery(config, text) |
すべて | red & shoe |
手動で演算子を記述 |
plainto_tsquery(config, text) |
すべて | red shoes |
プレーンテキスト、自動的に&で結合 |
phraseto_tsquery(config, text) |
9.6+ | red shoes |
プレーンテキスト、自動的に<->で結合 |
websearch_to_tsquery(config, text) |
11+ | "red shoes" -broken |
Web検索エンジン構文 |
tsvector @@ tsquery |
すべて | — | 一致演算子 |
▶ サンプル:plainto_tsquery — プレーンテキスト自動AND
SELECT plainto_tsquery('english', 'red shoes');
-- 'red' & 'shoe'(両方の語を含むドキュメントに一致)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル:phraseto_tsquery — フレーズ検索
SELECT phraseto_tsquery('english', 'red shoes');
-- 'red' <-> 'shoe'('red'が'shoe'の直前に来るドキュメントに一致)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル:websearch_to_tsquery — 検索エンジン構文
SELECT websearch_to_tsquery('english', '"red shoes" -broken OR new');
-- 'red' <-> 'shoe' & !'broken' | 'new'
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| websearch構文 | 意味 |
|---|---|
"exact phrase" |
フレーズ一致(<->) |
-word |
除外(!) |
word1 OR word2 |
OR(|) |
word1 word2 |
AND(&) |
▶ サンプル:@@ 演算子で検索
CREATE TABLE catalog (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
search_vector tsvector
);
INSERT INTO catalog (name, description, search_vector) VALUES
('Red Running Shoes', 'Lightweight red shoes for running',
to_tsvector('english', 'Lightweight red shoes for running')),
('Blue Casual Shoes', 'Comfortable blue shoes for daily wear',
to_tsvector('english', 'Comfortable blue shoes for daily wear')),
('Red Dress', 'Elegant red dress for special occasions',
to_tsvector('english', 'Elegant red dress for special occasions'));
-- "red shoes"を検索
SELECT name, description
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
Output:
INSERT 0 1
6. 概念:ランキングとハイライト表示
(1) ts_rankランキング
ts_rank はドキュメントに対するクエリの関連度を計算し、0から1のfloatを返します。値が高いほど関連度が高いことを示します。
▶ サンプル:関連度でソート
SELECT name,
ts_rank(search_vector, plainto_tsquery('english', 'red shoes')) AS rank
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes')
ORDER BY rank DESC;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| ランキング関数 | 説明 | ユースケース |
|---|---|---|
ts_rank(vector, query) |
頻度ベースのランク | 一般的なランキング |
ts_rank_cd(vector, query) |
カバー密度ランク | 短いドキュメントのより正確なランキング |
ts_rank(vector, query, weights) |
重み付きランク | タイトルを本文より高く重み付け |
▶ サンプル:重み付きランキング(タイトルを説明より高く重み付け)
ALTER TABLE catalog ADD COLUMN title_vector tsvector;
UPDATE catalog SET title_vector = to_tsvector('english', name);
-- 重みを設定:title=D(default)、body=A(highest)
SELECT name,
ts_rank(
setweight(title_vector, 'A') || setweight(search_vector, 'B'),
plainto_tsquery('english', 'red shoes')
) AS rank
FROM catalog
WHERE setweight(title_vector, 'A') || setweight(search_vector, 'B')
@@ plainto_tsquery('english', 'red shoes')
ORDER BY rank DESC;
Output:
UPDATE 3
| 重み文字 | デフォルト重量 | 典型的な用途 |
|---|---|---|
| A | 1.0 | タイトル |
| B | 0.4 | 本文 |
| C | 0.2 | サマリー |
| D | 0.1 | 補足情報 |
(2) ts_headlineハイライト表示
ts_headline は元のテキスト内の一致した語彙素をマークします。
▶ サンプル:検索結果のハイライト表示
SELECT name,
ts_headline('english', description, plainto_tsquery('english', 'red shoes')) AS highlighted
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
name | highlighted
-------------------+-------------------------------------------------
Red Running Shoes | Lightweight <b>red</b> <b>shoes</b> for running
▶ サンプル:カスタムハイライトタグ
SELECT ts_headline(
'english',
description,
plainto_tsquery('english', 'red shoes'),
'StartSel=<em>,StopSel=</em>,MaxWords=10,MinWords=5'
) AS highlighted
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| ハイライトパラメータ | 説明 | デフォルト |
|---|---|---|
StartSel |
一致開始タグ | <b> |
StopSel |
一致終了タグ | </b> |
MaxWords |
ハイライト断片の最大単語数 | 35 |
MinWords |
ハイライト断片の最小単語数 | 15 |
ShortWord |
無視する最小単語長 | 3 |
7. 概念:GINインデックスによる高速化
(1) 3つのGINインデックスアプローチ
| アプローチ | 構文 | 更新オーバーヘッド | 最適な用途 |
|---|---|---|---|
| 列ベース | gin(to_tsvector(config, column)) |
更新時に再構築 | 列の値が変わらない場合に最適 |
| 格納列ベース | gin(search_vector) |
更新オーバーヘッ��が低い | 高頻度更新、トリガー同期が必要 |
| FASTUPDATE | デフォルトで有効 | 遅延マージ | 書き込み重視、読み取り重視のシナリオ |
▶ サンプル:関数ベースのGINインデックス
CREATE INDEX idx_catalog_desc_fts
ON catalog USING gin (to_tsvector('english', description));
-- クエリは同じ設定を使用する必要あり
SELECT name FROM catalog
WHERE to_tsvector('english', description) @@ plainto_tsquery('english', 'red shoes');
Output:
CREATE TABLE
▶ サンプル:格納tsvector列へのGINインデックス
CREATE INDEX idx_catalog_search_fts ON catalog USING gin (search_vector);
-- クエリは格納列を使用
SELECT name FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
Output:
CREATE TABLE
▶ サンプル:tsvector列を自動同期するトリガー
CREATE FUNCTION catalog_search_vector_update() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_catalog_search
BEFORE INSERT OR UPDATE OF name, description ON catalog
FOR EACH ROW EXECUTE FUNCTION catalog_search_vector_update();
Output:
INSERT 0 1
| アプローチ | クエリスタイル | インデックス使用 | メンテナンスコスト |
|---|---|---|---|
| 関数インデックス | to_tsvector('english', col) @@ query |
あり | ゼロ(自動) |
| 格納列+インデックス | stored_col @@ query |
あり | トリガー同期が必要 |
| インデックスなし | to_tsvector('english', col) @@ query |
フルテーブルスキャン | ゼロ |
8. 概念:多言語設定
(1) PostgreSQL組み込み設定
| 設定 | 言語 | 説明 |
|---|---|---|
english |
英語 | デフォルト、ステミングとストップワードをサポート |
simple |
言語処理なし | トークン化のみ、ストップワードなし、ステミングなし |
zhpinyin |
中国語ピンイン | PG拡張、中国語からピンインへの検索 |
▶ サンプル:simple設定はストップワードをフィルタリングしない
SELECT to_tsvector('simple', 'The red shoes');
-- 'red':2 'shoes':3 'the':1('the'を保持、ステミングなし)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル:設定間での検索動作の比較
| 設定 | to_tsvector('the red shoes are nice') | 特徴 |
|---|---|---|
| english | 'nice':5 'red':2 'shoe':3 | ストップワード除去+ステミング |
| simple | 'are':4 'nice':5 'red':2 'shoes':3 'the':1 | 小文字化+トークン化のみ |
| zhpinyin | 拡張の実装に依存 | 中国語からピンイン |
(2) 中国語全文検索オプション
PostgreSQLにはネイティブの中国語トークナイザーが付属していません。一般的なアプローチ:
| アプローチ | 拡張 | トークン化 | インストールの複雑さ |
|---|---|---|---|
| zhparser | PostgreSQL拡張 | SimpleChineseSegベース | 中程度 |
| pg_jieba | PostgreSQL拡張 | jiebaトークナイザーベース | 中程度 |
| zhpinyin | PostgreSQL拡張 | ピンイン検索 | 低い |
| アプリケーション層トークン化 | なし | アプリがtsvectorを生成 | 低い |
▶ サンプル:アプリケーション層トークン化(拡張不要)
-- アプリケーション層で中国語テキストをスペースで事前分割
INSERT INTO catalog (name, description, search_vector)
VALUES (
'Red Running Shoes',
'light red running shoes for jogging',
to_tsvector('simple', 'light red running shoes for jogging')
);
-- 分割されたクエリで検索
SELECT name FROM catalog
WHERE search_vector @@ to_tsquery('simple', 'red & shoes');
Output:
INSERT 0 1
▶ サンプル:中国語と英語の混合検索
CREATE TABLE articles (
article_id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
fts_en tsvector,
fts_zh tsvector
);
-- 英語内容にはenglish設定、中国語にはsimpleを使用
UPDATE articles SET
fts_en = to_tsvector('english', COALESCE(title, '') || ' ' || COALESCE(content, '')),
fts_zh = to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, ''));
-- 英語検索
SELECT title FROM articles
WHERE fts_en @@ plainto_tsquery('english', 'database optimization');
-- 中国語検索(アプリケーション層で分割)
SELECT title FROM articles
WHERE fts_zh @@ to_tsquery('simple', 'data & optimization');
Output:
UPDATE 3
9. 概念:統計とシステムカタログ
(1) ts_statによる単語頻度
ts_stat はtsvector内の語彙素分布を分析するのに役立ち、検索設定のチューニングによく使われます。
▶ サンプル:最も一般的な語彙素を表示
SELECT word, ndoc, nentry
FROM ts_stat(
'SELECT search_vector FROM catalog'
)
ORDER BY nentry DESC
LIMIT 10;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| 列 | 説明 |
|---|---|
word |
語彙素 |
ndoc |
出現するドキュメント数 |
nentry |
総出現回数 |
(2) システムカタログ
▶ サンプル:利用可能な設定を表示
SELECT cfgname FROM pg_ts_config;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル:利用可能な辞書を表示
SELECT dictname, dictnamespace FROM pg_ts_dict;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル:LIKE vs 全文検索のパフォーマンス比較
-- テストデータを作成
INSERT INTO catalog (name, description, search_vector)
SELECT
'Product ' || i,
'High quality product number ' || i || ' with great features and amazing design',
to_tsvector('english', 'High quality product number ' || i || ' with great features and amazing design')
FROM generate_series(1, 100000) AS i;
-- LIKEクエリ(遅い、先頭ワイルドカードではインデックスサポートなし)
EXPLAIN ANALYZE
SELECT name FROM catalog WHERE description LIKE '%great features%';
-- GINインデックス付き全文検索(高速)
CREATE INDEX idx_catalog_fts ON catalog USING gin (search_vector);
EXPLAIN ANALYZE
SELECT name FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'great features');
Output:
INSERT 0 1
| クエリ方法 | 時間 | インデックスサポート | 機能 |
|---|---|---|---|
LIKE '%keyword%' |
約3000ms | なし | 正確な部分文字列一致 |
LIKE 'keyword%' |
約5ms | B-tree | プレフィックス一致 |
@@ 全文検索 + GIN |
約30ms | GIN | 語彙素一致+ステミング+ランキング |
10. 実践:Eコマース商品検索エンジン
Charlieは、複数フィールド検索、ランキング、ハイライト表示、ページネーションを含む完全な商品検索をEコマースプラットフォームに実装する必要があります。
-- ステップ1: 事前計算tsvector付きの商品テーブルを作成
CREATE TABLE shop_products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL,
search_doc tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('english', COALESCE(name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(category, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(description, '')), 'C')
) STORED
);
-- ステップ2: サンプルデータを挿入
INSERT INTO shop_products (name, category, description, price) VALUES
('Red Running Shoes', 'Shoes', 'Lightweight red running shoes with cushioned sole', 89.99),
('Blue Casual Shoes', 'Shoes', 'Comfortable blue shoes for everyday casual wear', 59.99),
('Red Leather Dress Shoes', 'Shoes', 'Premium red leather dress shoes for formal events', 149.99),
('Red Silk Dress', 'Dresses', 'Elegant red silk dress for special occasions', 199.99),
('Running Watch', 'Accessories', 'GPS running watch with heart rate monitor', 249.99),
('Running Shorts', 'Clothing', 'Breathable running shorts with pockets', 39.99);
-- ステップ3: 生成列にGINインデックスを作成
CREATE INDEX idx_shop_products_search ON shop_products USING gin (search_doc);
-- ステップ4: ランキング、ハイライト表示、ページネーション付き検索関数
CREATE FUNCTION search_products(
p_query TEXT,
p_limit INT DEFAULT 20,
p_offset INT DEFAULT 0
) RETURNS TABLE (
product_id INT,
name TEXT,
category TEXT,
price NUMERIC,
rank REAL,
headline TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
s.product_id,
s.name,
s.category,
s.price,
ts_rank(s.search_doc, websearch_to_tsquery('english', p_query)) AS rank,
ts_headline(
'english',
COALESCE(s.name, '') || '. ' || COALESCE(s.description, ''),
websearch_to_tsquery('english', p_query),
'StartSel=<mark>,StopSel=</mark>,MaxWords=20,MinWords=5'
) AS headline
FROM shop_products s
WHERE s.search_doc @@ websearch_to_tsquery('english', p_query)
ORDER BY rank DESC
LIMIT p_limit
OFFSET p_offset;
END;
$$ LANGUAGE plpgsql;
-- ステップ5: 検索をテスト
SELECT * FROM search_products('red shoes');
SELECT * FROM search_products('"running shoes"');
SELECT * FROM search_products('running -shoes');
-- ステップ6: カテゴリフィルター付き検索
SELECT name, price,
ts_rank(search_doc, plainto_tsquery('english', 'red')) AS rank
FROM shop_products
WHERE search_doc @@ plainto_tsquery('english', 'red')
AND category = 'Shoes'
ORDER BY rank DESC;
❓ よくある質問
<-> 演算子で隣接語彙素を照合)と近接検索(`<N>` 距離演算子)に使用されます。フレーズ検索が必要ない場合は、to_tsvectorで位置を削除できます。📖 まとめ
- 全文検索はトークン化、ストップワード除去、ステミングによってテキストをtsvectorに変換し、クエリはtsqueryになります
plainto_tsqueryはユーザー入力に適し、websearch_to_tsquery(PG 11+)は検索エンジン構文をサポートします@@演算子が照合を実行し、ts_rankがランキング、ts_headlineがハイライト表示を行います- GINインデックスは全文検索パフォーマンスの鍵であり、関数インデックスと格納列インデックスにはそれぞれ用途があります
- 重み(setweight A/B/C/D)によりタイトルの一致が本文より上位にランクされます
- 多言語には異なる設定を使用し、中国語にはzhparser/pg_jieba拡張またはアプリケーション層トークン化が必要です
ts_statは単語頻度を分析し、pg_ts_config/pg_ts_dictシステムカタログは設定を一覧表示します
📝 練習問題
-
⭐
title(TEXT)とbody(TEXT)を持つblog_postsテーブルを作成し、3行挿入し、to_tsvector+plainto_tsqueryを使って「データベース」を含む記事を検索してください。 -
⭐⭐
blog_postsにsearch_vector列(tsvector)を追加し、GINインデックスを作成し、INSERT/UPDATE時にsearch_vectorを自動更新するトリガーを書いてください(titleの重みはA、bodyの重みはB)。その後、「performance optimization」をts_rank+ts_headlineで検索し、ランクでソートしてください。 -
⭐⭐⭐ 中国語と英語の混合検索をサポートする
knowledge_articlesテーブルを設計してください:英語コンテンツにはenglish設定、中国語コンテンツにはsimple設定(アプリケーション層で事前分割)、2つのtsvector列と対応するGINインデックスを使用します。入力クエリの言語に基づいて適切な列を選択する検索関数を書き、ts_headlineで結果をハイライト表示し、ts_rankでソートし、ページネーションをサポートしてください。