Extend WordPress search to include custom post meta

After receiving numerous requests to allow the SubHeading value in my plugin to be searched when carrying out a default search, I turned my attention to finding a method to achieve this in WordPress 2.9.x.

The plugin stores a custom post meta entry for any post or page that requires a subtitle, in order to append this field to the search query I required the use of two actions.

  • posts_where_request
  • posts_join_request

Each of my plugins use a class based format, so the following examples will be provided in that format. This could quite easily be changed to regular functions

The first action is called when the class is instantiated:

class MyPlugin {
	function MyPlugin()
	{
		add_action('posts_where_request', array(&$this, 'search'));
	}
}

The first argument passed to this action is a string containing the where clause for the search query.

When the action is called we have also reached a point where we can check whether we are carrying out a blog search “is_search()“.

function search($where)
{
	if (is_search()) {
		global $wpdb, $wp;
		$where = preg_replace(
			"/($wpdb->posts.post_title (LIKE '%{$wp->query_vars['s']}%'))/i",
			"$0 OR ($wpdb->postmeta.meta_key = '_mymetakey' AND $wpdb->postmeta.meta_value $1)",
			$where
		);
		add_filter('posts_join_request', array(&$this, 'search_join'));
	}
	return $where;
}

All that we do here is include the meta key search by finding the post_title clause and inserting the meta key clause after it, using the preg_replace() function.

(Note: I have used the meta key “_mymetakey”, you will need to replace this with the key matching your custom meta data.)

The where condition is now in place, but we will need to add on the post_meta table, making use of the second action “posts_join_request” included in the function above.

function search_join($join)
{
	global $wpdb;
	return $join .= " LEFT JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id) ";
}

Putting all the basic functionality together you will have something along the lines of this:

class MyPlugin {
	function MyPlugin()
	{
		add_action('posts_where_request', array(&$this, 'search'));
	}
	function search($where)
	{
		if (is_search()) {
			global $wpdb, $wp;
			$where = preg_replace(
				"/(wp_posts.post_title (LIKE '%{$wp->query_vars['s']}%'))/i",
				"$0 OR ($wpdb->postmeta.meta_key = '_{$this->tag}' AND $wpdb->postmeta.meta_value $1)",
				$where
			);
			add_filter('posts_join_request', array(&$this, 'search_join'));
		}
		return $where;
	}
	function search_join($join)
	{
		global $wpdb;
		return $join .= " LEFT JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id) ";
	}
}

There are however a few possible pitfalls that I have yet to address, the main one being that the meta_value field is not indexed.

I’m not sure whether to try and automatically add an index to the field or add an additional option on my plugin settings page to create the index.

