1

I have a custom post type of Hotel. Hotels can be can be categorised with the taxonomy Resort.

On my search page I need to be able to list hotels and taxonomy terms in the results, as separate results. eg:

Resort Name
Hotel Name
Hotel Name
Hotel Name

Resorts should come first, followed by hotels and anything else. As far as I know taxonomy terms are not shown by default in WordPress search.

In my functions.php I'm currently limiting the search to specific post types:

function filter_search($query) {
// Don't run in admin area
if(!is_admin()) {
    // Limit search to posts
    if($query->is_main_query() && $query->is_search()) {
        $query->set('post_type', array('hotel', 'post', 'activities', 'page'));
    }

    // Return query
    return $query;
  }
}

add_filter('pre_get_posts', 'filter_search');

Then in my search.php I am grouping the results by post type:

<?php $types = array('hotel', 'post', 'activities', 'page');
foreach( $types as $type ){

echo '<div class="results"> '; ?>

<?php while( have_posts() ){
  the_post();
  if( $type == get_post_type() ){ ?>

    <div class="result">
      <div class="result-inner">
        <h2>
          <?php if ( $type ==  "hotel"  ) { ?>
            Hotel: 
          <?php } elseif ( $type == "activities" ) { ?>
            Activity: 
          <?php } elseif ( $type == "post" ) { ?>
            Blog Post: 
          <?php } elseif ( $type == "page" ) { ?>
            Page: 
          <?php } else { ?>
            Page: 
          <?php } ?>

          <a href="<?php the_permalink() ?>"><?php the_title(); ?></a>
        </h2>

        <p class="blog-more"><a href="<?php the_permalink() ?>" class="button">View Page</a></p>

        </div>
      </div>
    <?php }
} 
rewind_posts();
echo '</div> ';
} ?>

What I'm struggling with is how to tell WordPress to show the taxonomy term (Resort) in the results as it's own result. Can anybody help?

UPDATE: I'm happy to preserve the default WordPress search order. The results don't have to be grouped by taxonomy term. I just need to be able to spit out the term as a result itself, with a link to the term (and be counted in the search count query). So if a search matches a taxonomy term that should come first, followed by the other results in whatever order.

1
+50

I just need to be able to spit out the term as a result itself, with a link to the term (and be counted in the search count query). So if a search matches a taxonomy term that should come first, followed by the other results in whatever order.

WP_Query::$posts are WP_Post objects and can't include (or be mixed with) term/WP_Term objects.

However, there's a way to achieve what you want — use get_terms() to search for the taxonomy terms and modify the WP_Query's posts_per_page and offset parameters to make the matched terms as if they're part of the actual search results (which are posts).

Note though, for now, get_terms()/WP_Term_Query supports single search keywords only — i.e. foo, bar is treated as one keyword — in WP_Query, that would be two keywords (foo and bar).

The Code/Steps

In functions.php:

Add this:

function wpse342309_search_terms( $query, $taxonomy ) {
    $per_page = absint( $query->get( 'posts_per_page' ) );
    if ( ! $per_page ) {
        $per_page = max( 10, get_option( 'posts_per_page' ) );
    }

    $paged = max( 1, $query->get( 'paged' ) );
    $offset = absint( ( $paged - 1 ) * $per_page );
    $args = [
        'taxonomy'   => $taxonomy,
//      'hide_empty' => '0',
        'search'     => $query->get( 's' ),
        'number'     => $per_page,
        'offset'     => $offset,
    ];

    $query->terms = [];
    $terms = get_terms( $args );
    if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
        $query->terms = $terms;
    }

    $args['offset'] = 0; // always 0
    $args['fields'] = 'count';
    $query->found_terms = get_terms( $args );

    $query->term_count = count( $query->terms );
    $query->terms_per_page = $per_page; // for WP_Query::$max_num_pages
    $query->is_all_terms = ( (int) $per_page === $query->term_count );

    $query->set( 'posts_per_page', max( 1, $per_page - $query->term_count ) );
    $query->set( 'offset', $query->term_count ? 0 :
        max( 0, $offset - $query->found_terms ) );
}

In the filter_search(), add wpse342309_search_terms( $query, 'resort' ); after this line:

$query->set( 'post_type', array( 'hotel', 'post', 'activities', 'page' ) );

In search.php, for the main WordPress query:

<?php if ( have_posts() ) :

    $total = $wp_query->found_posts + $wp_query->found_terms;
    $wp_query->max_num_pages = ceil( $total / $wp_query->terms_per_page );

    echo '<div class="results">';

    // Display terms matching the search query.
    if ( ! empty( $wp_query->terms ) ) {
        global $term; // for use in the template part (below)
        foreach ( $wp_query->terms as $term ) {
            get_template_part( 'template-parts/content', 'search-term' );
        }
    }

    // Display posts matching the search query.
    if ( ! $wp_query->is_all_terms ) {
        while ( have_posts() ) {
            the_post();
            get_template_part( 'template-parts/content', 'search-post' );
        }
    }

    echo '</div><!-- .results -->';

    // Display pagination.
    //the_posts_pagination();
    echo paginate_links( [
        'total'   => $wp_query->max_num_pages,
        'current' => max( 1, get_query_var( 'paged' ) ),
    ] );

