PostgreSQL: PostgreSQL全文検索エンジン

最終更新:2026-08-26

1. 学習内容


2. ストーリー

CharlieのEコマースプラットフォームには50万件の商品レコードがあり、ユーザーは商品を検索する必要があります。彼は最初 LIKE '%red shoes%' を使いましたが、「red shoes」を検索しても説明文にたまたま連続した文字列「red shoes」が含まれる商品しか返らず、クエリには3秒かかりました。PostgreSQLの全文検索に切り替えた後:


3. 概念:全文検索の中心的概念

(1) 4つの中心的概念

概念 説明 PostgreSQLでの相当物
ドキュメント 検索対象のテキスト内容 tsvector
クエリ ユーザーの検索語 tsquery
辞書 単語の処理方法(ストップワード、ステミング)を定義 pg_ts_dict システムカタログ
設定 パーサーと辞書を組み合わせる pg_ts_config システムカタログ

(2) 全文検索処理パイプライン

100%
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[結果]

▶ サンプル:テキストがどのように処理されるかを確認

SQL
SELECT * FROM ts_debug('english', 'The red shoes are beautiful');
TEXT 📖 参照専用
 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に変換

SQL
SELECT to_tsvector('english', 'The red shoes are beautiful shoes');
TEXT 📖 参照専用
'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を構築

SQL
SELECT to_tsquery('english', 'red & shoe');
-- 'red' & 'shoe'

SELECT to_tsquery('english', 'red | blue');
-- 'red' | 'blue'

Output:

TEXT 📖 参照専用
 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

SQL
SELECT plainto_tsquery('english', 'red shoes');
-- 'red' & 'shoe'(両方の語を含むドキュメントに一致)

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル:phraseto_tsquery — フレーズ検索

SQL
SELECT phraseto_tsquery('english', 'red shoes');
-- 'red' <-> 'shoe'('red'が'shoe'の直前に来るドキュメントに一致)

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル:websearch_to_tsquery — 検索エンジン構文

SQL
SELECT websearch_to_tsquery('english', '"red shoes" -broken OR new');
-- 'red' <-> 'shoe' & !'broken' | 'new'

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
websearch構文 意味
"exact phrase" フレーズ一致(<->)
-word 除外(!)
word1 OR word2 OR(|)
word1 word2 AND(&)

▶ サンプル:@@ 演算子で検索

SQL
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:

TEXT 📖 参照専用
INSERT 0 1

6. 概念:ランキングとハイライト表示

(1) ts_rankランキング

ts_rank はドキュメントに対するクエリの関連度を計算し、0から1のfloatを返します。値が高いほど関連度が高いことを示します。

▶ サンプル:関連度でソート

SQL
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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
ランキング関数 説明 ユースケース
ts_rank(vector, query) 頻度ベースのランク 一般的なランキング
ts_rank_cd(vector, query) カバー密度ランク 短いドキュメントのより正確なランキング
ts_rank(vector, query, weights) 重み付きランク タイトルを本文より高く重み付け

▶ サンプル:重み付きランキング(タイトルを説明より高く重み付け)

SQL
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:

TEXT 📖 参照専用
UPDATE 3
重み文字 デフォルト重量 典型的な用途
A 1.0 タイトル
B 0.4 本文
C 0.2 サマリー
D 0.1 補足情報

(2) ts_headlineハイライト表示

ts_headline は元のテキスト内の一致した語彙素をマークします。

▶ サンプル:検索結果のハイライト表示

SQL
SELECT name,
       ts_headline('english', description, plainto_tsquery('english', 'red shoes')) AS highlighted
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
TEXT 📖 参照専用
 name              | highlighted
-------------------+-------------------------------------------------
 Red Running Shoes | Lightweight <b>red</b> <b>shoes</b> for running

▶ サンプル:カスタムハイライトタグ

SQL
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:

TEXT 📖 参照専用
 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インデックス

SQL
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル:格納tsvector列へのGINインデックス

SQL
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル:tsvector列を自動同期するトリガー

SQL
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:

TEXT 📖 参照専用
INSERT 0 1
アプローチ クエリスタイル インデックス使用 メンテナンスコスト
関数インデックス to_tsvector('english', col) @@ query あり ゼロ(自動)
格納列+インデックス stored_col @@ query あり トリガー同期が必要
インデックスなし to_tsvector('english', col) @@ query フルテーブルスキャン ゼロ

8. 概念:多言語設定

(1) PostgreSQL組み込み設定

設定 言語 説明
english 英語 デフォルト、ステミングとストップワードをサポート
simple 言語処理なし トークン化のみ、ストップワードなし、ステミングなし
zhpinyin 中国語ピンイン PG拡張、中国語からピンインへの検索

▶ サンプル:simple設定はストップワードをフィルタリングしない

SQL
SELECT to_tsvector('simple', 'The red shoes');
-- 'red':2 'shoes':3 'the':1('the'を保持、ステミングなし)

Output:

TEXT 📖 参照専用
 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を生成 低い

▶ サンプル:アプリケーション層トークン化(拡張不要)

SQL
-- アプリケーション層で中国語テキストをスペースで事前分割
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:

