`)がそのままごちゃ混ぜになって入っています。
もし、ユーザーがサイト内でキーワード検索を行ったとき、WordPressのデフォルトのクエリは次のようなSQLを発行します。
SELECT FROM wp_posts
WHERE post_content LIKE ‘%検索キーワード%’
AND post_status = ‘publish’;
ピンときた方もいるかもしれません。このクエリには、データベースエンジニアにとって悪夢のような特徴があります。
1.
前方一致ではなく「完全一致(部分一致)」の `LIKE ‘%…%’` なので、インデックス(INDEX)が効かない
2.
HTMLタグやブロック構文のゴミデータまで含めてスキャンするため、テキスト量が無駄に膨らむ
3.
記事数が数万件を超えてくると、検索のたびにMySQLがCPUを激しく消費し、サイト全体のパフォーマンスが急降下する
他のモダンなWebフレームワークであれば、専用の全文検索エンジン(ElasticsearchやAlgoliaなど)を挟むところですが、小〜中規模のWordPress案件ではインフラコストの都合上、それが難しいことも多いです。
そこで登場するのが、
「HTMLを剥ぎ取った純粋なテキストデータ(プレーンテキスト)を別の場所に保持し、検索負荷を劇的に下げる」 というアプローチです。
—
救世主:`post_content_filtered` カラムとは何か?
実は、`wp_posts` テーブルには最初から、この目的のために用意された空席(カラム)が存在します。それが
`post_content_filtered` です。
WordPressの公式ドキュメントや一般的なプラグイン開発でも滅多に使われませんが、コアの設計思想としては「フィルタリングされた(加工済みの)コンテンツを保存するため」に作られています。ここに「HTMLタグをすべて除去した純粋なテキスト」を保存しておくのです。
イメージ図:データパイプラインの構造
[ 記事保存・更新 (save_post) ]
│
▼
[ フックが発火 (Data Pipeline) ]
│
├─ 1. post_content から HTML / ブロック構文を除去
└─ 2. 純粋なテキストデータに変換
│
▼
[ wp_posts.post_content_filtered に格納 ]
│
▼
[ 検索時は post_content_filtered を対象にする (負荷軽減!) ]
このカラムを活用することで、検索時に無駄なHTMLパースや重いLIKE検索を行う必要がなくなり、データベースの負荷を大幅に抑えることができます。
—
実装:データパイプラインを構築するコード
それでは実際に、記事が保存・更新されたタイミングで自動的にHTMLを剥ぎ取り、`post_content_filtered` にクリーンなテキストを流し込む仕組み(データパイプライン)を書いてみましょう。
テーマの `functions.php` または自作プラグインに記述します。
/
- 記事保存時に HTML を除去したプレーンテキストを post_content_filtered に格納する
- @param int $post_id 投稿ID
- @param WP_Post $post 投稿オブジェクト
- @param bool $update 既存の更新かどうか
/
function my_custom_sync_filtered_content( $post_id, $post, $update ) {
// 1. リビジョンや自動保存、ゴミ箱行き、カスタム投稿タイプ以外の意図しない発火を防ぐガード節
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
// 公開ステータス以外のときは空にするなどの制御もここで可能
if ( ‘publish’ !== $post->post_status ) {
return;
}
// 無限ループを防ぐため、save_post フック内での update_post_meta 等の扱いに注意するが、
// wp_update_post を直接使うと無限ループするので、直接 $wpdb を叩くのが安全。
global $wpdb;
// 2. ブロックコメントや HTML タグを完全に除去し、純粋なテキスト抽出を行う
$raw_content = $post->post_content;
// Gutenberg のコメント構文を削除 ()
$clean_content = preg_replace( ‘//’, ”, $raw_content );
// HTML タグを除去し、HTMLエンティティをデコード
$clean_content = wp_strip_all_tags( $clean_content );
// 空白や改行を正規化(必要に応じて)
$clean_content = trim( preg_replace( ‘/\s+/’, ‘ ‘, $clean_content ) );
// 3. wp_posts テーブルの post_content_filtered カラムを直接アップデート
// ※ wp_update_post を使うと save_post が再帰的に呼ばれて無限ループするので $wpdb を使用します
$wpdb->update(
$wpdb->posts,
array( ‘post_content_filtered’ => $clean_content ),
array( ‘ID’ => $post_id ),
array( ‘%s’ ),
array( ‘%d’ )
);
}
add_action( ‘save_post’, ‘my_custom_sync_filtered_content’, 10, 3 );
コードの解説と重要なポイント
開発初心者が一番やりがちなのが、この関数の中で `wp_update_post()` を使ってしまうミスです。`wp_update_post()` を呼ぶと再び `save_post` フックが発火するため、
ブラウザがフリーズするほどの無限ループ(Maximum execution time exceeded)を引き起こします。これを防ぐために、直接 `$wpdb->update()` を使ってデータベースの特定カラムだけを静かに更新するのがプロの技です。
- `preg_replace` と `wp_strip_all_tags`
WordPressにはHTMLタグを取り除く便利な関数 `wp_strip_all_tags()` が用意されています。しかし、Gutenberg(ブロックエディター)環境下では HTML だけでなく “ のようなブロック制御コメントが大量に残るため、正規表現でこれらを事前に掃除してからタグ除去を行うのが綺麗にデータを保つコツです。
—
検索クエリを書き換えて負荷を分散させる
データが入るようになったら、次はフロントエンドからの検索クエリが、重い `post_content` ではなく、きれいな `post_content_filtered` を見るようにフックを掛け替えます。
WordPressの検索クエリをカスタマイズするには `posts_search` フィルターを使用します。
/
- 検索対象を post_content から post_content_filtered に差し替える
- @param string $search 既存のSQL検索WHERE句
- @param WP_Query $wp_query クエリインスタンス
- @return string 修飾されたSQL検索WHERE句
/
function my_optimize_search_query( $search, $wp_query ) {
// 管理画面やメインの検索クエリ以外では発火させない
if ( is_admin() || ! $wp_query->is_main_query() || ! $wp_query->is_search() ) {
return $search;
}
global $wpdb;
// 検索ワードを取得
$s = $wp_query->get( ‘s’ );
if ( empty( $s ) ) {
return $search;
}
// ここで安全にエスケープ処理を行う
$like = ‘%’ . $wpdb->esc_like( $s ) . ‘%’;
// デフォルトの検索条件(post_title または post_content)を
// post_title または post_content_filtered に置き換える
// ※ タイトル検索のロジックは残しつつ、本文の検索先だけをスリムなカラムに変更します
$search = $wpdb->prepare(
” AND (
({$wpdb->posts}.post_title LIKE %s)
OR
({$wpdb->posts}.post_content_filtered LIKE %s)
) AND {$wpdb->posts}.post_status = ‘publish'”,
$like,
$like
);
return $search;
}
add_filter( ‘posts_search’, ‘my_optimize_search_query’, 10, 2 );
これで、データベースは「余計なHTMLタグを含まないスリムなテキストカラム」に対してのみ `LIKE` 検索を行うようになり、スキャンコストが劇的に軽減されます。
—
陥りやすい文法エラーと開発時の注意点
1.
既存データへのマイグレーション忘れ
上記のコードを実装しても、
「過去にすでに書かれた記事」の `post_content_filtered` は空のままです。過去記事に対しても一度だけ一括変換をかけるスクリプト(WP-CLIや、一時的な一括更新バッチ処理)を走らせる必要があります。
2.
文字コードとエスケープ(`$wpdb->prepare`)
SQLインジェクション脆弱性を防ぐため、ユーザーが入力した検索ワードは必ず `$wpdb->esc_like()` でエスケープし、`$wpdb->prepare` を通すようにしてください。他の言語の経験者であれば当たり前のことですが、WordPressの歴史の長いコードベースではここを怠るサンプルも散見されるので注意しましょう。
—
まとめ:WordPressの内部構造を掌握する
今回は、`wp_posts` テーブルの隠し味とも言える `post_content_filtered` カラムを活用した検索負荷分散のデータパイプラインについて解説しました。
- `post_content` はHTMLやブロック情報が含まれるため、そのまま検索すると重い
- 保存時(`save_post`)にHTMLを剥ぎ取ったテキストを `post_content_filtered` に同期させる
- 検索クエリ(`posts_search`)の参照先を軽量なカラムに置き換える
- 無限ループ(`wp_update_post` の誤用)には細心の注意を払う
この仕組みを理解し実装できれば、単なる「WordPressの使い方を知っている人」から、「WordPressのコア構造をコントロールできるエンジニア」へと確実にステップアップできます。
データベースの裏側の動きを意識した設計ができると、どんなに規模の大きなサイトを作ってもびくともしない堅牢なシステムが構築できるようになりますよ。ぜひあなたの開発現場でも試してみてくださいね!こんにちは!WordPressの裏側の仕組みや、データベースがどう動いているのか気になって探求していませんか?
今回は、WordPressのデータベースの心臓部である `wp_posts` テーブル、その中でも普段はあまりスポットライトが当たらない
`post_content_filtered` カラム に焦点を当てていきます。「ここをクリアすれば、WordPressのデータベース構造の本質はバッチリマスターできますよ!」と言えるくらい、非常に面白く、かつ実務で使える強力なテクニックです。
他の言語(Ruby、Python、Node.jsなど)からWordPressの世界に入ってきた開発者の方に向けて、内部のデータパイプラインの構築方法を優しく、かつ深く解説していきますね。
—
なぜ、デフォルトのWordPress検索は重くなるのか?
WordPressで記事を投稿すると、そのメイン本文は `wp_posts` テーブルの `post_content` カラムに保存されますよね。ここには、HTMLタグやブロックエディター(Gutenberg)の構造を示すコメント(例: ``)がそのままごちゃ混ぜになって入っています。
もし、ユーザーがサイト内でキーワード検索を行ったとき、WordPressのデフォルトのクエリは次のようなSQLを発行します。
SELECT FROM wp_posts
WHERE post_content LIKE ‘%検索キーワード%’
AND post_status = ‘publish’;
ピンときた方もいるかもしれません。このクエリには、データベースエンジニアにとって悪夢のような特徴があります。
1.
前方一致ではなく「完全一致(部分一致)」の `LIKE ‘%…%’` なので、インデックス(INDEX)が効かない
2.
HTMLタグやブロック構文のゴミデータまで含めてスキャンするため、テキスト量が無駄に膨らむ
3.
記事数が数万件を超えてくると、検索のたびにMySQLがCPUを激しく消費し、サイト全体のパフォーマンスが急降下する
他のモダンなWebフレームワークであれば、専用の全文検索エンジン(ElasticsearchやAlgoliaなど)を挟むところですが、小〜中規模のWordPress案件ではインフラコストの都合上、それが難しいことも多いです。
そこで登場するのが、
「HTMLを剥ぎ取った純粋なテキストデータ(プレーンテキスト)を別の場所に保持し、検索負荷を劇的に下げる」 というアプローチです。
—
救世主:`post_content_filtered` カラムとは何か?
実は、`wp_posts` テーブルには最初から、この目的のために用意された空席(カラム)が存在します。それが
`post_content_filtered` です。
WordPressの公式ドキュメントや一般的なプラグイン開発でも滅多に使われませんが、コアの設計思想としては「フィルタリングされた(加工済みの)コンテンツを保存するため」に作られています。ここに「HTMLタグをすべて除去した純粋なテキスト」を保存しておくのです。
イメージ図:データパイプラインの構造
[ 記事保存・更新 (save_post) ]
│
▼
[ フックが発火 (Data Pipeline) ]
│
├─ 1. post_content から HTML / ブロック構文を除去
└─ 2. 純粋なテキストデータに変換
│
▼
[ wp_posts.post_content_filtered に格納 ]
│
▼
[ 検索時は post_content_filtered を対象にする (負荷軽減!) ]
このカラムを活用することで、検索時に無駄なHTMLパースや重いLIKE検索を行う必要がなくなり、データベースの負荷を大幅に抑えることができます。
—
実装:データパイプラインを構築するコード
それでは実際に、記事が保存・更新されたタイミングで自動的にHTMLを剥ぎ取り、`post_content_filtered` にクリーンなテキストを流し込む仕組み(データパイプライン)を書いてみましょう。
テーマの `functions.php` または自作プラグインに記述します。
/
- 記事保存時に HTML を除去したプレーンテキストを post_content_filtered に格納する
- @param int $post_id 投稿ID
- @param WP_Post $post 投稿オブジェクト
- @param bool $update 既存の更新かどうか
/
function my_custom_sync_filtered_content( $post_id, $post, $update ) {
// 1. リビジョンや自動保存、ゴミ箱行き、カスタム投稿タイプ以外の意図しない発火を防ぐガード節
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
// 公開ステータス以外のときは空にするなどの制御もここで可能
if ( ‘publish’ !== $post->post_status ) {
return;
}
// 無限ループを防ぐため、save_post フック内での update_post_meta 等の扱いに注意するが、
// wp_update_post を直接使うと無限ループするので、直接 $wpdb を叩くのが安全。
global $wpdb;
// 2. ブロックコメントや HTML タグを完全に除去し、純粋なテキスト抽出を行う
$raw_content = $post->post_content;
// Gutenberg のコメント構文を削除 ()
$clean_content = preg_replace( ‘//’, ”, $raw_content );
// HTML タグを除去し、HTMLエンティティをデコード
$clean_content = wp_strip_all_tags( $clean_content );
// 空白や改行を正規化(必要に応じて)
$clean_content = trim( preg_replace( ‘/\s+/’, ‘ ‘, $clean_content ) );
// 3. wp_posts テーブルの post_content_filtered カラムを直接アップデート
// ※ wp_update_post を使うと save_post が再帰的に呼ばれて無限ループするので $wpdb を使用します
$wpdb->update(
$wpdb->posts,
array( ‘post_content_filtered’ => $clean_content ),
array( ‘ID’ => $post_id ),
array( ‘%s’ ),
array( ‘%d’ )
);
}
add_action( ‘save_post’, ‘my_custom_sync_filtered_content’, 10, 3 );
コードの解説と重要なポイント
開発初心者が一番やりがちなのが、この関数の中で `wp_update_post()` を使ってしまうミスです。`wp_update_post()` を呼ぶと再び `save_post` フックが発火するため、
ブラウザがフリーズするほどの無限ループ(Maximum execution time exceeded)を引き起こします。これを防ぐために、直接 `$wpdb->update()` を使ってデータベースの特定カラムだけを静かに更新するのがプロの技です。
- `preg_replace` と `wp_strip_all_tags`
WordPressにはHTMLタグを取り除く便利な関数 `wp_strip_all_tags()` が用意されています。しかし、Gutenberg(ブロックエディター)環境下では HTML だけでなく `` のようなブロック制御コメントが大量に残るため、正規表現でこれらを事前に掃除してからタグ除去を行うのが綺麗にデータを保つコツです。
—
検索クエリを書き換えて負荷を分散させる
データが入るようになったら、次はフロントエンドからの検索クエリが、重い `post_content` ではなく、きれいな `post_content_filtered` を見るようにフックを掛け替えます。
WordPressの検索クエリをカスタマイズするには `posts_search` フィルターを使用します。
/
- 検索対象を post_content から post_content_filtered に差し替える
- @param string $search 既存のSQL検索WHERE句
- @param WP_Query $wp_query クエリインスタンス
- @return string 修飾されたSQL検索WHERE句
/
function my_optimize_search_query( $search, $wp_query ) {
// 管理画面やメインの検索クエリ以外では発火させない
if ( is_admin() || ! $wp_query->is_main_query() || ! $wp_query->is_search() ) {
return $search;
}
global $wpdb;
// 検索ワードを取得
$s = $wp_query->get( ‘s’ );
if ( empty( $s ) ) {
return $search;
}
// ここで安全にエスケープ処理を行う
$like = ‘%’ . $wpdb->esc_like( $s ) . ‘%’;
// デフォルトの検索条件(post_title または post_content)を
// post_title または post_content_filtered に置き換える
// ※ タイトル検索のロジックは残しつつ、本文の検索先だけをスリムなカラムに変更します
$search = $wpdb->prepare(
” AND (
({$wpdb->posts}.post_title LIKE %s)
OR
({$wpdb->posts}.post_content_filtered LIKE %s)
) AND {$wpdb->posts}.post_status = ‘publish'”,
$like,
$like
);
return $search;
}
add_filter( ‘posts_search’, ‘my_optimize_search_query’, 10, 2 );
これで、データベースは「余計なHTMLタグを含まないスリムなテキストカラム」に対してのみ `LIKE` 検索を行うようになり、スキャンコストが劇的に軽減されます。
—
陥りやすい文法エラーと開発時の注意点
1.
既存データへのマイグレーション忘れ
上記のコードを実装しても、
「過去にすでに書かれた記事」の `post_content_filtered` は空のままです。過去記事に対しても一度だけ一括変換をかけるスクリプト(WP-CLIや、一時的な一括更新バッチ処理)を走らせる必要があります。
2.
文字コードとエスケープ(`$wpdb->prepare`)
SQLインジェクション脆弱性を防ぐため、ユーザーが入力した検索ワードは必ず `$wpdb->esc_like()` でエスケープし、`$wpdb->prepare` を通すようにしてください。他の言語の経験者であれば当たり前のことですが、WordPressの歴史の長いコードベースではここを怠るサンプルも散見されるので注意しましょう。
—
まとめ:WordPressの内部構造を掌握する
今回は、`wp_posts` テーブルの隠し味とも言える `post_content_filtered` カラムを活用した検索負荷分散のデータパイプラインについて解説しました。
- `post_content` はHTMLやブロック情報が含まれるため、そのまま検索すると重い
- 保存時(`save_post`)にHTMLを剥ぎ取ったテキストを `post_content_filtered` に同期させる
- 検索クエリ(`posts_search`)の参照先を軽量なカラムに置き換える
- 無限ループ(`wp_update_post` の誤用)には細心の注意を払う
この仕組みを理解し実装できれば、単なる「WordPressの使い方を知っている人」から、「WordPressのコア構造をコントロールできるエンジニア」へと確実にステップアップできます。
データベースの裏側の動きを意識した設計ができると、どんなに規模の大きなサイトを作ってもびくともしない堅牢なシステムが構築できるようになりますよ。ぜひあなたの開発現場でも試してみてくださいね!