else :
    echo '<h1>No Posts Found</h1>';

endif; // end have_posts()
?>

In search.php, for a custom WordPress query:

You should always manually call wpse342309_search_terms().

<?php
$my_query = new WP_Query( [
    's' => 'foo',
    // ... your args here ...
] );

// Manually apply the terms search.
wpse342309_search_terms( $my_query, 'resort' );

if ( $my_query->have_posts() ) :

    $total = $my_query->found_posts + $my_query->found_terms;
    $my_query->max_num_pages = ceil( $total / $my_query->terms_per_page );

    echo '<div class="results">';

    // Display terms matching the search query.
    if ( ! empty( $my_query->terms ) ) {
        global $term; // for use in the template part (below)
        foreach ( $my_query->terms as $term ) {
            get_template_part( 'template-parts/content', 'search-term' );
        }
    }

    // Display posts matching the search query.
    if ( ! $my_query->is_all_terms ) {
        while ( $my_query->have_posts() ) {
            $my_query->the_post();
            get_template_part( 'template-parts/content', 'search-post' );
        }
    }

    echo '</div><!-- .results -->';

    // Display pagination.
    echo paginate_links( [
        'total'   => $my_query->max_num_pages,
        'current' => max( 1, get_query_var( 'paged' ) ),
    ] );

else :
    echo '<h1>No Posts Found</h1>';

endif; // end have_posts()
?>

Template Part 1: template-parts/content-search-term.php

<?php global $term;
$term_link = get_term_link( $term, 'resort' ); ?>

<div class="result">
    <div class="result-inner">
        <h2><a href="<?php echo esc_url( $term_link ); ?>"><?php echo esc_html( $term->name ); ?></a></h2>

        <p class="blog-more"><a href="<?php echo esc_url( $term_link ); ?>" class="button">View Page</a></p>
    </div>
</div>

Template Part 2: template-parts/content-search-post.php

<?php $type = get_post_type(); ?>

<div class="result">
    <div class="result-inner">
        <h2>
            <?php if ( $type ==  "hotel"  ) { ?>
                Hotel:
            <?php } elseif ( $type == "activities" ) { ?>
                Activity:
            <?php } elseif ( $type == "post" ) { ?>
                Blog Post:
            <?php } elseif ( $type == "page" ) { ?>
                Page:
            <?php } else { ?>
                Page:
            <?php } ?>

            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
        </h2>

        <p class="blog-more"><a href="<?php the_permalink(); ?>" class="button">View Page</a></p>
    </div>
</div>
  • Thanks @sally-cj. I think it's almost there but this is giving me a server error now. I think it might be infinite looping. If I remove the foreach ( $types as $type ) { from the content-search.php it lists out the same resort name multiple times (the correct amount of times - there should be 7 results and it lists the same resort 7 times). – Xav Jul 20 at 13:20
  • Check the updated answer. (and remove the template-parts/content-search.php file) – Sally CJ Jul 20 at 15:33
  • Thank you so much @sally-cj, this fixed it! The results are now as they should be. The only thing that's a bit off is the result count which doesn't seem to include any resort results. I'm using this outside of the loop: <?php $allsearch = new WP_Query("s=$s&showposts=-1"); $key = wp_specialchars($s, 1); $count = $allsearch->post_count; echo $count . ' '; _e('pages found'); wp_reset_query(); ?> – Xav Jul 21 at 15:20
  • I've updated all the code, so be sure to update yours. (check the answer starting from the "The Code/Steps" line) Let me know if you have questions regarding the updated code. – Sally CJ Jul 21 at 18:13
  • 1
    All working now! Thank you so much for your help :) – Xav Jul 22 at 13:26
2

If you mean to have a display like this:

Hotels: Resort 1 <- "Resort 1" is a term in the "resort" taxonomy
  Sample Hotel
  Sample Hotel 2
  Sample Hotel 4 <- This post is assigned to both "Resort 1" and "Resort 2"

Hotels: Resort 2 <- "Resort 2" is a term in the "resort" taxonomy
  Sample Hotel 3
  Sample Hotel 4 <- This post is assigned to both "Resort 1" and "Resort 2"

Blog Posts
  Sample Post
  Sample Post 2

Activities
  Sample Activity
  Sample Activity 2

Pages
  Sample Page

Then this can give you that, where we first group the posts by their post type or by the "resort" term name for "hotel" posts: (this is inside search.php)

