`など)、アトリビュートが入り交じったカオスな文字列と化している。
その巨大な生テキストに対して、以下のようなクエリを投げ込んでいないだろうか?
— 開発現場で絶対にやってはいけないアンチパターン
SELECT FROM wp_posts WHERE post_content LIKE ‘%検索キーワード%’;
フルテーブルスキャン(全件走査)の発生だ。 インデックスが完全に効かないこのクエリは、データ量が数万件を超えたあたりからMySQLのプロセスリストを埋め尽くし、サイト全体を沈没させる。
今回は、WordPressコアテーブルに隠された「救世主」、`post_content_filtered` カラムを用いた非正規化戦略と、プロダクション環境で耐えうる堅牢な実装コードを授ける。
—
なぜ `post_content_filtered` なのか?
WordPressのデータベーススキーマ(`wp_posts`)を眺めてほしい。歴史的経緯やプラグイン互換性のために用意されつつも、コアの機能としては長らく「使われていなかった」カラムが存在する。それが `post_content_filtered` だ。
- `post_content`: 生データ(HTML + ブロック構造)を保持する。
- `post_content_filtered`: 開発者が自由に設計・加工したデータをキャッシュ(非正規化)するための領域。
このカラムに「HTMLタグを完全に排除し、検索対象となるプレーンテキストのみ」を抽出して格納する。さらに、そのカラムに対してインデックスを張り、FULLTEXT検索や前方一致検索を組み合わせることで、検索パフォーマンスを劇的に改善できる。
—
アーキテクチャ設計:データフローの全体像
1.
フックの捕捉: 投稿の保存(`save_post` または `wp_insert_post`)を検知。
2.
テキスト抽出: `post_content` からショートコードを展開し、HTMLタグをすべて剥ぎ取る。
3.
非正規化データの書き込み: 抽出したクリーンテキストを `post_content_filtered` に同期保存。
4.
インデックス最適化: 必要に応じて `post_content_filtered` にインデックスを付与し、検索クエリを発行。
—
プロダクションコード:堅牢な同期・検索レイヤーの実装
それでは、実際のプロジェクトに組み込めるレベルの洗練されたコードを提示しよう。エラーハンドリング、トランザクション的配慮、無限ループ(再帰保存)の防止策まで網羅している。
1. テキストの同期処理(フック実装)
Plugin Name: WP Core Content Filtered Sync
Description: wp_posts.post_content_filtered を活用した高速検索インフラストラクチャ
Version: 1.0.0
Author: Tech Lead
/
declare(strict_types=1);
namespace Enterprise\SearchOptimization;
if ( ! defined( ‘ABSPATH’ ) ) {
exit;
}
class ContentFilterSync {
/
/
public static function init(): void {
// 自動下書きやリビジョンでの無駄な実行を防ぎつつ、確実性にフックする
add_action( ‘save_post’, [ self::class, ‘handle_save_post’ ], 10, 3 );
}
/
- save_post ハンドラー
- @param int $post_id 投稿ID
- @param \WP_Post $post 投稿オブジェクト
- @param bool $update 既存の更新か否か
/
public static function handle_save_post( int $post_id, \WP_Post $post, bool $update ): void {
// 1. セキュリティ・環境チェック(リビジョン、オートセーブ、権限なしの自動更新を弾く)
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
// 2. 意図しない投稿タイプやステータスの除外(公開・固定ページのみを対象とする例)
$allowed_post_types = [ ‘post’, ‘page’ ];
if ( ! in_array( $post->post_type, $allowed_post_types, true ) ) {
return;
}
if ( ‘publish’ !== $post->post_status ) {
return;
}
// 3. 無限ループ(save_post の再帰呼び出し)を防ぐためのガード
// wp_update_post を内部で叩くため、処理中フラグを立てる
static $is_syncing = false;
if ( $is_syncing ) {
return;
}
$is_syncing = true;
// 4. コンテンツの抽出とプレーンテキスト化
$plain_text = self::extract_searchable_text( $post->post_content );
// 5. データベースの直接更新(wp_update_post を使うと無限ループの危険性があるため $wpdb を使用、もしくは条件付きで制御)
global $wpdb;
$wpdb->update(
$wpdb->posts,
[ ‘post_content_filtered’ => $plain_text ],
[ ‘ID’ => $post_id ],
[ ‘%s’ ],
[ ‘%d’ ]
);
// キャッシュのクリア(オブジェクトキャッシュやトランジェントのパージ)
clean_post_cache( $post_id );
$is_syncing = false;
}
/
- HTMLタグ、ショートコード、Gutenbergコメントを排除し、検索用テキストを生成する
- @param string $content 生コンテンツ
- @return string サニタイズされたプレーンテキスト
/
private static function extract_searchable_text( string $content ): string {
// ショートコードを展開(必要に応じてカスタムフィールドの値などもここで結合可能)
$content = do_shortcode( $content );
// Gutenbergのブロックコメント(例: )を削除
$content = preg_replace( ‘//’, ”, $content );
// HTMLタグを除去
$content = wp_strip_all_tags( $content );
// 連続する空白や改行を正規化
$content = preg_replace( ‘/\s+/’, ‘ ‘, $content );
return trim( (string) $content );
}
}
// 起動
ContentFilterSync::init();
—
2. 高速化されたカスタム検索クエリの実装
データが綺麗に `post_content_filtered` に格納されたら、あとはクエリの勝負だ。以下は、LIKE検索の負荷を劇的に下げる検索クラスのサンプルである。
namespace Enterprise\SearchOptimization;
class OptimizedSearch {
/
- post_content_filtered を用いた高速キーワード検索
- @param string $keyword 検索キーワード
- @param int $limit 取得件数
- @return array 投稿オブジェクトの配列
/
public static function search( string $keyword, int $limit = 10 ): array {
global $wpdb;
// LIKEを使う場合でも、インデックスが効かない完全前方一致・後方一致のコストを下げる
// ※ もしMySQL 5.7+ / 8.0でFULLTEXTインデックスを構築しているなら MATCH AGAINST に書き換えるべきだ
$keyword_sql = ‘%’ . $wpdb->esc_like( $keyword ) . ‘%’;
$query = $wpdb->prepare(
”
SELECT
FROM {$wpdb->posts}
WHERE post_type = ‘post’
AND post_status = ‘publish’
AND post_content_filtered LIKE %s
ORDER BY post_date DESC
LIMIT %d
“,
$keyword_sql,
$limit
);
// クエリ結果をオブジェクトキャッシュに載せる設計が望ましい
$cache_key = ‘opt_search_’ . md5( $keyword . ‘_’ . $limit );
$results = wp_cache_get( $cache_key, ‘search_optimization’ );
if ( false === $results ) {
$results = $wpdb->get_results( $query );
wp_cache_set( $cache_key, $results, ‘search_optimization’, HOUR_IN_SECONDS );
}
return $results;
}
}
—
テックリードからの実践的アドバイス:インデックス設計の極意
このアプローチを採用する際、データベースの物理層で必ず行うべきチューニングがある。
1.
インデックスの付与:
`wp_posts` テーブルの `post_content_filtered` カラムは、デフォルトではインデックスを持っていない。そのため、以下のマイグレーションSQL(またはデプロイ時の処理)を実行し、インデックスを追加する必要がある。
— 部分インデックスやプレフィックスインデックスの検討(TEXT型の場合はサイズ制限に注意)
ALTER TABLE wp_posts ADD INDEX idx_content_filtered (post_content_filtered(255));
※注意: `TEXT` 型カラムにインデックスを貼る場合、MySQLではプレフィックス長(例: 255文字)を指定する必要がある点を見落とすな。
2.
FULLTEXTインデックスへの移行パス:
もし日本語の形態素解析(MeCabなど)やMySQLのインバーテッド・インデックス(全文検索)を組み合わせるフェーズに来たら、この `post_content_filtered` に対して `FULLTEXT` インデックスを定義するのが最も美しい。LIKE検索すら不要になり、検索スループットは異次元の速度に達する。
3.
既存データへのバッチ移行(Backfill):
過去に作成された数万件の投稿に対してこの仕組みを適用する場合、フック(`save_post`)だけでは過去データが取り残される。必ず WP-CLI を用いたバッチスクリプトを書き、一括で `post_content_filtered` を埋めるマイグレーションを実行すること。
結びにかえて
フレームワークの抽象化層の裏側を覗き、データベースの物理構造とインデックスの特性を理解してコードを書くこと。それこそが、ジュニアからシニア、そして真のテクニカルリードへ至る境界線だ。
「なぜそのカラムが存在するのか」「どう使えばクエリのコストを最小化できるか」。
常に背後にあるデータベースの挙動を脳内でシミュレートし、美しくスケーラブルなコードベースを構築してほしい。期待している。wp_postsテーブルの `post_content_filtered` カラムを活用せよ:LIKE検索の負荷を劇的に下げる非正規化の極意
テックリードの私だ。コードレビューの際、「なぜかデータベースのCPU使用率が跳ね上がっている」「特定のキーワード検索でクエリがスローダウンする」という問題に直面したことはないか?
原因の多くは決まっている。Gutenberg(ブロックエディター)の普及により、現代の `wp_posts.post_content` は、大量のHTMLタグ、JSON形式のブロックコメント(``など)、アトリビュートが入り交じったカオスな文字列と化している。
その巨大な生テキストに対して、以下のようなクエリを投げ込んでいないだろうか?
— 開発現場で絶対にやってはいけないアンチパターン
SELECT FROM wp_posts WHERE post_content LIKE ‘%検索キーワード%’;
フルテーブルスキャン(全件走査)の発生だ。 インデックスが完全に効かないこのクエリは、データ量が数万件を超えたあたりからMySQLのプロセスリストを埋め尽くし、サイト全体を沈没させる。
今回は、WordPressコアテーブルに隠された「救世主」、`post_content_filtered` カラムを用いた非正規化戦略と、プロダクション環境で耐えうる堅牢な実装コードを授ける。
—
なぜ `post_content_filtered` なのか?
WordPressのデータベーススキーマ(`wp_posts`)を眺めてほしい。歴史的経緯やプラグイン互換性のために用意されつつも、コアの機能としては長らく「使われていなかった」カラムが存在する。それが `post_content_filtered` だ。
- `post_content`: 生データ(HTML + ブロック構造)を保持する。
- `post_content_filtered`: 開発者が自由に設計・加工したデータをキャッシュ(非正規化)するための領域。
このカラムに「HTMLタグを完全に排除し、検索対象となるプレーンテキストのみ」を抽出して格納する。さらに、そのカラムに対してインデックスを張り、FULLTEXT検索や前方一致検索を組み合わせることで、検索パフォーマンスを劇的に改善できる。
—
アーキテクチャ設計:データフローの全体像
1.
フックの捕捉: 投稿の保存(`save_post` または `wp_insert_post`)を検知。
2.
テキスト抽出: `post_content` からショートコードを展開し、HTMLタグをすべて剥ぎ取る。
3.
非正規化データの書き込み: 抽出したクリーンテキストを `post_content_filtered` に同期保存。
4.
インデックス最適化: 必要に応じて `post_content_filtered` にインデックスを付与し、検索クエリを発行。
—
プロダクションコード:堅牢な同期・検索レイヤーの実装
それでは、実際のプロジェクトに組み込めるレベルの洗練されたコードを提示しよう。エラーハンドリング、トランザクション的配慮、無限ループ(再帰保存)の防止策まで網羅している。
1. テキストの同期処理(フック実装)
Plugin Name: WP Core Content Filtered Sync
Description: wp_posts.post_content_filtered を活用した高速検索インフラストラクチャ
Version: 1.0.0
Author: Tech Lead
/
declare(strict_types=1);
namespace Enterprise\SearchOptimization;
if ( ! defined( ‘ABSPATH’ ) ) {
exit;
}
class ContentFilterSync {
/
/
public static function init(): void {
// 自動下書きやリビジョンでの無駄な実行を防ぎつつ、確実性にフックする
add_action( ‘save_post’, [ self::class, ‘handle_save_post’ ], 10, 3 );
}
/
- save_post ハンドラー
- @param int $post_id 投稿ID
- @param \WP_Post $post 投稿オブジェクト
- @param bool $update 既存の更新か否か
/
public static function handle_save_post( int $post_id, \WP_Post $post, bool $update ): void {
// 1. セキュリティ・環境チェック(リビジョン、オートセーブ、権限なしの自動更新を弾く)
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
// 2. 意図しない投稿タイプやステータスの除外(公開・固定ページのみを対象とする例)
$allowed_post_types = [ ‘post’, ‘page’ ];
if ( ! in_array( $post->post_type, $allowed_post_types, true ) ) {
return;
}
if ( ‘publish’ !== $post->post_status ) {
return;
}
// 3. 無限ループ(save_post の再帰呼び出し)を防ぐためのガード
// wp_update_post を内部で叩くため、処理中フラグを立てる
static $is_syncing = false;
if ( $is_syncing ) {
return;
}
$is_syncing = true;
// 4. コンテンツの抽出とプレーンテキスト化
$plain_text = self::extract_searchable_text( $post->post_content );
// 5. データベースの直接更新(wp_update_post を使うと無限ループの危険性があるため $wpdb を使用、もしくは条件付きで制御)
global $wpdb;
$wpdb->update(
$wpdb->posts,
[ ‘post_content_filtered’ => $plain_text ],
[ ‘ID’ => $post_id ],
[ ‘%s’ ],
[ ‘%d’ ]
);
// キャッシュのクリア(オブジェクトキャッシュやトランジェントのパージ)
clean_post_cache( $post_id );
$is_syncing = false;
}
/
- HTMLタグ、ショートコード、Gutenbergコメントを排除し、検索用テキストを生成する
- @param string $content 生コンテンツ
- @return string サニタイズされたプレーンテキスト
/
private static function extract_searchable_text( string $content ): string {
// ショートコードを展開(必要に応じてカスタムフィールドの値などもここで結合可能)
$content = do_shortcode( $content );
// Gutenbergのブロックコメント(例: )を削除
$content = preg_replace( ‘//’, ”, $content );
// HTMLタグを除去
$content = wp_strip_all_tags( $content );
// 連続する空白や改行を正規化
$content = preg_replace( ‘/\s+/’, ‘ ‘, $content );
return trim( (string) $content );
}
}
// 起動
ContentFilterSync::init();
—
2. 高速化されたカスタム検索クエリの実装
データが綺麗に `post_content_filtered` に格納されたら、あとはクエリの勝負だ。以下は、LIKE検索の負荷を劇的に下げる検索クラスのサンプルである。
namespace Enterprise\SearchOptimization;
class OptimizedSearch {
/
- post_content_filtered を用いた高速キーワード検索
- @param string $keyword 検索キーワード
- @param int $limit 取得件数
- @return array 投稿オブジェクトの配列
/
public static function search( string $keyword, int $limit = 10 ): array {
global $wpdb;
// LIKEを使う場合でも、インデックスが効かない完全前方一致・後方一致のコストを下げる
// ※ もしMySQL 5.7+ / 8.0でFULLTEXTインデックスを構築しているなら MATCH AGAINST に書き換えるべきだ
$keyword_sql = ‘%’ . $wpdb->esc_like( $keyword ) . ‘%’;
$query = $wpdb->prepare(
”
SELECT
FROM {$wpdb->posts}
WHERE post_type = ‘post’
AND post_status = ‘publish’
AND post_content_filtered LIKE %s
ORDER BY post_date DESC
LIMIT %d
“,
$keyword_sql,
$limit
);
// クエリ結果をオブジェクトキャッシュに載せる設計が望ましい
$cache_key = ‘opt_search_’ . md5( $keyword . ‘_’ . $limit );
$results = wp_cache_get( $cache_key, ‘search_optimization’ );
if ( false === $results ) {
$results = $wpdb->get_results( $query );
wp_cache_set( $cache_key, $results, ‘search_optimization’, HOUR_IN_SECONDS );
}
return $results;
}
}
—
テックリードからの実践的アドバイス:インデックス設計の極意
このアプローチを採用する際、データベースの物理層で必ず行うべきチューニングがある。
1.
インデックスの付与:
`wp_posts` テーブルの `post_content_filtered` カラムは、デフォルトではインデックスを持っていない。そのため、以下のマイグレーションSQL(またはデプロイ時の処理)を実行し、インデックスを追加する必要がある。
— 部分インデックスやプレフィックスインデックスの検討(TEXT型の場合はサイズ制限に注意)
ALTER TABLE wp_posts ADD INDEX idx_content_filtered (post_content_filtered(255));
※注意: `TEXT` 型カラムにインデックスを貼る場合、MySQLではプレフィックス長(例: 255文字)を指定する必要がある点を見落とすな。
2.
FULLTEXTインデックスへの移行パス:
もし日本語の形態素解析(MeCabなど)やMySQLのインバーテッド・インデックス(全文検索)を組み合わせるフェーズに来たら、この `post_content_filtered` に対して `FULLTEXT` インデックスを定義するのが最も美しい。LIKE検索すら不要になり、検索スループットは異次元の速度に達する。
3.
既存データへのバッチ移行(Backfill):
過去に作成された数万件の投稿に対してこの仕組みを適用する場合、フック(`save_post`)だけでは過去データが取り残される。必ず WP-CLI を用いたバッチスクリプトを書き、一括で `post_content_filtered` を埋めるマイグレーションを実行すること。
結びにかえて
フレームワークの抽象化層の裏側を覗き、データベースの物理構造とインデックスの特性を理解してコードを書くこと。それこそが、ジュニアからシニア、そして真のテクニカルリードへ至る境界線だ。
「なぜそのカラムが存在するのか」「どう使えばクエリのコストを最小化できるか」。
常に背後にあるデータベースの挙動を脳内でシミュレートし、美しくスケーラブルなコードベースを構築してほしい。期待している。