PostgreSQL「syntax error in tsquery」の原因と対処

PostgreSQL「syntax error in tsquery」の原因と対処

全文検索の tsquery 構文が不正なときに出るエラー。未対応の記号を含む文字列を to_tsquery に渡す、演算子の並び/括弧が不正、引用符の使い方違い、ストップワード除去で式が崩れる、プレフィックス指定 :* の誤用、ダブルクオートの取り違えなどが主因。安全なクエリ生成は「ユーザー入力→plainto_tsquery/websearch_to_tsquery」を原則にし、to_tsquery を使うときは自前で無害化・検証する。

エラーの意味と発生条件(tsquery 構文の基本)

tsquery は AND(&) / OR(|) / NOT(!) / 位置演算子(<->) / 括弧() / プレフィックス(:*) を持つブール式。to_tsquery は「すでに構文化された式」を受けるため、未エスケープの記号や不正な並びを含むと「syntax error in tsquery」になる。plainto_tsquery / phraseto_tsquery / websearch_to_tsquery は「自然文→式」を安全に生成するため原則こちらを使う。

最短再現(よくある NG 例)

-- 1) 連続演算子:左辺/右辺がない
SELECT to_tsquery('english', 'cat & & dog');  -- NG: syntax error in tsquery

-- 2) ダブルクオートを to_tsquery に渡す(これは websearch 用の記法)
SELECT to_tsquery('english', '"cat dog"');    -- NG

-- 3) C++ などの記号語をそのまま
SELECT to_tsquery('english', 'c++');          -- NG

-- 4) プレフィックス :* の付け方が不正
SELECT to_tsquery('english', 'postgres:*');   -- 失敗する例あり(書き方注意)

-- 5) 括弧の不一致
SELECT to_tsquery('simple', '(cat | dog');    -- NG

to_tsquery / plainto_tsquery / phraseto_tsquery / websearch_to_tsquery の違い

to_tsquery は「構文済み」。演算子/括弧/プレフィックスを自分で正しく書けるときに使う。plainto_tsquery は空白区切りの単語列を AND で連結して安全に変換。phraseto_tsquery は「語句を隣接(<->)で連結」。websearch_to_tsquery は Google 風(引用符でフレーズ、-で除外、OR など)を安全に tsquery に変換。ユーザー入力には websearch_to_tsquery、少なくとも plainto_tsquery を優先。

ユーザー入力から安全にクエリ化(原則テンプレ)

-- もっとも安全:web 検索風の入力をそのまま受ける
SELECT websearch_to_tsquery('english', ' "cat dog" OR rabbit -mouse ');

-- 単語列なら plainto(AND 連結)
SELECT plainto_tsquery('english', 'cat dog rabbit');

-- 明示フレーズなら phraseto
SELECT phraseto_tsquery('english', 'cat dog');

予約記号の無害化(to_tsquery を使う場合の前処理)

-- 予約記号を空白へ、連続空白を 1 つに
-- & | ! ( ) : < > ' " は to_tsquery で問題になりやすい
SELECT regexp_replace(
         regexp_replace($$C++ (intro) & tips$$, '[&|!():<>''"]', ' ', 'g'),
         '\s+', ' ', 'g');

-- C++ のような記号語は plainto/websearch で吸収するのが無難
SELECT websearch_to_tsquery('english', '"c++"');  -- OK

フレーズ検索・前方一致・ブール演算の正しい書き方

-- フレーズ(to_tsquery を使うなら <-> で連結)
SELECT to_tsquery('english', 'cat<->dog');   -- 「cat」と「dog」が隣接