TEXT 📖 参照専用
INSERT 0 1

▶ サンプル:中国語と英語の混合検索

SQL
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:

TEXT 📖 参照専用
UPDATE 3

9. 概念:統計とシステムカタログ

(1) ts_statによる単語頻度

ts_stat はtsvector内の語彙素分布を分析するのに役立ち、検索設定のチューニングによく使われます。

▶ サンプル:最も一般的な語彙素を表示

SQL
SELECT word, ndoc, nentry
FROM ts_stat(
  'SELECT search_vector FROM catalog'
)
ORDER BY nentry DESC
LIMIT 10;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
説明
word 語彙素
ndoc 出現するドキュメント数
nentry 総出現回数

(2) システムカタログ

▶ サンプル:利用可能な設定を表示

SQL
SELECT cfgname FROM pg_ts_config;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル:利用可能な辞書を表示

SQL
SELECT dictname, dictnamespace FROM pg_ts_dict;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル:LIKE vs 全文検索のパフォーマンス比較

SQL
-- テストデータを作成
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:

TEXT 📖 参照専用
INSERT 0 1
クエリ方法 時間 インデックスサポート 機能
LIKE '%keyword%' 約3000ms なし 正確な部分文字列一致
LIKE 'keyword%' 約5ms B-tree プレフィックス一致
@@ 全文検索 + GIN 約30ms GIN 語彙素一致+ステミング+ランキング

10. 実践:Eコマース商品検索エンジン

Charlieは、複数フィールド検索、ランキング、ハイライト表示、ページネーションを含む完全な商品検索をEコマースプラットフォームに実装する必要があります。

SQL
-- ステップ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;

❓ よくある質問

Q 全文検索とLIKEの本質的な違いは?
A LIKEは正確な部分文字列一致で、意味を理解しません。全文検索はトークン化、ストップワード除去、ステミングを行い、語彙素による一致とブーリアン組み合わせ、ランキングを提供します。全文検索にはGINインデックスサポートがあり、先頭ワイルドカード付きLIKEはインデックスを使用できません。
Q to_tsqueryとplainto_tsqueryのどちらを使うべき?
A ユーザーが直接検索語を入力する場合はplainto_tsqueryまたはwebsearch_to_tsquery(PG 11+)を使用し、プレーンテキストを自動的にクエリに変換します。to_tsqueryは& | !演算子を手動で記述する必要があり、プログラムで生成されるクエリに適しています。
Q インデックスとクエリで同じ設定を使わなければならないのはなぜ?
A 異なる設定はトークン化とステミングが異なります。インデックスがenglish設定でクエリがsimpleの場合、語彙素が一致せず検索結果が返りません。一貫性を保つ必要があります。
Q GENERATED ALWAYS AS ... STORED列とトリガーはどちらが良い?
A GENERATED列(PG 12+)は自動的に計算され、トリガーが不要で推奨されます。PG 12未満の場合やテーブル間でtsvectorを結合する必要がある場合はトリガーを使用してください。
Q tsvectorの位置情報は何に使われる?
A 位置情報はフレーズ検索(phraseto_tsqueryは <-> 演算子で隣接語彙素を照合)と近接検索(`<N>` 距離演算子)に使用されます。フレーズ検索が必要ない場合は、to_tsvectorで位置を削除できます。
Q GINインデックスは書き込みパフォーマンス��影響しますか?
A はい。GINインデックスの更新は転置リストをマージする必要があるためB-treeより遅くなります。PostgreSQLはデフォルトでFASTUPDATEを有効にし、更新をバッファリングしてから一括マージすることで、クエリのリアルタイム性をわずかに犠牲にして書き込みパフォーマンスを向上させます。
Q 中国語の全文検索はどうすればよい?
A PostgreSQLは中国語トークン化をネイティブサポートしていません。推奨:zhparser/pg_jieba拡張をインストールする(ネイティブトークナイザー)、またはアプリケーション層でトークナイザーを使ってテキストを事前分割し、simple設定でtsvectorに格納します。アプリケーション層アプローチが最もシンプルで拡張のインストールが不要です。

📖 まとめ


📝 練習問題

  1. title(TEXT)と body(TEXT)を持つ blog_posts テーブルを作成し、3行挿入し、to_tsvector + plainto_tsquery を使って「データベース」を含む記事を検索してください。

  2. ⭐⭐ blog_postssearch_vector 列(tsvector)を追加し、GINインデックスを作成し、INSERT/UPDATE時にsearch_vectorを自動更新するトリガーを書いてください(titleの重みはA、bodyの重みはB)。その後、「performance optimization」を ts_rank + ts_headline で検索し、ランクでソートしてください。

  3. ⭐⭐⭐ 中国語と英語の混合検索をサポートする knowledge_articles テーブルを設計してください:英語コンテンツにはenglish設定、中国語コンテンツにはsimple設定(アプリケーション層で事前分割)、2つのtsvector列と対応するGINインデックスを使用します。入力クエリの言語に基づいて適切な列を選択する検索関数を書き、ts_headline で結果をハイライト表示し、ts_rank でソートし、ページネーションをサポートしてください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%