コードレビューの最中、ジュニアエンジニアが書いた次のようなコードを見て、頭を抱えたことはないだろうか。
// レビュー対象のコード:やってはいけない典型例
$args = array(
‘post_type’ => ‘product’,
‘s’ => ‘WordPress データベース’,
‘posts_per_page’ => 10,
);
$query = new WP_Query( $args );
「一見、何の問題もないシンプルな検索クエリに見えます。なぜこれがプロダクション環境でボトルネックになるのでしょうか?」
そう問いかける彼らに、私はこう返す。「君は今、MySQLに全行スキャン(Full Table Scan)という名の刑罰を執行した。データ量が100万件を超えた瞬間、このサーバーは息絶えるだろう」と。
今回は、WordPressの `WP_Query` における `s` パラメータ(キーワード検索)の内部挙動を解剖し、なぜそれが遅いのか、そして我々プロフェッショナルがどう設計すべきかをロジカルに解説する。
—
なぜ `WP_Query` の `s` パラメータは遅いのか?(内部構造の解剖)
WordPressでキーワード検索を実行した際、コアが裏側で何をしているか知っているだろうか? `WP_Query` のソースコード(主に `wp-includes/class-wp-query.php` の `get_search_sql` メソッド)を覗くと、発行されるSQLの正体がわかる。
`s` パラメータを指定すると、WordPressは次のようなSQLを生成する(簡略化)。
SELECT FROM wp_posts
WHERE 1=1
AND (((wp_posts.post_title LIKE ‘%WordPress%’)
OR (wp_posts.post_excerpt LIKE ‘%WordPress%’)
OR (wp_posts.post_content LIKE ‘%WordPress%’)))
AND wp_posts.post_type = ‘product’
AND wp_posts.post_status = ‘publish’
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10;
1. `LIKE ‘%keyword%’`(前方一致しないワイルドカード)の呪い
データベースのインデックス(B-tree)は、「左側から特定できるデータ」を探すために存在する。しかし、先頭に `%` を付与する部分一致検索(Trailing and leading wildcards)では、インデックスのツリー構造をたどることが不可能になる。
結果として、MySQLは `wp_posts` テーブルの全レコードの先頭から終わりまで、1行ずつ文字列をスキャン(Full Table Scan)し始める。
2. 巨大なテキストカラムへの負荷
`post_content` は `LONGTEXT` 型であり、HTMLタグやショートコードが大量に詰まっている。ここに `LIKE` 検索をかけることは、I/Oバウンドな処理を極限まで引き起こし、InnoDBのバッファプール(Buffer Pool)を汚染する。キャッシュに乗らないクエリが頻発すれば、DBサーバーのCPU使用率は天井に張り付く。
3. `OR` 条件によるオプティマイザの混乱
タイトル、抜粋、本文の3つのカラムに対して `OR` で結ばれた `LIKE` 条件が評価されるため、MySQLのクエリオプティマイザは効率的な実行計画を立てづらくなる。
—
限界の打破:プロが選ぶべき3つのアプローチ
この構造的欠陥に対して、実務ではどのように立ち回るべきか。設計アプローチは主に3つ存在する。
1. MySQL全文検索(FULLTEXTインデックス)への移行
2. 外部検索エンジン(Elasticsearch / Algolia)の導入
3. メタデータやタクソノミによる絞り込みの強制(プレフィルタリング)
今回は、インフラを追加せずに既存のMySQL環境で劇的な速度改善を生む、「MySQLの `FULLTEXT` インデックスを活用した `posts_clauses` フックによるクエリ書き換え」のプロダクションコードを伝授しよう。
—
【実装例】FULLTEXTインデックスを活用した高速キーワード検索
MySQL 5.6以降(InnoDB)では、日本語の形態素解析には注意が必要だが、英数字やスペース区切りのキーワード、あるいは適切なパーサー(Mecabプラグイン等)が導入された環境において、`FULLTEXT` 検索は `LIKE` 検索の悪夢を終わらせる特効薬となる。
以下のコードは、`WP_Query` の標準的な `s` パラメータの挙動を検出し、内部生成されるSQLの `WHERE` 句を `MATCH AGAINST` 構文に安全にすげ替える高度なコンポーネントである。
/
class High_Performance_Search {
public function __construct() {
// posts_clausesフックを使い、SQLの生成直後に介入する
add_filter( ‘posts_clauses’, array( $this, ‘optimize_search_clauses’ ), 10, 2 );
}
/
- SQLの句(clauses)をフックして書き換える
- @param array $clauses データベースクエリの各句 (where, join, groupby, etc.)
- @param WP_Query $query WP_Queryのインスタンス
- @return array
/
public function optimize_search_clauses( $clauses, $query ) {
// 管理画面や、対象外のクエリ、検索ワードがない場合はスルー
if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
return $clauses;
}
$search_term = $query->get( ‘s’ );
if ( empty( $search_term ) ) {
return $clauses;
}
global $wpdb;
// エスケープ処理(SQLインジェクション対策)
$safe_term = $wpdb->_real_escape( $search_term );
// MATCH AGAINST構文の構築(BOOLEAN MODEを使用し、高度な検索を許容)
// ※注意: あらかじめ wp_posts の post_title, post_content に FULLTEXT インデックスを貼っておくこと
$match_columns = “{$wpdb->posts}.post_title, {$wpdb->posts}.post_content”;
// 既存のLIKEベースのWHERE句を強制的に置き換える、あるいはアペンドする
// ここではWP_Queryが生成したデフォルトのLIKE条件を正規表現で綺麗にパージして差し替える
$clauses[‘where’] = preg_replace(
‘/\(\s’ . $wpdb->posts . ‘\.post_title LIKE [^\)]+\)/’,
“MATCH({$match_columns}) AGAINST(‘{$safe_term}’ IN BOOLEAN MODE)”,
$clauses[‘where’]
);
// スコアリング順(関連度順)にソートを最適化したい場合
// $clauses[‘orderby’] = “MATCH({$match_columns}) AGAINST(‘{$safe_term}’ IN BOOLEAN MODE) DESC”;
// クエリキャッシュやデバッグ用のログ出力(必要に応じて)
// error_log( ‘Optimized SQL WHERE: ‘ . $clauses[‘where’] );
return $clauses;
}
}
// 初期化
new High_Performance_Search();
このコードの設計美と注意点
1. `posts_clauses` フックの選択理由
`posts_search` フックを使う手もあるが、あれは `LIKE` の文字列を結合するためのものであり、構文そのものを `MATCH … AGAINST` に変えるには不十分である。`posts_clauses` であれば、発行されるSQLの全貌(`WHERE`, `JOIN`, `ORDER BY`)を直接コントロールできるため、極めて抽象度が高く堅牢な制御が可能になる。
2. 事前準備としてのDBインデックス設計
このコードを本番適用する前に、対象のMySQLテーブルに対して必ず以下のインデックスを手動(またはマイグレーションスクリプト)で追加しておく必要がある。
ALTER TABLE wp_posts ADD FULLTEXT INDEX ft_search_index (post_title, post_content);
(※MySQL 5.7未満や、日本語環境でイングラム・パーサーを設定していない場合は、空間インデックスや外部検索SaaSの併用を検討すること)
—
コードレビューアーからの総括
「とりあえず動くから `s` パラメータを使う」というフェーズは、開発初期のプロトタイピングで終わらせるべきだ。
データが肥大化した瞬間にシステムを崩壊させる `LIKE ‘%keyword%’` のメカニズムを理解し、データベースのインデックス構造(B-tree vs フルテキスト)を逆算してアーキテクチャを組むこと。それこそが、シニアエンジニアとジュニアエンジニアを分かつ境界線である。
次に君が書くコードは、インデックスを殺さない、洗練された美しいクエリであることを期待する。