-- 前方一致(lexeme':* の形。lexeme は基本クオートする)
SELECT to_tsquery('simple', '''postgre'':*');   -- 'postgre':*

-- OR / AND / NOT
SELECT to_tsquery('english', 'cat | dog & !mouse');

括弧と演算子の優先順位(崩れると即エラー)

& は AND、| は OR、! は単項 NOT。! は直後にオペランドが必要で、& や | を続けると不正。括弧は必ず対応させる。ストップワードが消えた結果「演算子の片側が消える」ことがあるため、to_tsquery では特に注意。

ストップワードと空クエリの扱い(落ちない形に整える)

英語の the, a, an などは除去される。to_tsquery に「the &」のような文字列を渡すと、the が消えた結果で構文が壊れてエラーになりやすい。plainto_tsquery/websearch_to_tsquery は自動で安全な式に落とすが、to_tsquery で自前構築するなら「空・片側欠落」を検出して NULL(条件なし)にする分岐を用意する。

実務テンプレ:安全ラッパー関数(空なら NULL)

-- ユーザー入力を websearch→(失敗時)plain にフォールバックし、空は NULL
CREATE OR REPLACE FUNCTION safe_tsquery(conf regconfig, q text)
RETURNS tsquery
LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE t tsquery;
BEGIN
  IF q IS NULL OR btrim(q) = '' THEN
    RETURN NULL;
  END IF;
  BEGIN
    t := websearch_to_tsquery(conf, q);
  EXCEPTION WHEN syntax_error THEN
    t := plainto_tsquery(conf, q);
  END;
  IF t::text = '' THEN
    RETURN NULL;
  END IF;
  RETURN t;
END$$;

-- 利用例
SELECT to_tsvector('english', title || ' ' || body) @@ safe_tsquery('english', :q);

デバッグ:どこで壊れているかを特定(ts_debug/::text)

-- トークナイズ/辞書適用の結果を確認
SELECT * FROM ts_debug('english', 'C++ tips for the cat and the dog');

-- tsquery の文字列表現を確認(演算子の付き方を可視化)
SELECT to_tsquery('english', 'cat & dog | !mouse')::text;

インデックスと検索の基本形(GIN/GiST + tsvector)

-- 1) tsvector を作って GIN インデックス
CREATE INDEX idx_docs_fts
ON docs USING GIN (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,'')));

-- 2) 検索は @@ 演算子で
SELECT id, title
FROM docs
WHERE to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,'')) @@
      websearch_to_tsquery('english', :q)
ORDER BY ts_rank_cd(to_tsvector('english', title || ' ' || body),
                    websearch_to_tsquery('english', :q)) DESC
LIMIT 50;

エラーを起こす典型パターン→書き換え(NG→OK 早見)

-- NG: ユーザー入力をそのまま to_tsquery
SELECT to_tsquery('english', :q);                 -- ❌

-- OK: websearch_to_tsquery / plainto_tsquery
SELECT websearch_to_tsquery('english', :q);       -- ✅

-- NG: "ダブルクオート" を to_tsquery に渡す
SELECT to_tsquery('english', '"cat dog"');        -- ❌

-- OK: フレーズは phraseto_tsquery か websearch_to_tsquery
SELECT phraseto_tsquery('english', 'cat dog');    -- ✅

-- NG: プレフィックスの書式
SELECT to_tsquery('simple', 'postgres:*');        -- ❌(lexeme の指定が不正)

-- OK: lexeme':* 形式(シングルクオート)
SELECT to_tsquery('simple', '''postgres'':*');    -- ✅  'postgres':*

入力正規化のクイックレシピ(Bash/SQL)

-- Bash: ユーザー入力らしき文字列から危険記号を空白化して渡す(最終手段)
echo "$q" | sed 's/[&|!():"'\'']\+//g' | tr -s ' ' | psql -v q -c \
  "SELECT websearch_to_tsquery('english', :'q');"

-- SQL: 記号を取り除いた上で plainto に渡す(安全優先)
WITH cleaned AS (
  SELECT regexp_replace(:q, '[&|!():"'\'']', ' ', 'g') AS s
)
SELECT plainto_tsquery('english', s) FROM cleaned;

エラーを握り潰さず運用で防ぐ(アプリ側の要点)

ユーザー入力→そのまま to_tsquery を禁止。websearch_to_tsquery をデフォルトにし、失敗や空(tsquery::text = ”)は条件をスキップするか 400 を返す。検索ボックスに「フレーズは引用符、除外は -、OR は大文字」などの簡易ヘルプを併記して無効な文字の混入を抑える。

テスト観点(回帰しやすい境界ケース)

C++/C#、メールアドレス、URL、括弧/記号だらけの文字列、ダブルクオート含み、片括弧、末尾が演算子、ストップワードだけ、全角記号混在、日本語と英数字の境界。これらを websearch_to_tsquery と plainto_tsquery の双方で通し、意図通りの tsquery になるかを確認する。