15 Replies to “Extend WordPress search to include custom post meta”

  1. Hey, that’s a great article, thanks. I used this in an entirely unrelated function, but found that when I searched, I would get the same result appearing multiple times.

    Eg, a search for ‘about’ would show the “About” page three times in the results.

    I got around this by adding a filter:

    add_filter(‘posts_distinct_request’, array(&$this, ‘search_distinct’));
    function search_distinct($distinct) {
    $distinct = “DISTINCT”;
    return $distinct;
    }

    Haven’t found any side-effects yet, might help someone else.

    Like

  2. Is there any way you could explain this in simpler terms?
    Is that last box of code suppose to be the everything you need? Are
    you writing this into your functions.php file? You call it a plugin
    but putting a php file with this code into your plugins folder will
    not show up in WordPress as a plugin. I saw the “mymetakey” line is
    that the only thing you would need to change for you specific
    custom post type? And would that just be the name of your custom
    post type?

    Like

  3. Excellent work, I modded and cut and paste into functions.php to work for wp-ecommerce sku added to site searching

    function search($where)
    {
    if (is_search()) {
    global $wpdb, $wp;
    $where = preg_replace(
    “/(wp_posts.post_title (LIKE ‘%{$wp->query_vars[‘s’]}%’))/i”,
    “$0 OR ($wpdb->postmeta.meta_key = ‘_wpsc_sku’ AND $wpdb->postmeta.meta_value $1)”,
    $where
    );
    add_filter(‘posts_distinct_request’, ‘search_distinct’);
    add_filter(‘posts_join_request’, ‘search_join’);
    }
    return $where;
    }

    function search_join($join)
    {
    global $wpdb;
    return $join .= ” LEFT JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id) “;
    }

    add_action(‘posts_where_request’, ‘search’);

    function search_distinct($distinct) {
    $distinct = “DISTINCT”;
    return $distinct;
    }

    works a treat 🙂 Thank you

    Like

    1. :* :* :* :* :* :*
      @Mindeater
      It worked after a bit of change
      $where = preg_replace(
      “/(wp_posts.post_title (LIKE ‘%{$wp->query_vars[‘s’]}%’))/i”,
      “$0 ) OR ($wpdb->postmeta.meta_key = ‘_wpsc_sku’ AND $wpdb->postmeta.meta_value $1”,
      $where
      );

      Like

  4. This is exactly what I was looking for but it looks like the code breaks when your search term includes more than 1 word. WordPress will break the search term into pieces so it will apply an OR to each individual term.

    So if you’re searching ‘Pepperoni Pizza’ WordPress will search post_title and post_content for both Pepperoni and Pizza. The regular expression you’re using breaks in this instance because it’s using the `$wp->query_vars[‘s’]` in the LIKE clause (which is never found because the entire term never appears.

    Like

  5. Thanks Steve for this helpful post.

    To make this hack also work for the admin search and for custom search I have removed the if (is_search()) test and replaced the “$wp->query_vars[‘s’]” in the regexp by “.*”

    This change also fix the multi-words problem raised by Brian in the comment above.

    Like

  6. Hi! I’m trying to use this code along with advanced custom fields (plugin) in which I’ve created lots of meta keys..
    this is how my code looks like:

    // Search include custom fields
    function MyPlugin()
    {
    add_action(‘posts_where_request’, array(&$this, ‘search’));
    }

    function search($where)
    {
    global $wpdb, $wp;
    $where = preg_replace(
    “/(wp_posts.post_title (LIKE ‘.*’))/i”,
    “$0 OR ($wpdb->postmeta.meta_key = ‘about_competition_0_page_text_about’ AND $wpdb->postmeta.meta_value $1)” ,
    $where
    );

    add_filter(‘posts_distinct_request’, ‘search_distinct’);
    add_filter(‘posts_join_request’, ‘search_join’);

    return $where;
    }

    function search_join($join)
    {
    global $wpdb;
    return $join .= ” LEFT JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id) “;
    }

    add_action(‘posts_where_request’, ‘search’);

    function search_distinct($distinct) {
    $distinct = “DISTINCT”;
    return $distinct;
    }

    As you can see I’ve edited the code according to the comments 🙂 However, I cant get this to work?! I cant even search for regular posts etc. it always turns up as “no mathing results”. If i remove the $1 I can get the regular posts etc. but still no metavalues.. Any idea why this doesnt work? I’m running latest wordpress (3.3) with Multisite installation.

    Like

    1. I was having trouble with the preg_replace. I changed it to this and it worked:

      $where = preg_replace(
      “/(wp_posts.post_title (LIKE ‘%{$wp->query_vars[‘s’]}%’))/i”,
      “$0 OR ($wpdb->postmeta.meta_key = ‘my_key’ AND $wpdb->postmeta.meta_value LIKE ‘%{$wp->query_vars[‘s’]}%’)”,
      $where
      );

      Like

  7. Hi there, I did the same with the meta_query functionality:

    function custom_search_query( $query ) {
    $custom_fields = array(
    // put all the meta fields you want to search for here
    "_post_title",
    "text_english",
    "text_deutsch"
    );
    $searchterm = $query->query_vars['s'];
    // we have to remove the "s" parameter from the query, because it will prevent the posts from being found
    $query->query_vars['s'] = "";
    if ($searchterm != "") {
    $meta_query = array('relation' => 'OR');
    foreach($custom_fields as $cf) {
    array_push($meta_query, array(
    'key' => $cf,
    'value' => $searchterm,
    'compare' => 'LIKE'
    ));
    }
    $query->set("meta_query", $meta_query);
    };
    }
    add_filter( "pre_get_posts", "custom_search_query");
    add_action( "save_post", "add_title_custom_field");

    function add_title_custom_field($postid){
    // since we removed the "s" from the search query, we want to create a custom field for every post_title. I don't use post_content, if you also want to index this, you will have to add this also as meta field.
    update_post_meta($postid, "_post_title", $_POST["post_title"]);
    }

    Like

  8. For folks finding this blog post in search of a way to add Advanced Custom Field meta data to the fields being queried I found the following works.
    It’s slightly different REGEX for preg_replace to others above – but it’s 2015 and WP v4.1 – not sure if that’s made a change…
    Anyways here’s the complete code:
    function acf_fields_search($where) {

    if ( is_search() ) {

    global $wpdb, $wp;
    // Dump value of $where to check preg_replace pattern
    // print_r( $where );
    $where = preg_replace(
    "/($wpdb->posts.post_title LIKE '%{$wp->query_vars['s']}%')/i",
    "$0 OR ($wpdb->postmeta.meta_value LIKE '%{$wp->query_vars['s']}%')",
    $where
    );
    add_filter( 'posts_distinct_request', 'search_distinct' );
    add_filter( 'posts_join_request', 'search_join' );
    }
    return $where;
    }

    function search_join( $join ) {

    global $wpdb;
    return $join .= " LEFT JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id) ";
    }

    add_action( 'posts_where_request', 'acf_fields_search' );

    function search_distinct( $distinct ) {

    $distinct = "DISTINCT";
    return $distinct;
    }

    Hope this helps someone.

    Like

Leave a comment