Websites, Design & Online Marketing

Searching custom posts and metadata with WordPress

Most of the time WordPress makes it pretty easy to do things.  And if you get stuck a two minute search with Google usually turns up a simple, elegant solution. Most of the time…but I found an exception to that: custom searches, in this case searching custom posts and meta data.

I was recently using custom post types and custom fields to add structure to a website’s content. My application required a search function to match custom posts only, and to match not only the post content but also content contained in custom fields – ‘metadata’.

After more than two minutes on Google I couldn’t find a simple answer. I didn’t want to use a plugin, so I created a solution of my own. In my functions.php file I hooked into three WordPress filters:

add_filter('posts_join', 'websmart_search_join' );
add_filter('posts_groupby', 'websmart_search_groupby' );
add_filter('posts_where', 'websmart_search_where' );

I used the posts_join filter to extend the search query to the wp_postmeta table, where custom field data is stored.

function websmart_search_join( $join ) {
        global $wpdb;
        if( is_search() && !is_admin()) {
                $join .= "LEFT JOIN $wpdb->postmeta AS m ON ($wpdb->posts.ID = m.post_id) ";
        }
        return $join;
}

Having joined to the wp_postmeta table (giving it the alias m) I needed to be careful to group the results by post id, to avoid returning duplicates.

function websmart_search_groupby( $groupby ) {
        global $wpdb;
        if( is_search() && !is_admin()) {
                $groupby = "$wpdb->posts.ID";
        }
        return $groupby;
}

With the query join and group by set up I used the posts_where filter to create a where clause to do the meta data matching.

function websmart_search_where( $where ) {
        global $wpdb, $wp_query;
        if( is_search() && !is_admin()) {
                $where = "";
                $search_terms = se_get_search_terms();
                $n = !empty($wp_query->query_vars['exact']) ? '' : '%';
                $searchand = '';
                if (count($search_terms) < 1) {
                        // no search term provided: so return no results
                        $search = "1=0";
                } else {
                        foreach( $search_terms as $term ) {
                                $term = esc_sql( like_escape( $term ) );
                                $search .= "{$searchand}(($wpdb->posts.post_title LIKE '{$n}{$term}{$n}') OR ($wpdb->posts.post_content LIKE '{$n}{$term}{$n}') OR (m.meta_value LIKE '{$n}{$term}{$n}'))";
                                $searchand = ' AND ';
                        }
                }
                $where .= " AND ${search} ";
                $where .= " AND (m.meta_key IN ('custom_field1', 'custom_field2')) ";
                $where .= " AND ($wpdb->posts.post_password = '') ";
                $where .= " AND ($wpdb->posts.post_type IN (/* 'post', 'page', */ 'custom_post')) ";
                $where .= " AND ($wpdb->posts.post_status = 'publish') ";
        }
        return $where;
}

The meat and potatoes is the second line inside the foreach loop.  It creates a search string which matches against post_title, post_content and m.meta_value.  The m.meta_value term  matches against all custom fields.  I didn’t want that so I added the m.meta_key clause to restrict custom field search to two fields, custom_field1 and custom_field2.  The wp_post.post_type clause also restricts to searching posts of type custom_post, the name of the custom post type I created (elsewhere in my functions.php file) using register_post_type().  I could have added ‘post’, ‘page’ or some other custom post type into this part of the query.

I got a lot of help in understanding how these filters work by looking at the Search Everywhere plugin.  It’s a very nice plugin!  They have a utility function to build search terms from WordPress query parameters, which I borrowed.

// Code from Search Everywhere plugin
function se_get_search_terms()
{
        global $wpdb, $wp_query;
        $s = isset($wp_query->query_vars['s']) ? $wp_query->query_vars['s'] : '';
        $sentence = isset($wp_query->query_vars['sentence']) ? $wp_query->query_vars['sentence'] : false;
        $search_terms = array();

        if ( !empty($s) )
        {
                // added slashes screw with quote grouping when done early, so done later
                $s = stripslashes($s);
                if ($sentence)
                {
                        $search_terms = array($s);
                } else {
                        preg_match_all('/".*?("|$)|((?<=[\\s",+])|^)[^\\s",+]+/', $s, $matches);
                        $search_terms = array_map(create_function('$a', 'return trim($a, "\\"\'\\n\\r ");'), $matches[0]);
                }
        }
        return $search_terms;
}

Thanks!

By the way, if you are having trouble working out what WordPress is doing with all your filter results, add this debugging statement somewhere near the top of your search results template file, e.g. search.php:

<?php global $wp_query; print_r($wp_query->request); ?>

 

5 thoughts on “Searching custom posts and metadata with WordPress

  1. Thanks for this Mick.
    It’s interesting approach but I believe you can search custom posts using arguments in meta_query and tax_query.

  2. Hi Mick, Thanks for the code. I’ve been searching the interwebs for hours looking for a good solution to this and your solution works well for what I need. Question though – How can we match whole keywords words rather than partial keywords and have the results show alphabetically? Is there an easy tweak to your code to make this happen? Thanks again for the code and help on this.

  3. Thanks so much Mick for this solution.. I was heading down this track myself when i came across your post!

    Feels like a shortfall in WordPress that is so difficult to add searching to meta_values!

Leave a Reply

Your email address will not be published. Required fields are marked *