Taxonomy Queries

tax_query filters posts by terms using indexed term tables, with field, terms, operator and relation.

Syntax'tax_query' => array( 'relation' => 'AND', array( 'taxonomy' => 'genre', 'field' => 'slug', 'terms' => array( 'noir' ), 'operator' => 'IN' ) )

tax_query filters a post query by taxonomy terms. It is parsed by WP_Tax_Query, which joins wp_term_relationships and wp_term_taxonomy. Unlike a meta query, these joins hit indexed columns, so taxonomy filtering is the cheap way to slice a large post set.

Structure

tax_query is an array of clauses, optionally prefixed by 'relation', which accepts 'AND' or 'OR' and defaults to 'AND'. Each clause takes taxonomy, terms, and optionally field, operator and include_children. field is what terms is matched against: 'term_id' (the default), 'slug', 'name' or 'term_taxonomy_id'. operator accepts 'IN' (the default), 'NOT IN', 'AND', 'EXISTS' and 'NOT EXISTS'. include_children defaults to true and only matters on hierarchical taxonomies.

AND versus IN

These are the two operators people confuse. 'IN' with three terms matches a post in any of them. 'AND' matches only posts carrying all three. The clause-level relation is a different axis again: it joins whole clauses, usually across different taxonomies.

Shorthand and longhand

The built-in shortcuts are shorthand for a tax_query clause. 'category_name' => 'news' is the same query as a clause with taxonomy 'category', field 'slug', terms 'news'; 'tag' does the same for post_tag. Custom taxonomies also accept 'my_tax' => 'slug' at the top level. Use the full form once you need an operator or a second clause.

Example

Example · php
<?php
// Reviews tagged 'noir' or 'thriller', but not anything in the 'spoilers' tag.
$reviews = new WP_Query(
	array(
		'post_type'      => 'review',
		'posts_per_page' => 10,

		'tax_query'      => array(
			'relation' => 'AND', // both clauses must hold

			array(
				'taxonomy'         => 'genre',
				'field'            => 'slug', // default is 'term_id'
				'terms'            => array( 'noir', 'thriller' ),
				'operator'         => 'IN',   // any of these terms; 'AND' = all of them
				'include_children' => true,   // default; hierarchical taxonomies only
			),
			array(
				'taxonomy' => 'post_tag',
				'field'    => 'slug',
				'terms'    => array( 'spoilers' ),
				'operator' => 'NOT IN',
			),
		),
	)
);

When to use it

  • Build a faceted filter where each selected term narrows the results
  • List posts in a category while excluding one tag
  • Find posts that have no term assigned in a required taxonomy
  • Restrict a landing page to a parent term without pulling in child terms

More examples

Taxonomy shortcuts and their tax_query longhand

The shortcuts read well for a single category or tag and are what you will see in most themes. Move to the longhand as soon as you need NOT IN, a custom taxonomy alongside a built-in one, or an OR relation between clauses.

Example · php
<?php
// Shorthand: top-level taxonomy arguments.
$q = new WP_Query(
	array(
		'category_name' => 'news',
		'tag'           => 'php',
	)
);

// Longhand: the same query written out. Note field => 'slug' —
// the shortcuts match on slug, while tax_query defaults to 'term_id'.
$q = new WP_Query(
	array(
		'tax_query' => array(
			'relation' => 'AND',
			array(
				'taxonomy' => 'category',
				'field'    => 'slug',
				'terms'    => 'news',
			),
			array(
				'taxonomy' => 'post_tag',
				'field'    => 'slug',
				'terms'    => 'php',
			),
		),
	)
);

// 'cat' => 5 is the term_id equivalent of 'category_name'.

IN versus AND on the same terms

This is the single most common tax_query bug: a faceted filter built with the default 'IN' returns everything the user did not ask for. Use 'AND' for cumulative filters where each checkbox narrows the results.

Example · php
<?php
$terms = array( 'php', 'mysql', 'linux' );

// IN: posts tagged with ANY of the three. Wide result set.
$any = new WP_Query(
	array(
		'tax_query' => array(
			array(
				'taxonomy' => 'post_tag',
				'field'    => 'slug',
				'terms'    => $terms,
				'operator' => 'IN', // the default
			),
		),
	)
);

// AND: posts tagged with ALL three. Narrow, and a heavier join.
$all = new WP_Query(
	array(
		'tax_query' => array(
			array(
				'taxonomy' => 'post_tag',
				'field'    => 'slug',
				'terms'    => $terms,
				'operator' => 'AND',
			),
		),
	)
);

Turning off include_children on a hierarchical taxonomy

include_children is true by default, which surprises people building a landing page that should list only the section's own posts. NOT EXISTS is the editorial-cleanup query: everything missing a required classification.

Example · php
<?php
// 'category' is hierarchical, so a query for the parent 'Hardware'
// also returns posts filed only under its children by default.
$parent_only = new WP_Query(
	array(
		'post_type' => 'post',
		'tax_query' => array(
			array(
				'taxonomy'         => 'category',
				'field'            => 'slug',
				'terms'            => 'hardware',
				'include_children' => false, // exact term only
			),
		),
	)
);

// Also useful: find posts with no term at all in a taxonomy.
$untagged = new WP_Query(
	array(
		'post_type' => 'product',
		'tax_query' => array(
			array(
				'taxonomy' => 'brand',
				'operator' => 'NOT EXISTS', // no 'terms' key needed
			),
		),
	)
);

Discussion

  • Be the first to comment on this lesson.