PostgreSQL『syntax error in tsquery』の原因と対処(全文検索/tsquery)
- 作成日 2025.09.08
- 更新日 2025.10.06
- PostgreSQL
- PostgreSQL
to_tsquery/phraseto_tsquery/websearch_to_tsquery 等で作る tsquery の構文が不正なときに出る。代表パターン(演算子の並び・未対応の記号・不一致なカッコ・ストップワードで空オペランド化)を網羅し、最短復旧、堅牢なユーザー入力処理、デバッグ手順、再発防止の設計指針をまとめた。
目次
- 1. エラーの意味と代表メッセージ
- 2. まず把握:tsquery の基本文法と許可トークン
- 3. 典型的な発生条件(チェックリスト)
- 4. 最短復旧:ユーザー入力は websearch_to_tsquery か plainto_tsquery に切替
- 5. 再現と修正のミニ例(NG→OK)
- 6. 未対応記号の扱い(C++ / C# / e-mailなど)
- 7. フレーズ検索は <-> か phraseto_tsquery を使う
- 8. 辞書/ストップワードの影響で空になったときの扱い
- 9. 安全な入力処理レシピ(SQLのみで完結)
- 10. to_tsquery を使う「機械生成」パターン(構文を自前で保証)
- 11. デバッグ手順:どこで壊れているかを特定
- 12. インデックスとパフォーマンス(GIN/tsvector)
- 13. アプリ言語からの安全実装(例:Node/Python)
- 14. 再発防止チェックリスト
- 15. 通し例:壊れやすい入力を安全に検索(C++ / “foo bar” / -baz)
エラーの意味と代表メッセージ
ERROR: syntax error in tsquery: "foo & & bar"
ERROR: syntax error in tsquery: "foo |"
ERROR: syntax error in tsquery: "(foo & bar"
ERROR: syntax error in tsquery: "the & and"・tsquery の構文(演算子/トークン/括弧)が壊れている
・左/右オペランドの欠落、未対応文字、括弧不一致、辞書処理後に“空”になってしまう等が主因
まず把握:tsquery の基本文法と許可トークン
-- 演算子の例
a & b -- AND
a | b -- OR
!a -- NOT
a <-> b -- 隣接(語順/距離演算子:a と b が隣接)
a:* -- prefix マッチ("a..."で始まる語)
(a & b) | c -- 括弧で優先順位制御
-- 入力関数の違い(安全性と表現力)
to_tsquery('simple', 'a & b') -- 厳格な構文を要求(主に機械生成向け)
plainto_tsquery('simple', 'a b') -- 単語列を安全に AND 連結
phraseto_tsquery('simple', 'a b') -- 連語(a <-> b <-> ...)に展開
websearch_to_tsquery('simple', 'a "b c" -d OR e') -- Web検索風構文を安全に解釈典型的な発生条件(チェックリスト)
□ "&" や "|" が連続/末尾/先頭にある(例: "a & & b", "a |")
□ 未対応記号をそのまま渡している(例: "C++ & C#" を to_tsquery に直渡し)
□ 括弧の不一致("(" と ")" の数/位置が合わない)
□ 演算子の前後どちらかが空(ストップワード除去後に "the & and" など)
□ to_tsquery にユーザー文字列を直結(意図せず演算子扱い)
□ ダブルクオートで「フレーズ検索」を期待(to_tsquery は理解しない)
□ prefix の :* の付け方が誤り("a:*:*" 等)最短復旧:ユーザー入力は websearch_to_tsquery か plainto_tsquery に切替
-- ユーザーの自由入力はまずこちら(Web検索風を安全解釈)
SELECT websearch_to_tsquery('simple', :q);
-- “語の AND 列”として扱えばよいなら
SELECT plainto_tsquery('simple', :q);
-- 連語を重視するなら
SELECT phraseto_tsquery('simple', :q);・to_tsquery は厳格構文のため、人間が打つ文字列をそのまま渡すのは危険
・機械的に a & b & c… を組み立てる用途でのみ to_tsquery を使う
再現と修正のミニ例(NG→OK)
-- 1) 演算子が連続
SELECT to_tsquery('simple', 'foo & & bar'); -- NG: syntax error
SELECT to_tsquery('simple', 'foo & bar'); -- OK
-- 2) 末尾が演算子
SELECT to_tsquery('simple', 'foo |'); -- NG
SELECT to_tsquery('simple', 'foo | bar'); -- OK
-- 3) 括弧不一致
SELECT to_tsquery('simple', '(foo & bar'); -- NG
SELECT to_tsquery('simple', '(foo & bar)'); -- OK
-- 4) ストップワードで空オペランド化(英語辞書で "the","and" は除去)
SELECT to_tsquery('english', 'the & and'); -- NG
SELECT plainto_tsquery('english', 'the and'); -- OK: 空になったら ' を返すことも
未対応記号の扱い(C++ / C# / e-mailなど)
-- NG: 記号を含む語を to_tsquery に直渡し
SELECT to_tsquery('simple', 'C++ & C#'); -- NG: syntax error の典型
-- OK: ユーザー入力は websearch_to_tsquery へ
SELECT websearch_to_tsquery('simple', 'C++ AND "C#"');
-- どうしても to_tsquery を使う場合は正規化(記号除去/置換)してから
SELECT to_tsquery('simple', 'c & c'); -- 例:正規化後・辞書/構成により “+” や “#” は分割/除去されることが多い
・「正規化→to_tsquery」より「そのまま→websearch_to_tsquery」が堅牢
フレーズ検索は <-> か phraseto_tsquery を使う
-- NG: to_tsquery は "foo bar"(ダブルクオート)をフレーズと解釈しない
SELECT to_tsquery('simple', '"foo bar"'); -- NG: syntax error(または期待と違う)
-- OK: 距離演算子 <->(隣接)で表現
SELECT to_tsquery('simple', 'foo <-> bar');
-- 連語は phraseto_tsquery が簡単
SELECT phraseto_tsquery('simple', 'foo bar'); -- = 'foo' <-> 'bar'辞書/ストップワードの影響で空になったときの扱い
-- “英語辞書”で the/and は除去 → 演算子だけが残ると構文エラー
SELECT to_tsquery('english', 'the & report'); -- the が消えても OK(右が残る)
SELECT to_tsquery('english', 'the & and'); -- 両方消えて NG
-- 事前ガード:空なら全文検索をスキップ
WITH q AS (SELECT websearch_to_tsquery('english', :q) AS tsq)
SELECT *
FROM docs, q
WHERE q.tsq <> ''::tsquery -- tsq が空('')なら検索条件を外す など安全な入力処理レシピ(SQLのみで完結)
-- 1) ユーザー入力を Web 検索構文で解釈
WITH q AS (
SELECT websearch_to_tsquery('simple', :q) AS tsq
)
SELECT id, title, ts_rank_cd(to_tsvector('simple', body), tsq) AS rank
FROM articles, q
WHERE to_tsvector('simple', body) @@ tsq
ORDER BY rank DESC
LIMIT 20;
-- 2) 空/危険入力のガード(tsq = '' を弾く)
...
WHERE q.tsq <> ''::tsquery・アプリ側で“予約記号”を消すより、websearch_to_tsquery で自然に解釈させる方が事故が少ない
to_tsquery を使う「機械生成」パターン(構文を自前で保証)
-- 例:AND 連結だけを機械的に構築
WITH terms AS (SELECT unnest(ARRAY['postgresql','tsquery','syntax']) AS t)
SELECT to_tsquery('simple',
string_agg(quote_ident(t) || ':*', ' & ' ORDER BY t)
-- ↑ prefix 検索を付けたい場合の一例(:*は語に付ける)
) AS tsq;・terms が空なら ”::tsquery にする処理を入れる
・ユーザー入力をこの経路に通す場合はトークン化/正規化済みであること
デバッグ手順:どこで壊れているかを特定
-- 1) その辞書でのトークン化を観察
SELECT ts_debug('english', :text); -- どの語が除去/保持されたか
-- 2) 生成クエリを可視化
SELECT to_tsvector('english', :text);
SELECT websearch_to_tsquery('english', :q);
SELECT phraseto_tsquery('english', :q);
-- 3) インデックス統計
EXPLAIN ANALYZE
SELECT * FROM docs WHERE to_tsvector('english', body) @@ websearch_to_tsquery('english', :q);インデックスとパフォーマンス(GIN/tsvector)
-- 典型的な GIN インデックス
CREATE INDEX IF NOT EXISTS idx_docs_body_fts
ON docs USING GIN (to_tsvector('simple', body));
-- @@ に合わせる(辞書/構成を揃える)
SELECT * FROM docs
WHERE to_tsvector('simple', body) @@ websearch_to_tsquery('simple', :q)
ORDER BY ts_rank_cd(to_tsvector('simple', body),
websearch_to_tsquery('simple', :q)) DESC;・to_tsvector と tsquery の「構成名(辞書)」は一致させる
・構成が違うとヒットしない/遅い/思わぬ除去が起きる
アプリ言語からの安全実装(例:Node/Python)
// Node (node-postgres)
const q = req.query.q ?? '';
const sql = `
WITH x AS (SELECT websearch_to_tsquery('simple', $1) AS tsq)
SELECT id, title
FROM articles, x
WHERE x.tsq <> ''::tsquery
AND to_tsvector('simple', body) @@ x.tsq
ORDER BY ts_rank_cd(to_tsvector('simple', body), x.tsq) DESC
LIMIT 20
`;
const rows = await client.query(sql, [q]);
# Python (psycopg)
q = request.args.get("q", "")
sql = """
WITH x AS (SELECT websearch_to_tsquery('simple', %s) AS tsq)
SELECT id, title
FROM articles, x
WHERE x.tsq <> ''::tsquery
AND to_tsvector('simple', body) @@ x.tsq
ORDER BY ts_rank_cd(to_tsvector('simple', body), x.tsq) DESC
LIMIT 20
"""
cur.execute(sql, (q,))再発防止チェックリスト
□ ユーザー入力 → websearch_to_tsquery / phraseto_tsquery / plainto_tsquery を使用
□ to_tsquery を使う場合は機械生成のみ(構文保証済み)
□ 空 tsquery('')のときの分岐を実装(検索スキップ/全件抑止)
□ to_tsvector と tsquery の構成名(辞書)を統一
□ 未対応記号を“消す”より、ファンクションに任せる(正規化は最小限)
□ フレーズ検索は phraseto_tsquery か <-> を使用
□ デバッグ時は ts_debug で辞書の動作を観察通し例:壊れやすい入力を安全に検索(C++ / “foo bar” / -baz)
-- 対象テーブル
CREATE TABLE docs(id serial PRIMARY KEY, body text);
CREATE INDEX IF NOT EXISTS idx_docs_fts
ON docs USING GIN (to_tsvector('simple', body));
-- サンプルデータ
INSERT INTO docs(body) VALUES
('C++ で foo bar を処理する'),
('C# で foo を試す'),
('baz を含む別記事');
-- NG:to_tsquery に直渡し(エラーになり得る)
-- SELECT * FROM docs
-- WHERE to_tsvector('simple', body) @@ to_tsquery('simple', 'C++ "foo bar" -baz');
-- OK:websearch_to_tsquery に任せる
SELECT id, body
FROM docs
WHERE to_tsvector('simple', body) @@ websearch_to_tsquery('simple', 'C++ "foo bar" -baz')
ORDER BY ts_rank_cd(to_tsvector('simple', body),
websearch_to_tsquery('simple', 'C++ "foo bar" -baz')) DESC;
-
前の記事
PostgreSQL『current transaction is aborted, commands ignored until end of transaction block』エラーの原因と対処 2025.09.05
-
次の記事
PostgreSQL『ERROR: operator does not exist』の原因と対処 2025.09.09
コメントを書く