Using tax_query with hierarchical custom taxonomies

I have been working on a site with fairly complex cataloguing and searching requirements, and ran into a few oversights (of my own) when setting tax_query for WP_Query within the pre_get_posts hook. This may not be the exact problem you're dealing with, but it might serve as a If you're just after the technical details, skip past the next section.

The context

This particular site includes a database of organisations (using a custom post type). Amongst the metadata attached to these organisations, there is a custom taxonomy called "service-area", which is the particular region(s) of Victoria that the organisation will deal with. This taxonomy is configured to be hierarchical, and has statewide, region and local government areas (LGAs) as the levels. Here's a sample:

  • Statewide
  • Hume
    • Alpine Shire
    • City of Greater Shepparton
  • Gippsland
    • Bass Coast Shire
    • Shire of Baw Baw
  • Metropolitan
    • etc...

When administering the organisations, it should behave as simply as possible. If it's statewide, just tick Statewide. If it services a whole region, tick just the region name.

In the front end, visitors have two filtering options: they can click on a map of regions or select from a drop down which includes all of the taxonomy terms. If a selects Hume, Wordpress should return all of the organisations that service any of the LGAs within Hume, Hume itself, or any that are marked as statewide.

Without any change to the process, any searches for Hume won't return any statewide organisations, severely limiting the search results.

The approach

I set up a filter using the pre_get_posts hook to modify the WP_Query object when someone selected a term. We want to modify the tax_query property.

Here's what I started with:

add_filter('pre_get_posts','tyson_add_statewide_orgs');
function tyson_add_statewide_orgs(&$wp_query) {
    if (!get_query_var('service-area')) return;
    $requested_area = get_query_var('service-area');
    $tax_query = array(
               array(
                 'taxonomy'=>'service-area',
                 'field'=>'slug',
                 'terms'=>array($requested_area,<strong>'statewide'</strong>),
                 'operator'=>'IN'
               )
             );
    $wp_query->set('tax_query',$tax_query);
}

Always remember that tax_query should be an array of arrays, because it can also take a 'relation' key which sets the rules of play when multiple tax queries are used (we're going to use do that in a second).

It just didn't seem to work. A search for any taxonomy would include every organisation. I hooked into the posts_where filter to confirm that the resulting SQL was limiting the terms as I'd wanted. I considered throwing it out and modifying the posts_where SQL myself, but that's a messy solution.

The solution

The answer was another key in the tax_query array, include_children. It specifies whether child terms of the queried term are included in the final search, and it's set to true by default. When I was adding the statewide term in hopes of pulling just those orgs that service statewide, it was adding every child term of statewide (all of them), thereby negating the user's choice.

I therefore needed to:

  • include child terms for the user-chosen term, and

  • exclude child terms for statewide

To do so, I needed to start using multiple branches to the tax_query.

add_filter('pre_get_posts','tyson_add_statewide_orgs');
function tyson_add_statewide_orgs(&$wp_query) {
    if (!get_query_var('service-area')) return;
    $requested_area = get_query_var('service-area');
    $tax_query = array(
               array(
                 'taxonomy'=>'service-area',
                 'field'=>'slug',
                 'terms'=>array($requested_area),
                 'operator'=>'IN'
               ),
               array(
                 'taxonomy'=>'service-area',
                 'field'=>'slug',
                 'terms'=>array('statewide'),
                 'operator'=>'IN', 
                 <strong>'include_children'=>0</strong>
               ),
               'relation'=>'OR'
             );
    $wp_query->set('tax_query',$tax_query);
}

Notice how it's split into two arrays, because the 'statewide' array needs includes_children to be false (or 0). Also, although relation is 'OR' by default, I included it just to be explicit.

Low and behold, it works like a charm.