pgsearch 拡張機能は、AnalyticDB for PostgreSQL に BM25 ベースの全文検索を提供します。高性能検索エンジンである Tantivy 上で構築されており、Elasticsearch が採用するのと同じアルゴリズムで検索結果を関連性に基づいてランキングします。外部検索サービスを必要とせず、SQL から直接大規模テーブル内のキーワードやフレーズを検索できます。
前提条件
作業を開始する前に、以下の要件を満たしていることを確認してください。
AnalyticDB for PostgreSQL インスタンス
送信済みのチケット(pgsearch 拡張機能のインストールにはコンソールからの操作は利用できません)
拡張機能のインストール後にインスタンスを再起動済みであること
pgsearch 拡張機能をアンインストールする場合は、チケットを送信してください。
クイックスタート
このセクションでは、テストテーブルの作成、BM25 インデックスの構築、および最初の検索実行までの一連のワークフローを説明します。
1. テストテーブルを作成します。
CALL pgsearch.create_test_table(table_name => 'mock_items', schema_name => 'public');これにより、次のスキーマを持つ mock_items テーブルが作成されます。
CREATE TABLE mock_items (
description TEXT,
rating INTEGER CHECK (rating BETWEEN 1 AND 5),
category VARCHAR(255),
in_stock BOOLEAN,
metadata JSONB,
created_at TIMESTAMP,
last_updated_date DATE,
latest_available_time TIME
);2. BM25 インデックスを作成します。
CALL pgsearch.create_bm25(
index_name => 'search_idx',
table_name => 'mock_items',
text_fields => '{description: {tokenizer: {type: "jieba"}}, category: {}}',
datetime_fields => '{created_at: {}, last_updated_date: {}}',
numeric_fields => '{rating: {}}',
json_fields => '{metadata: {tokenizer: {type: "en_stem"}}}',
boolean_fields => '{in_stock: {}}'
);3. 検索を実行します。
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:keyboard')
LIMIT 10;結果は BM25 スコアに基づいてランキングされ、最も関連性の高い行が先頭に表示されます。
BM25 インデックスの作成
pgsearch.create_bm25() を呼び出してインデックスを作成します。1 つのテーブルに対して BM25 インデックスは 1 つだけ作成してください。複数のインデックスを作成するとリソースを過剰に消費します。
インデックスの作成および削除はロールバックできません。
構文
CALL pgsearch.create_bm25(
index_name => '<index_name>',
table_name => '<table_name>',
schema_name => '<schema_name>',
text_fields => '<text_fields> | pgsearch.field()',
numeric_fields => '<numeric_fields> | pgsearch.field()',
boolean_fields => '<boolean_fields> | pgsearch.field()',
json_fields => '<json_fields> | pgsearch.field()',
datetime_fields => '<datetime_fields> | pgsearch.field()'
)パラメーター
| パラメーター | 必須 | サポートされるデータの型 | 説明 |
|---|---|---|---|
index_name | はい | STRING | インデックスの名前。 |
table_name | はい | STRING | インデックス対象のテーブル名。 |
schema_name | いいえ | STRING | テーブルのスキーマ。デフォルトは現在のスキーマ。 |
text_fields | はい(少なくとも 1 つ) | VARCHAR, VARCHAR[], TEXT, TEXT[] | インデックス対象のテキストフィールド。フィールドごとの設定が可能です。JSON5 文字列または pgsearch.field() を使用します。 |
numeric_fields | いいえ | INT2, INT2[], INT4, INT4[], INT8, INT8[], OID, OID[], XID, XID[], FLOAT4, FLOAT4[], FLOAT8, FLOAT8[], NUMERIC, NUMERIC[] | インデックス対象の数値フィールド。 |
boolean_fields | いいえ | BOOLEAN, BOOLEAN[] | インデックス対象のブール値フィールド。 |
json_fields | いいえ | JSON, JSONB | インデックス対象の JSON フィールド。JSON 値内のネストされたテキストフィールドに対する全文検索をサポートします。 |
datetime_fields | いいえ | DATE, DATE[], TIMESTAMP, TIMESTAMP[], TIMESTAMPTZ, TIMESTAMPTZ[], TIME, TIME[], TIMETZ, TIMETZ[] | インデックス対象の日時フィールド。デフォルトで UTC タイムゾーンおよび RFC 3339 フォーマットを使用します。 |
フィールド設定オプション
各フィールドには、JSON5 設定文字列または pgsearch.field() を指定できます。以下のオプションが適用されます。
| オプション | 説明 | デフォルト |
|---|---|---|
fast | スコアリングおよびフィルタリングのための高速ランダムアクセスを有効にします。 | false はテキストおよび JSON フィールド用;true は数値、ブール、日時 フィールド用 |
fieldnorms | フィールド長を格納します。BM25 スコア計算に必要です。 | true |
tokenizer | トークナイザーおよびその設定。詳細については、「トークナイザー」をご参照ください。 | — |
record | インデックスレコードタイプ:raw、freq、または position。position を指定するとフレーズ検索が可能になります。 | position |
expand_dots | (JSON フィールドのみ)ピリオドを含む JSON キーを展開します。たとえば、{"metadata.color": "red"} は {"metadata": {"color": "red"}} としてインデックスされます。 | true |
pgsearch.field() 関数
pgsearch.field() は、単一フィールドのインデックス設定を生成します。複数のフィールドを設定するには、|| で複数の呼び出しを連結します。
pgsearch.field(<name>, <fast>, <fieldnorms>, <record>, <expand_dots>, <tokenizer>)AnalyticDB for PostgreSQL V7.2.1.0 以降では、インデックス作成時に pgsearch.field() を使用する必要があります。これを行わないと、Jieba トークナイザー用のカスタム分かち書き辞書およびストップワード辞書が有効になりません。
トークナイザー
pgsearch 拡張機能には、英語・中国語・韓国語・日本語向けの組み込みトークナイザーが含まれています。追加の拡張機能は不要です。
| トークナイザー | 説明 | 設定 |
|---|---|---|
default | スペースおよび句読点で分割し、小文字に変換し、255 バイトを超えるトークンを除外します。 | {type: "default"} |
raw | フィールド値全体を 1 つのトークンとして扱います。 | {type: "raw"} |
en_stem | スペースおよび句読点で分割し、小文字に変換し、英語の語幹処理を適用します。40 文字を超えるトークンを除外します。英語コンテンツを含む JSON フィールドに使用します。 | {type: "en_stem"} |
whitespace | 空白文字でのみ分割します。 | {type: "whitespace"} |
ngram | n-gram シーケンスを生成します。パラメーター:min_gram、max_gram、prefix_only。 | {type: "ngram", min_gram: 1, max_gram: 2, prefix_only: true} |
chinese_compatible | スペースおよび句読点で分割します。各中国語文字は個別のトークンとなり、連続する非中国語文字は 1 つのトークンを形成します。例:"我爱吃橙子 oranges!12" → 我, 爱, 吃, 橙, 子, oranges, 12。 | {type: "chinese_compatible"} |
chinese_lindera | CC-CEDICT 辞書を使用する中国語トークナイザー。 | {type: "chinese_lindera"} |
korean_lindera | KoDic 辞書を使用する韓国語トークナイザー。 | {type: "korean_lindera"} |
japanese_lindera | IPADIC 辞書を使用する日本語トークナイザー。 | {type: "japanese_lindera"} |
jieba | jieba ライブラリを使用する中国語トークナイザー。カスタム分かち書き辞書およびストップワード辞書をサポートします。追加パラメーター:hmm(未知語に対する隠れマルコフモデル。デフォルトは true)、search(詳細な分かち書き。デフォルトは true)。詳細については、「カスタム分かち書き辞書の設定」および「ストップワード辞書の設定」をご参照ください。 | {type: "jieba", hmm: true, search: true} |
Jieba トークナイザーの hmm および search パラメーターは、AnalyticDB for PostgreSQL V7.2.1.0 以降で利用可能です。
インデックス作成前にトークナイザーの出力を確認してください。
pgsearch.tokenizer() を使用して、インデックス作成前にトークナイザーがテキストをどのようにセグメント化するかを確認できます。
-- Jieba が中国語テキストをどのようにセグメント化するかを確認
SELECT pgsearch.tokenizer('{type: "jieba"}', '数据仓库');
-- ngram がテキストをどのようにセグメント化するかを確認
SELECT pgsearch.tokenizer('{type: "ngram", min_gram: 1, max_gram: 2, prefix_only: true}', 'hell');pgsearch.tokenizer() 関数
pgsearch.tokenizer() はトークナイザー設定文字列を生成します。pgsearch.field() と組み合わせて使用することで、トークナイザーやステミング、ストップワード除去などのフィルターを適用できます。
pgsearch.tokenizer(<name>, <min_gram>, <max_gram>, <prefix_only>, <search>, <hmm>, <dict>, <stopword>, <lowercase>, <remove_long>, <stemmer>)| パラメーター | 説明 |
|---|---|
name | トークナイザー名。有効な値については上記のトークナイザーテーブルをご参照ください。 |
min_gram、max_gram、prefix_only | ngram トークナイザーのパラメーター。 |
search、hmm | Jieba トークナイザーのパラメーター。 |
dict | Jieba 用のカスタム分かち書き辞書名。「カスタム分かち書き辞書の設定」をご参照ください。 |
stopword | ストップワード辞書名。「ストップワード辞書の設定」をご参照ください。 |
lowercase | キーワードを小文字に変換します。デフォルト:true。 |
remove_long | バイト長がこの値以上になるトークンを除外します。中国語文字 1 文字は 3 バイトです。 |
stemmer | 語幹を抽出します。英語の場合は en を設定します。例:run、running、runs、ran はすべて run に変換されます。 |
AnalyticDB for PostgreSQL V7.2.1.0 以降では、Jieba トークナイザー用のカスタム分かち書き辞書およびストップワード辞書を適用するには、pgsearch.tokenizer() を使用する必要があります。
使用例
JSON5 設定文字列を使用する場合:
-- ngram トークナイザー — すべてのパラメーターを指定する必要があります
CALL pgsearch.create_bm25(
index_name => 'search_idx',
table_name => 'mock_items',
text_fields => '{description: {tokenizer: {type: "ngram", min_gram: 2, max_gram: 3, prefix_only: false}}}'
);
-- Jieba トークナイザー
CALL pgsearch.create_bm25(
index_name => 'search_idx',
table_name => 'mock_items',
text_fields => '{description: {tokenizer: {type: "jieba"}}}'
);
-- 複数のフィールドタイプをインデックス
CALL pgsearch.create_bm25(
index_name => 'search_idx',
table_name => 'mock_items',
text_fields => '{description: {tokenizer: {type: "jieba"}}, category: {}}',
datetime_fields => '{created_at: {}, last_updated_date: {}}',
numeric_fields => '{rating: {}}',
json_fields => '{metadata: {tokenizer: {type: "en_stem"}}}',
boolean_fields => '{in_stock: {}}'
);pgsearch.field() および pgsearch.tokenizer() を使用する場合(V7.2.1.0 以降で必須):
-- description フィールドを高速アクセス、position レコード、およびカスタム辞書と組み込み CN_SIMPLE ストップワードを使用する Jieba トークナイザーで設定します。
-- 複数のフィールド設定は || で連結します。
CALL pgsearch.create_bm25(
index_name => 'search_idx',
table_name => 'mock_items',
text_fields => pgsearch.field('description',
fast => true,
record => 'position',
tokenizer => pgsearch.tokenizer('jieba',
search => false,
dict => 'user_dict',
stopword => 'CN_SIMPLE'))
|| pgsearch.field('category')
);
-- フィルターを適用した Jieba トークナイザーの出力を確認
SELECT pgsearch.tokenizer(
pgsearch.tokenizer('jieba',
search => false, dict => 'user_dict', stopword => 'CN_SIMPLE',
lowercase => false, remove_long => 27, stemmer => 'en')::text,
'永和服装饰品有限公司。 Shoping'
);検索構文
すべての検索は @@@ 演算子と pgsearch.config() を使用します。結果は BM25 スコアの降順でソートされます。
SELECT * FROM <table_name>
ORDER BY <index_col> @@@ pgsearch.config('<query>')
LIMIT <n>;<index_col>:インデックスフィールド名。検索キーワードの対象となるフィールドを指定します。<query>:検索式。文字列を渡すと、pgsearch.parse()が暗黙的に解析を行います。
以下の 2 つの文は同等です。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config('description:socks');
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(query => pgsearch.parse('description:socks'));基本的なクエリ
テキストフィールドの検索
単語またはフレーズを検索します。複数語からなるフレーズは二重引用符で囲んでください。
-- 「keyboard」という単語を検索
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:keyboard');
-- 「hello world」という正確なフレーズを検索
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:"hello world"');JSON フィールドの検索
-- metadata に {"color": "white"} を含む行を検索
SELECT * FROM mock_items
ORDER BY metadata @@@ pgsearch.config('metadata.color:white');日時フィールドの検索
日時フィールドは UTC タイムゾーンおよび RFC 3339 フォーマットを使用します。
SELECT * FROM mock_items
ORDER BY created_at @@@ pgsearch.config('created_at:"2023-05-01T09:12:34Z"')
LIMIT 10;数値およびブール値フィールドのフィルタリング
数値およびブール値フィールドのフィルター式は pgsearch.config() 内で使用します。インデックスフィールドに対するフィルターは、標準 SQL の WHERE 句よりも高速です。
-- description の全文検索を行い、rating が 4 未満にフィルタリング
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:keyboard AND rating:<4');近接検索
~N を使用して、最大 N 語離れた語をマッチさせます。
-- 「ergonomic keyboard」の間に最大 1 語を挟んでマッチ
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:"ergonomic keyboard"~1');ブール演算子
AND、OR、および NOT を使用して検索語を組み合わせます。条件をグループ化するには括弧を使用します。
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:keyboard OR category:toy');ランキング強化
^ の後に倍率を指定して、特定の語の BM25 スコアを強化し、該当行を検索結果の上位に表示できます。
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:keyboard^2 OR category:electronics^3');セット検索(IN 演算子)
IN 演算子は複数の OR 条件と同等ですが、CPU 使用量が少ないです。各フレーズは 1 つのトークンを生成する必要があります。
IN は英語でのみ使用してください。中国語の分かち書きではトークン数が予測できないため、トークンレベルでの IN は信頼性が低くなります。中国語の場合は OR を使用してください。
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:IN [keyboard toy]');ページネーション
SELECT * FROM mock_items
ORDER BY description @@@ pgsearch.config('description:socks')
OFFSET 2 LIMIT 10;高度なクエリ
タームセット検索
配列内のいずれかのタームに一致する行を検索します。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.term_set(
terms => ARRAY[
pgsearch.term(field => 'description', value => 'socks'),
pgsearch.term(field => 'description', value => 'novel')
]
)
);トークナイザーはインデックスされたキーワードを小文字に変換します。pgsearch.term() では小文字の値を使用してください。
| パラメーター | 説明 |
|---|---|
field | 検索対象フィールド。すべてのインデックスフィールドを対象にする場合は空欄のままにします。 |
terms | マッチさせるタームの配列。 |
フレーズ検索
指定された順序でキーワード配列を含む行を検索します。インデックスの record オプションを position に設定する必要があります。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.phrase(
field => 'description',
phrases => ARRAY['little', 'red', 'riding', 'hood'],
slop => 0
)
);| パラメーター | 説明 |
|---|---|
field | 検索対象フィールド。すべてのインデックスフィールドを対象にする場合は空欄のままにします。 |
phrases | 順序付きキーワード配列。すべてのキーワードが同じ順序で行内に存在する必要があります。 |
slop | 隣接するキーワード間の最大距離。0 はキーワードが連続していることを意味します。0 より大きい値は、その間に他の語を許容します。 |
フレーズプレフィックス検索
指定された順序のキーワードシーケンスで始まる行を検索します。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.phrase_prefix(
field => 'description',
phrases => ARRAY['little', 're'],
max_expansion => 1
)
);| パラメーター | 説明 |
|---|---|
field | 検索対象フィールド。すべてのインデックスフィールドを対象にする場合は空欄のままにします。 |
phrases | プレフィックスキーワード配列。例:ARRAY['little', 're'] は「little red riding hood」にマッチします。 |
max_expansion | プレフィックスから展開されるタームバリエーションの最大数。検索範囲を絞り込みます。 |
トークナイザーターム検索
指定されたトークナイザーでクエリをセグメント化し、生成されたトークンを検索します。トークン間でブール論理をサポートします。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
pgsearch.tokenizer_terms('description', '朝阳百货', pgsearch.tokenizer('jieba'), 'OR')
);| パラメーター | 説明 |
|---|---|
tokenizer | pgsearch.tokenizer() からのトークナイザー設定文字列。デフォルト:jieba。 |
operator | トークン間のブール演算子。OR はいずれかのトークンにマッチする行を返します。AND はすべてのトークンが必要です。デフォルト:OR。 |
ファジーターム検索
指定された編集距離(レーベンシュタイン距離)内で語をマッチさせ、スペルミスがあっても結果を返します。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.fuzzy_term(
field => 'description',
value => 'wow',
distance => 2,
tranposition_cost_one => true,
prefix => true
)
);| パラメーター | 説明 |
|---|---|
field | 検索対象フィールド。すべてのインデックスフィールドを対象にする場合は空欄のままにします。 |
value | 検索キーワード。ファジー一致はレーベンシュタイン距離を使用します。 |
distance | 最大編集距離。デフォルト:2。最大値:2。 |
tranposition_cost_one | 隣接する 2 文字の入れ替えを 2 回の編集ではなく 1 回としてカウントします。デフォルト:true。 |
prefix | キーワードのプレフィックスを編集距離計算から除外します。デフォルト:true。 |
範囲検索
数値または日時の範囲内の値を検索します。
SELECT * FROM mock_items ORDER BY rating @@@ pgsearch.config(
query => pgsearch.range(
field => 'rating',
range => '[1,4)'::int4range
)
);| パラメーター | 説明 |
|---|---|
field | 検索対象フィールド。すべてのインデックスフィールドを対象にする場合は空欄のままにします。 |
range | 値の範囲。サポートされる型:INT4RANGE、INT8RANGE、DATERANGE、TSRANGE、TSTZRANGE。 |
正規表現検索
正規表現パターンに一致する語を含む行をマッチさせます。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.regex(
field => 'description',
pattern => '(glass|screen|like|cloth|phone)'
)
);| パラメーター | 説明 |
|---|---|
field | 検索対象フィールド。すべてのインデックスフィールドを対象にする場合は空欄のままにします。 |
pattern | 正規表現パターン。 |
ブールクエリ
must、must_not、および should 条件を使用してサブクエリを組み合わせます。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.boolean(
should => ARRAY[
pgsearch.parse('description:socks'),
pgsearch.phrase_prefix(field => 'description', phrases => ARRAY['book']),
pgsearch.term(field => 'description', value => 'writer'),
pgsearch.fuzzy_term(field => 'description', value => 'wow')
],
must_not => ARRAY[
pgsearch.term(field => 'description', value => 'writer')
],
must => ARRAY[
pgsearch.term(field => 'rating', value => 4)
]
)
);| パラメーター | 説明 |
|---|---|
must | マッチする行すべてが満たす必要がある条件。 |
must_not | マッチ時に行を除外する条件。 |
should | must が設定されていない場合、少なくとも 1 つがマッチする必要がある条件。 |
ランキングブースト
サブクエリの BM25 スコアに定数を乗算して、その結果をランキングの上位に表示します。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.boost(query => pgsearch.parse('description:socks'), boost => 2)
);| パラメーター | 説明 |
|---|---|
boost | 各結果スコアに適用される倍率。 |
query | ブースト対象のサブクエリ。 |
定数スコア検索
サブクエリに一致するすべての行に固定スコアを割り当て、BM25 スコア計算をバイパスします。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.const_score(query => pgsearch.all(), score => 2)
);| パラメーター | 説明 |
|---|---|
score | 各マッチ行に割り当てられる固定スコア。 |
query | 評価対象のサブクエリ。 |
最大選言検索
複数のサブクエリに対して行をマッチさせます。より多くのサブクエリに一致する行ほど高いスコアになります。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.disjunction_max(
disjuncts => ARRAY[
pgsearch.parse('description:socks'),
pgsearch.parse('description:Generic')
],
tie_breaker => 0.75
)
);| パラメーター | 説明 |
|---|---|
disjuncts | サブクエリの配列。 |
tie_breaker | 複数のサブクエリに一致する行のスコア調整値。最終スコアは次の式で計算されます:best_match_score + tie_breaker × sum_of_other_scores。例:ある行が最初のサブクエリで 1.0、2 番目のサブクエリで 0.5 のスコアを獲得した場合、最終スコアは 1.0 + 0.75 × 0.5 = 1.375 になります。 |
全件検索
インデックスフィールドを持つすべての行を返します。各行のスコアは 1.0 になります。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.all()
);空検索
行を返しません。テストシナリオやエッジケースでのプレースホルダーとして使用します。
SELECT * FROM mock_items ORDER BY description @@@ pgsearch.config(
query => pgsearch.empty()
);BM25 スコアの取得
@@@ 式を SELECT リストに記述し、エイリアスを設定することで、各行の BM25 スコアを取得できます。
AnalyticDB for PostgreSQL はスコアを負の値で返すため、ORDER BY score(昇順)で自然にスコアの高い行が先頭に表示されます。スコアが -2.86 の場合、実際の BM25 スコアは 2.86 です。
BM25 スコアは text_fields および json_fields のみで計算されます。numeric_fields、datetime_fields、および boolean_fields での検索は、スコアとして -1 を返します。
SELECT *, mock_items @@@ pgsearch.config(<query>) AS bm25
FROM mock_items
ORDER BY bm25;使用例:
-- テキストフィールドでの全文検索
SELECT description, rating, description @@@ pgsearch.config('description:socks') AS bm25
FROM mock_items ORDER BY bm25 LIMIT 1; description | rating | bm25
----------------+--------+------------
Generic socks | 4 | -2.1048825
(1 row)-- 数値フィルター付き全文検索
SELECT description, rating, description @@@ pgsearch.config('description:socks AND rating:4') AS bm25
FROM mock_items ORDER BY bm25 LIMIT 4; description | rating | bm25
----------------+--------+------------
Generic socks | 4 | -3.1081846
(1 row)-- JSON フィールドでの検索
SELECT metadata, metadata @@@ pgsearch.config('metadata.color:White') AS bm25
FROM mock_items ORDER BY bm25 LIMIT 1; metadata | bm25
-------------------------------------------+-----------
{"color": "White", "location": "China"} | -3.453373
(1 row)-- 数値フィールドでの範囲検索 — BM25 スコアは利用不可で、-1 を返します
SELECT description, rating, rating @@@ pgsearch.config('rating:[4 TO 5]') AS bm25
FROM mock_items ORDER BY bm25 LIMIT 1; description | rating | bm25
-------------------+--------+------
Plastic Keyboard | 4 | -1
(1 row)インデックス管理
クエリインデックス構成
SELECT * FROM pgsearch.schema_bm25('search_idx'::regclass);インデックスの削除
DROP INDEX search_idx;インデックスの再構築
REINDEX INDEX search_idx;インデックスの削除はロールバックできません。