エンジニア諸君、今日も`wp_options`の肥大化や、不毛な`LIKE`検索によるスロークエリと戦っているようだな。
多くの開発者がWordPressの検索機能を拡張する際、安易に「外部検索プラグインを入れる」か、あるいは「`WP_Query`の`s`パラメータをそのまま使う」という二択に逃げる。しかし、プロフェッショナルならば、まずはデータベーススキーマの物理構造を深く洞察すべきだ。
今回は、`wp_posts`テーブルにひっそりと鎮座し、コアのコードでもほとんど活用されていない「眠れる遺産」、`post_content_filtered`カラムを、検索パフォーマンスを劇的に向上させる「検索専用バッファ」へと転生させる設計思想を伝授する。
—
1. なぜ `post_content` を直接検索してはいけないのか
現在のWordPressにおいて、`post_content`はもはや「純粋なテキスト」ではない。Gutenberg(ブロックエディタ)の登場により、内部は複雑なJSON風のHTMLコメントや、レンダリングが必要なショートコードで溢れかえっている。
この状態で `LIKE ‘%キーワード%’` を走らせると何が起きるか?
1. ノイズの混入: ブロックの属性値(`{“align”:”center”}`等)まで検索対象になり、精度の低い結果が返る。
2. インデックスの無効化: 前方一致すら使えない中間一致検索は、レコード数が増えるほど線形的にレスポンスを悪化させる。
3. 計算リソースの無駄: 検索のたびに、DBエンジンは膨大なHTMLタグを舐めるようにスキャンせねばならない。
ここで我々が活用すべきが、`post_content_filtered` だ。
—
2. 物理構造の再定義: `post_content_filtered` の役割
`wp_posts` テーブルの物理設計を思い出してほしい。`post_content_filtered` は、本来「加工済みのコンテンツ(Markdownから変換されたHTMLなど)」を保持するために用意されたカラムだが、現在のコアではほぼ未使用だ。
このカラムを、「検索エンジン・外部API向けの、ノイズを排除したプレーンテキスト・キャッシュ」として再定義する。
設計のメリット
- 検索精度の向上: ショートコードやHTMLタグを完全に除去した「純粋なテキスト」のみを格納できる。
- 同期コストの最小化: `post_content` と同じテーブルにあるため、外部DB(Elasticsearch等)への同期フラグとしても機能し、JOINの必要がない。
- 検索クエリの軽量化: 検索対象をこのカラムに絞ることで、スキャン対象のデータサイズを大幅に削減できる。
—
3. 実装:自動プレ処理とカラムへの格納戦略
まずは、投稿保存時に `post_content` をクリーニングし、`post_content_filtered` に自動同期するロジックを実装する。ここで `save_post` フックを使うのは二流だ。`wp_insert_post_data` を使い、DBに書き込まれる直前のデータをインターセプトするのが最も効率的で堅牢だ。
/
- 投稿保存時に検索専用のクリーンなテキストを作成し、
- post_content_filteredカラムに格納する。
/
add_filter(‘wp_insert_post_data’, function (array $data, array $postarr) {
// 自動保存やリビジョン、特定の投稿タイプを除外(設計に応じて調整)
if (defined(‘DOING_AUTOSAVE’) && DOING_AUTOSAVE) return $data;
if ($data[‘post_status’] === ‘inherit’) return $data;
// 1. ブロックをパースし、プレーンテキストのみを抽出
// do_blocks() を通すことで、動的ブロックの出力内容も考慮可能だが、
// 重い場合は strip_tags(render_block()) 等で調整する。
$content = $data[‘post_content’];
// 2. タグの除去とデコード
// ショートコードも解決したい場合は do_shortcode() を挟む
$clean_text = wp_strip_all_tags($content);
$clean_text = strip_shortcodes($clean_text);
// 3. 検索精度を上げるための正規化(全角半角、大文字小文字など)
$clean_text = mb_convert_kana($clean_text, ‘rnkKV’); // 必要に応じて
$clean_text = mb_strtolower($clean_text, ‘UTF-8’);
// 4. フィルタリング済みカラムに格納
$data[‘post_content_filtered’] = $clean_text;
return $data;
}, 10, 2);
—
4. 検索エンジンのハイジャック: `posts_search` フックの活用
次に、フロントエンドの検索クエリが走った際、`post_content` ではなく `post_content_filtered` を見に行くようにSQLを書き換える。
/
- 標準の検索対象を post_content から post_content_filtered に切り替える
/
add_filter(‘posts_search’, function ($search, \WP_Query $query) {
if (is_admin() || !$query->is_main_query() || !$query->is_search()) {
return $search;
}
global $wpdb;
// 標準の検索SQL構文: AND (((wp_posts.post_title LIKE ‘%…%’) OR (wp_posts.post_content LIKE ‘%…%’)))
// これを置換して post_content_filtered を見るように変更する
$search = str_replace(
“{$wpdb->posts}.post_content LIKE”,
“{$wpdb->posts}.post_content_filtered LIKE”,
$search
);
return $search;
}, 10, 2);
—
5. 発展:外部検索エンジン(Elasticsearch)との連携基盤
この設計の真価は、外部検索エンジンとの連携で発揮される。
例えば Elasticsearch や Algolia へデータを飛ばす際、通常は「HTMLをパースして、不要なデータを除いて…」という重い処理を非同期ジョブ側で行う必要がある。
しかし、この設計ならば 「`post_content_filtered` が更新されたら、その中身をそのまま検索エンジンに Put するだけ」 という極めてシンプルな実装に落とし込める。
/
- 外部検索エンジン(例: Elasticsearch)への同期トリガー
/
add_action(‘save_post’, function ($post_id, $post, $update) {
if (!$update || $post->post_status !== ‘publish’) return;
// すでに前処理済みのデータが DB に入っているため、取得して飛ばすだけ
$search_optimized_content = $post->post_content_filtered;
// 非同期リクエスト(wp_remote_post等)で外部エンジンに同期
// MySearchEngine::sync($post_id, $search_optimized_content);
}, 10, 3);
—
6. パフォーマンス上の注意点と「プロの詰め」
この設計を導入する際、以下の2点を必ず確認せよ。
1. 既存レコードの更新: このロジックを導入しただけでは、過去の投稿の `post_content_filtered` は空のままだ。WP-CLI を使い、全投稿を `wp_update_post()` するスクリプトを一度走らせる必要がある。
2. インデックスの検討: もし `post_content_filtered` に対して頻繁に DB レベルの検索を行うなら、MySQLの FULLTEXT INDEX をこのカラムに付与することを検討しろ。`post_content` 全体に貼るよりも、ノイズが少ない分インデックスサイズも劇的に小さく済む。
結論
システムのボトルネックは、常に「不透明なデータ構造」にある。
WordPressが提供するスキーマを「ただそこにあるもの」として使うのではなく、その物理的な特性を理解し、「用途に応じたデータの正規化バッファ」として再定義する。
`post_content_filtered` の活用は、その第一歩に過ぎない。
コードレビューで「なぜ `post_content` をそのまま検索してはいけないのか?」と問われたら、胸を張ってこの設計思想を語りたまえ。
健闘を祈る。