WordPress Custom Field Search Plugin

Some friends of mine that have started offering custom WordPress designed sites and consulting hit me up a couple of weeks ago for some help. They had a site for a client that was utilizing custom fields to provide some additional CMS-like functionality to WordPress and had hit a snag. Their custom fields were not being searched.

Why custom fields in the first place? Using the custom fields allowed them to add deck, byline, bio, kicker text that their custom template was checking for and formatting appropriately. It really makes life easier when you are dealing with clients that come from the standard publishing background. Not to mention how much easier it is to apply a global formatting change to those elements since you don't have to manually edit each post body.

Here's where I came in. The client noticed that when they entered in a post authors' name(custom field, byline) no search results were being returned. So they called me up for help.

I did some quick digging online and found a few blog posts that had people writing custom queries to fetch the search information. Further digging revealed the WordPress filters that are responsible for building the SQL query for the search: posts_join, posts_groupby, posts_where, and posts_request.

For those wanting to play around, the posts_request filter is likely the first place you will want to look. I tossed the following code into our plugin and was able to echo out the entire sql request that was being made for each page:

function custom_search_request($request) {
    echo($request);
    return($request);
}
add_filter('posts_request', 'custom_search_request');

Once that is done, a few page views will let you know what WordPress is doing internally to pull up the posts for each page. You can then update the above to work with the other filters to show you how everything is assembled.

From there, it was a matter of looking at the existing code in the get_posts funciton in wp-includes/query.php and figuring out what additional query information needs to be passed in the filters. Since that function is the densest code I've seen yet in WordPress, I won't go into all of the details. I'll leave that to folks who have good eyes and brave hearts.

When putting it all together, there are several things to note. Always check to make sure that you are currently in a search page and return the original query snippet if you are not. The example I have below adds the custom fields: bio, byline, kicker, and deck.

function custom_search_join($join) {
    if ( is_search() && isset($_GET['s'])) {
        global $wpdb;

       $join = " LEFT JOIN $wpdb->postmeta ON $wpdb->posts.ID = $wpdb->postmeta.post_id ";
    }
    return($join);
}
add_filter('posts_join', 'custom_search_join');

function custom_search_groupby($groupby) {
    if ( is_search() && isset($_GET['s'])) {
        global $wpdb;
        $groupby = " $wpdb->posts.ID ";
    }
    return($groupby);
}
add_filter('posts_groupby', 'custom_search_groupby');

function custom_search_where($where) {
    $old_where = $where;
    if (is_search() && isset($_GET['s'])) {
        global $wpdb;
        $customs = Array('bio', 'byline', 'kicker', 'deck');
        $query = '';
        $var_q = stripslashes($_GET['s']);
        if ($_GET['sentence']) {
            $search_terms = array($var_q);
        }
        else {
            preg_match_all('/".*?("|$)|((?<=[\\s",+])|^)[^\\s",+]+/', $var_q, $matches);
            $search_terms = array_map(create_function('$a', 'return trim($a, "\\"\'\\n\\r ");'), $matches[0]);
        }
        $n = ($_GET['exact']) ? '' : '%';
        $searchand = '';
        foreach((array)$search_terms as $term) {
            $term = addslashes_gpc($term);
            $query .= "{$searchand}(";
                        $query .= "($wpdb->posts.post_title LIKE '{$n}{$term}{$n}')";
            $query .= " OR ($wpdb->posts.post_content LIKE '{$n}{$term}{$n}')";
            foreach($customs as $custom) {
                $query .= " OR (";
                $query .= "($wpdb->postmeta.meta_key = '$custom')";
                $query .= " AND ($wpdb->postmeta.meta_value  LIKE '{$n}{$term}{$n}')";
                $query .= ")";
            }
            $query .= ")";
            $searchand = ' AND ';
        }
        $term = $wpdb->escape($var_q);
        if (!$_GET['sentense'] && Count($search_terms) > 1 && $search_terms[0] != $var_q) {
            $search .= " OR ($wpdb->posts.post_title LIKE '{$n}{$term}{$n}')";
            $search .= " OR ($wpdb->posts.post_content LIKE '{$n}{$term}{$n}')";
        }

        if (!empty($query)) {
            $where = " AND ({$query}) AND ($wpdb->posts.post_status = 'publish') ";
        }
    }

    return($where);
}
add_filter('posts_where', 'custom_search_where');

With all of that done, I added the above code into their plugin and the new fields were being used in the search. It should be noted that the code is adding significant overhead to the post searching. In the above example, each keyword in a search generates 5 SQL LIKE statements instead of 1. In addition, these LIKEs are being performed across joined tables. Because this was for a small website, I do not know the full performance trade-off of the changes. For larger sites, I'll almost always advocate using an external search engine such as Google or Sphinx.

Attachments:
January 27th, 2009