階層タクソノミーの呪縛を解く:Redisによる「O(1)」検索アーキテクチャの構築
WordPressの `get_terms()` を再帰的に呼び出し、階層構造を構築するために `wp_get_term_children()` を何度も叩く——。もし君がそんなコードをプロダクション環境で動かしているなら、今すぐその手を止めるべきだ。
WordPressのタクソノミーテーブル(`wp_term_taxonomy`)は、JOINと再帰の嵐だ。データ量が増大するにつれ、MySQLのクエリプランナは悲鳴を上げ、`wp_cache` のオブジェクトキャッシュだけでは、複雑な階層構造の再構築コストを完全にはカバーできない。
今日は、Redisを単なる「KVストア」としてではなく、「タクソノミーのフラット化インデックス」として活用し、階層データをO(1)で取得する設計パターンを伝授する。
—
1. なぜWordPress標準のキャッシュでは不十分なのか
`get_terms()` は内部的に `WP_Term_Query` を呼び出し、結果を `wp_cache_set` する。しかし、階層構造を保持したツリー状の配列を動的に生成する場合、毎回 `parent` フィールドを照合して親子関係を再帰的に繋ぎ直す必要がある。
この「再帰的構築」こそが、CPUとメモリを浪費する真犯人だ。解決策はシンプルだ。「構築済みツリーをシリアライズしてRedisに置く」。これに尽きる。
—
2. 堅牢な実装:`TaxonomyTreeBuilder` パターン
バグを埋め込まないための鉄則は、「DB更新時にキャッシュをパージするイベント駆動設計」である。`clean_term_cache` をフックし、整合性を担保する。
以下に、実務でそのまま使える堅牢なクラス設計を示す。
/
class TaxonomyTreeCache {
private const CACHE_KEY = ‘app_tax_tree_%s’; // %s = taxonomy_name
public static function get_tree(string $taxonomy): array {
$key = sprintf(self::CACHE_KEY, $taxonomy);
$cached = wp_cache_get($key, ‘tax_cache’);
if (false !== $cached) {
return $cached;
}
// キャッシュがない場合、構築して保存
$tree = self::build_recursive_tree($taxonomy);
wp_cache_set($key, $tree, ‘tax_cache’, HOUR_IN_SECONDS);
return $tree;
}
private static function build_recursive_tree(string $taxonomy, int $parent = 0): array {
$terms = get_terms([‘taxonomy’ => $taxonomy, ‘parent’ => $parent, ‘hide_empty’ => false]);
$branch = [];
foreach ($terms as $term) {
$branch[] = [
‘term_id’ => $term->term_id,
‘name’ => $term->name,
‘children’ => self::build_recursive_tree($taxonomy, $term->term_id)
];
}
return $branch;
}
// 更新時のキャッシュ破棄(ここが設計の肝)
public static function purge_cache($term_id, $taxonomy) {
wp_cache_delete(sprintf(self::CACHE_KEY, $taxonomy), ‘tax_cache’);
}
}
// フックの登録
add_action(‘created_term’, [‘TaxonomyTreeCache’, ‘purge_cache’], 10, 2);
add_action(‘edited_term’, [‘TaxonomyTreeCache’, ‘purge_cache’], 10, 2);
add_action(‘delete_term’, [‘TaxonomyTreeCache’, ‘purge_cache’], 10, 2);
—
3. パフォーマンス最適化の極意
このコードをプロダクションで運用する際、以下の3点に注意を払う必要がある。これこそが「動くコード」と「運用に耐えるシステム」の境界線だ。
① プリフェッチの戦略
`build_recursive_tree` 内で何度も `get_terms` を呼ぶのは避けるべきだ。理想は、タクソノミーの全データを一度のクエリで取得し、PHP側でハッシュマップ(連想配列)を作成して、リンクを繋ぎ変える「単一パスでのツリー構築」だ。再帰回数を減らすことが、メモリ消費の抑制に繋がる。
② Redisのシリアライズコスト
巨大なタクソノミーの場合、`serialize()` は重い。もし階層が数千単位に及ぶなら、JSONへの変換や、Redisのハッシュ型(`HSET`)を用いて構造を保存する手法も検討せよ。WordPressの `wp_cache_set` はシリアライズを自動で行うが、データサイズが大きすぎるとRedisの処理時間がボトルネックになる。
③ 整合性とRace Condition
`created_term` フックでのパージは重要だが、トラフィックが激しいサイトでは「パージしてから再構築されるまでの間」に大量のリクエストがDBを叩く「キャッシュ雪崩(Cache Stampede)」が起きる。
これを防ぐには、「キャッシュのパージ時に、即座に非同期処理(Action Schedulerなど)でキャッシュを再構築する」のがプロの解法だ。
—
最後に:エンジニアへの提言
WordPressにおいて「データベースへのアクセスを減らす」ことは、単なる高速化ではない。それはシステムの寿命を延ばすことと同義だ。
今回紹介したアプローチは、Redisという強力な武器を使い、DBへの負荷を最小限に抑えつつ、アプリケーションのレスポンスをミリ秒単位で短縮する。設計の際は、「なぜキャッシュが消えるのか」「どうすればキャッシュミス時のコストを最小化できるか」を常に自問自答してほしい。
コードは書くだけなら誰でもできる。「スケールを前提とした堅牢な設計」こそが、君たちを「ただのコーダー」から「真のエンジニア」へ引き上げる。次のレビューでは、このアーキテクチャを堂々と提案してくれ。期待している。