<?php if ( have_posts() ) : ?>

    <div class="results">

        <?php
        $_posts = $wp_query->posts;
        $posts_arr = [];
        $resorts = [];

        foreach ( $_posts as $i => $p ) {
            // Group "hotel" posts by the "resort" term name.
            if ( 'hotel' === $p->post_type ) {
                $terms = get_the_terms( $p, 'resort' );
                if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
                    foreach ( $terms as $term ) {
                        if ( ! isset( $resorts[ $term->name ] ) ) {
                            $resorts[ $term->name ] = [];
                        }
                        $resorts[ $term->name ][] =& $_posts[ $i ];
                    }
                }
            // Group other posts by their type.
            } else {
                if ( ! isset( $posts_arr[ $p->post_type ] ) ) {
                    $posts_arr[ $p->post_type ] = [];
                }
                $posts_arr[ $p->post_type ][] =& $_posts[ $i ];
            }
        }
        $posts_arr['hotel'] = ! empty( $resorts );

        // List of post_type => Heading.
        $types = [
            'hotel'      => 'Hotels',
            'post'       => 'Blog Posts',
            'activities' => 'Activities',
            'page'       => 'Pages',
        ];

        foreach ( $types as $type => $heading ) {
            if ( ! isset( $posts_arr[ $type ] ) ) {
                continue;
            }

            // Display "hotel" posts.
            if ( 'hotel' === $type ) {
                foreach ( $resorts as $name => $ps ) {
                    echo '<h2>Hotels: ' . $name . '</h2>';
                    foreach ( $ps as $post ) {
                        setup_postdata( $post );
                        get_template_part( 'template-parts/content', 'search' );
                    }
                }
            // Display posts in other post types.
            } else {
                echo '<h2>' . $heading . '</h2>';
                foreach ( $posts_arr[ $type ] as $post ) {
                    setup_postdata( $post );
                    get_template_part( 'template-parts/content', 'search' );
                }
            }
        }

        unset( $_posts, $posts_arr, $resorts );
        ?>

    </div><!-- .results -->

<?php endif; // end have_posts() ?>

And the template part (content-search.php inside wp-content/themes/your-theme/template-parts):

<div <?php post_class( 'result' ); ?>>
    <div class="result-inner">
        <h4><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h4>
        <p class="blog-more"><a href="<?php the_permalink(); ?>" class="button">View Page</a></p>
    </div>
</div>
  • Thank you @sally-cj. This is nice but I need pagination as kuchenundkakao suggests. And I want the term to be counted as a result itself. I have updated my question. – Xav Jul 19 at 13:38
  • Perhaps my other answer can help? – Sally CJ Jul 19 at 19:05
1

If you want to group your search results by a custom taxonomy, you can either group your posts AFTER the query like Sally CJ proposed, but this would only work correctly if you have all of your search results on one page.

To order search results AND use pagination, you have to alter your MySQL Query, so that's what we're going to do.

Step 1: Add a filter to "posts_clauses" within the pre_get_posts action to change the generated Query:

add_action('pre_get_posts','setup_my_query_interception');

function setup_my_query_interception($query){
    if(!is_admin() && $query->is_search() && $query->is_main_query()){
     add_filter( 'posts_clauses', 'order_by_taxonomy_intercept_query_clauses', 20, 1 );
  }
}

Step 2: In the order_by_taxonomy_intercept_query_clauses function, you enhance the Query:

function order_by_taxonomy_intercept_query_clauses($pieces){
        global $wpdb;
        //First we add the taxonomy and term tables to our joins...
        $pieces['join'].=" LEFT JOIN $wpdb->term_relationships trt ON ($wpdb->posts.ID = trt.object_id) LEFT JOIN $wpdb->term_taxonomy ttt ON (trt.term_taxonomy_id = ttt.term_taxonomy_id) INNER JOIN $wpdb->terms termts ON (termts.term_id = ttt.term_id)";
        //Now we add a WHERE condition that we only want Resort Terms
        $pieces['where'].=" AND ttt.taxonomy = 'resort'"; //change this, if your taxonomy type is named different
        //At last, we tell the orderby to FIRST order by the term title, after that use the wanted orderby
        $pieces['orderby']="termts.name ASC, ".$pieces['orderby'];
        //Remove the filter so it only runs once in our main query
        remove_filter('posts_clauses', 'order_by_taxonomy_intercept_query_clauses');
        return $pieces;
}

Step 3: Your search results will now be ordered by resorts first. If you want to echo the Resort name BEFORE the results, you will have to do something like this in your search.php or index.php:

before the loop:

if(is_search()){
    global $activetax;
    $activetax = "";
    echo '<div class="mywrapper">
}

in the loop:

if(is_search()){
$resorts = get_the_terms(get_the_ID(),'resort');
if(is_array($resorts)){
    if(!($activetax==$resorts[0]->name)){
            $activetax = $resorts[0]->name;
            ?>
            </div>
            <h2 id="tax_id_<?php echo $resorts[0]->term_id; ?>" class="resort-header"><?php echo $activetax; ?></h2>
            <div class="mywrapper">
            <?php
    }
}
}
get_template_part('content'); 

after the loop:

if(is_search()){
 echo "</div>";
}

Did not test it, but it should work.

Happy Coding!

  • Thank you @kuchenundkakao. This feels really close. I have updated my question slightly as I need the term to be counted as a result itself, rather than a heading. The results don't seem to include results from any other post types using this code either. I need it to include activities, hotels, pages and posts if possible. – Xav Jul 19 at 13:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.