Пользовательские таксономии (Custom Taxonomies) в WordPress — это нормализованная реляционная подсистема классификации и фасетной группировки сущностей (WP_Post, WP_User, кастомных объектов). В отличие от таблицы метаданных wp_postmeta (построенной по неиндексируемому текстовому паттерну Entity-Attribute-Value), архитектура таксономий спроектирована вокруг трех специализированных таблиц с жесткими целочисленными B-Tree индексами:
wp_terms (Словарь уникальных лексем): Хранит базовую идентификацию терма (term_id BIGINT UNSIGNED, name VARCHAR(200), slug VARCHAR(200)). Индексирована по slug и name.wp_term_taxonomy (Контекст классификации): Связывает лексему term_id с конкретной таксономией (taxonomy VARCHAR(32)), хранит описание, родительский элемент иерархии (parent BIGINT UNSIGNED) и денормализованный счетчик привязанных объектов (count BIGINT).wp_term_relationships (Матрица связей многие-ко-многим): Ультракомпактная связующая таблица из трех целочисленных столбцов (object_id, term_taxonomy_id, term_order). Ее первичный ключ PRIMARY KEY (object_id, term_taxonomy_id) и обратный индекс KEY term_taxonomy_id (term_taxonomy_id) позволяют движку InnoDB выполнять пересечения миллионов связей в памяти (Index-Only Scan) за 1–3 миллисекунды.wp_termmeta (Атрибуты терма): Расширяет термы дополнительными полями (иконки брендов, SEO-метаданные, цветовые токены) с автоматической пакетной предзагрузкой через update_term_meta_cache().Регистрация таксономии через register_taxonomy() на хуке init создает экземпляр класса WP_Taxonomy в глобальном реестре $wp_taxonomies, компилирует правила ЧПУ в WP_Rewrite и инициализирует контроллер WP_REST_Terms_Controller для работы в блочном редакторе Gutenberg и Headless-приложениях.
Архитектуру таксономий следует воспринимать как индексированный граф связей между документами (object_id) и узлами классификатора (term_taxonomy_id), где SQL-компилятор WP_Tax_Query строит пересечения множеств без чтения тяжелых таблиц контента.
[wp_posts] (ID: 1050)
│
│ 1. JOIN по целочисленному индексу (object_id)
▼
[wp_term_relationships]
├── object_id: 1050 ◄──► term_taxonomy_id: 42 (PRIMARY KEY)
│
│ 2. JOIN по первичному ключу (term_taxonomy_id)
▼
[wp_term_taxonomy]
├── term_taxonomy_id: 42
├── term_id: 18
├── taxonomy: 'tech_stack'
├── parent: 0
└── count: 340 (денормализованный кэш количества записей)
│
├───────────────────────────────┐
▼ ▼
[wp_terms] [wp_termmeta]
├── term_id: 18 ├── term_id: 18
├── name: 'TypeScript 5.8' ├── meta_key: '_badge_color'
└── slug: 'typescript' └── meta_value: '#3178C6'Вызов register_taxonomy() на хуке init (приоритет 9, до CPT). Инстанцирование WP_Taxonomy в $wp_taxonomies.
Связывание с типами записей и активация WP_REST_Terms_Controller (show_in_rest => true) для панелей Gutenberg.
Трансляция дерева условий tax_query (AND/OR) в SQL-клаузы JOIN и WHERE по таблице wp_term_relationships.
Выполнение пересечения множеств ID записей в InnoDB строго по покрывающему индексу (object_id, term_taxonomy_id).
Пакетный прайминг объектов WP_Term и их метаданных (wp_termmeta) в Persistent Object Cache за один сетевой раунд.
Объявление конфигурации таксономии, ярлыков и REST-схемы до инициализации зависимых типов записей.
Флаг show_in_rest => true обязателен для отображения панели выбора термов в Gutenberg.
При создании терма ядро проверяет уникальность slug в wp_terms и создает запись контекста в wp_term_taxonomy.
Механизм Term Splitting гарантирует изоляцию термов разных таксономий.
Связь поста с термом записывается в wp_term_relationships как пара чисел (object_id, term_taxonomy_id).
Одновременно пересчитывается поле count в таблице wp_term_taxonomy и сбрасывается объектный кэш.
Формирование оптимизированных INNER JOIN по составному первичному ключу таблицы связей.
Параметр include_children => false исключает рекурсивное чтение дочерних веток.
Все связанные поля из wp_termmeta загружаются в память за один запрос WHERE term_id IN (...).
Обеспечивает мгновенный доступ к иконкам и цветам термов без N+1 запросов.
Почему фильтрация сущностей через WP_Tax_Query по таблице wp_term_relationships на миллионах строк выполняется за миллисекунды, в отличие от фильтрации по wp_postmeta?
meta_query на таксономии снижает время SQL-выборки с 4.2 секунд до 8 миллисекунд.public => false) для внутренней маршрутизации: Использование невидимых в публичном UI таксономий (как product_visibility в WooCommerce для маркеров featured, outofstock, exclude-from-catalog) вместо медленных булевых флагов в wp_postmeta.industry, region, compliance_standard), одновременно объединяющих разные типы записей (case_study, whitepaper, news, video) в единые тематические хабы.register_term_meta: Привязка к термам таксономии SVG-иконок, цветовых палитр и JSON-конфигураций с валидацией схемы и отдачей напрямую в Gutenberg Block Bindings и WPGraphQL.Главное архитектурное решение при проектировании базы данных в WordPress — проведение четкой границы между таксономиями и произвольными мета-полями:
WP_Tax_Query: параметр include_children#По умолчанию для любой иерархической таксономии (hierarchical => true) класс WP_Tax_Query устанавливает параметр 'include_children' => true. Это означает, что при запросе родительской рубрики ядро предварительно извлекает все дерево дочерних подкатегорий и подставляет их в огромный оператор WHERE term_taxonomy_id IN (12, 15, 19, ... 450). На глубоких деревьях каталогов это раздувает SQL-запрос. Установка 'include_children' => false при точной фильтрации экономит до 40% времени компиляции запроса.
Ниже представлен production-ready модуль на PHP 8.4+, регистрирующий сквозную таксономию технологического стека с типизированными мета-полями терма (register_term_meta), экспозицией в REST API / Gutenberg и высокоскоростным методом фасетной фильтрации.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Taxonomy & Facet Engine
* Description: Регистрация сквозных таксономий, типизированных мета-полей термов и высокоскоростных фасетных выборок.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\Taxonomies;
use WP_Query;
if (!defined('ABSPATH')) {
exit;
}
final readonly class TechStackTaxonomy
{
public const TAXONOMY = 'tech_stack';
public const OBJECT_TYPES = ['case_study', 'post'];
public static function init(): void
{
// Приоритет 9: регистрируем таксономию гарантированно ДО регистрации зависимых CPT (приоритет 10)
add_action('init', self::registerTaxonomy(...), 9);
add_action('init', self::registerTermMetadata(...), 10);
}
public static function registerTaxonomy(): void
{
$labels = [
'name' => 'Технологический стек',
'singular_name' => 'Технология',
'search_items' => 'Найти технологию',
'all_items' => 'Весь стек технологий',
'parent_item' => 'Категория стека',
'parent_item_colon' => 'Категория стека:',
'edit_item' => 'Редактировать технологию',
'update_item' => 'Обновить технологию',
'add_new_item' => 'Добавить технологию',
'new_item_name' => 'Название новой технологии',
'menu_name' => 'Стек технологий',
];
$args = [
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_in_rest' => true, // Обязательно для Gutenberg и Headless REST API
'rest_base' => 'tech-stack',
'query_var' => true,
'rewrite' => [
'slug' => 'stack',
'with_front' => false,
'hierarchical' => false, // Плоский URL /stack/typescript/ вместо длинных цепочек
],
];
register_taxonomy(self::TAXONOMY, self::OBJECT_TYPES, $args);
}
/**
* Строгая регистрация мета-полей терма с валидацией JSON Schema для REST API и Block Bindings.
*/
public static function registerTermMetadata(): void
{
register_term_meta(self::TAXONOMY, '_stack_badge_hex', [
'type' => 'string',
'description' => 'HEX-код акцентного цвета бейджа технологии',
'single' => true,
'default' => '#06B6D4',
'sanitize_callback' => static fn(mixed $val): string => sanitize_hex_color((string) $val) ?: '#06B6D4',
'show_in_rest' => [
'schema' => [
'type' => 'string',
'pattern' => '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',
],
],
]);
}
/**
* Выполнение высокопроизводительной фасетной выборки по пересечению нескольких термов
* с отключением избыточного обхода дочерних деревьев (include_children => false).
*
* @param list<string> $requiredSlugs
* @return list<int>
*/
public static function queryPostIdsByFacetSlugs(array $requiredSlugs, int $limit = 24): array
{
if ($requiredSlugs === []) {
return [];
}
$query = new WP_Query([
'post_type' => 'case_study',
'post_status' => 'publish',
'posts_per_page' => min(max($limit, 1), 100),
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => true,
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => self::TAXONOMY,
'field' => 'slug',
'terms' => array_map('sanitize_title', $requiredSlugs),
'operator' => 'AND',
'include_children' => false, // Подавляем лишний SQL-запрос дерева потомков
],
],
]);
/** @var list<int> */
return array_map('intval', $query->posts);
}
}
TechStackTaxonomy::init();При проектировании каталога с глубокой иерархией категорий запрос WP_Query с tax_query неожиданно генерирует тяжелый SQL с сотнями ID в условии IN. Какая настройка решает эту проблему?
В чем суть ошибки: Разработчик создает текстовое или сериализованное поле в wp_postmeta (например, brand = 'Apple' или stack = '["React", "PHP"]') и строит каталог с фильтрацией через meta_query с оператором LIKE.
Почему разработчики так делают: Кажется проще добавить еще одно поле в группу ACF, чем регистрировать отдельную таксономию через register_taxonomy().
Что происходит в продакшене: Таблица wp_postmeta не имеет индекса по значениям LONGTEXT. При фильтрации по 3–4 параметрам MySQL генерирует серию тяжелых INNER JOIN wp_postmeta, создавая временные таблицы на диске (Using temporary; Using filesort). На базе из 50 000 записей запрос начинает выполняться 3–7 секунд, блокируя пул воркеров PHP-FPM.
Как правильно: Любой атрибут, по которому выполняется фильтрация, группировка или построение списков, ОБЯЗАН проектироваться как таксономия (включая скрытые системные таксономии с 'public' => false).
В чем суть ошибки: Выполнение десятков tax_query на странице каталога без явного указания 'include_children' => false.
Почему разработчики так делают: Незнание того, что WP_Tax_Query по умолчанию устанавливает include_children в true для всех иерархических таксономий.
Что происходит в продакшене: Перед компиляцией основного запроса ядро вызывает get_term_children() для каждого переданного терма, разворачивая гигантские массивы дочерних ID и раздувая условие IN (...) до сотен элементов.
Как правильно: Всегда явно передавать 'include_children' => false, если бизнес-логика требует точного совпадения с выбранными узлами фильтра.
В чем суть ошибки: Вызов register_taxonomy() на хуке init с приоритетом 20, в то время как связанный с ней CPT регистрируется с дефолтным приоритетом 10 и использует плейсхолдер таксономии в своем rewrite['slug'].
Почему разработчики так делают: Произвольная расстановка приоритетов хуков в разных классах плагина.
Что происходит в продакшене: В момент компиляции правил ЧПУ для CPT таксономия еще не существует в глобальном реестре $wp_taxonomies. Правила маршрутизации генерируются некорректно, а связь в объектной модели требует дополнительного вызова register_taxonomy_for_object_type().
Как правильно: Регистрировать таксономии с более ранним приоритетом (add_action('init', ..., 9)), чем зависимые от них типы записей (add_action('init', ..., 10)).
В чем суть ошибки: Получение массива термов через get_terms(['update_term_meta_cache' => false]) или кастомный SQL с последующим вызовом get_term_meta($term->term_id, '_icon', true) внутри цикла foreach.
Почему разработчики так делают: Отключение кэша ради мнимой экономии памяти при выборке термов.
Что происходит в продакшене: На меню из 80 категорий с иконками выполняется 80 одиночных SQL-запросов SELECT meta_key, meta_value FROM wp_termmeta WHERE term_id = X.
Как правильно: Оставлять параметр 'update_term_meta_cache' => true в get_terms() (активен по умолчанию) либо вызывать update_termmeta_cache($termIds) перед циклом рендеринга.