Enable Custom Field Search In Wordpress

When you write a post in Wordpress you can set certain custom fields. The default search behaviour of Wordpress is to search only the title and main text of the posts, which makes these custom fields not all that useful. With a little bit of tinkering you can get Wordpress to search any custom fields that you have set, so if you store things like "Author" you can allow people to view all posts by that author by clicking on a link or doing a search. To see more information about Wordpress custom fields see this Wordpress codex site article. Wordpress stores these custom fields in a table called postmeta where each custom field name (called meta_key) is associated with a custom field value (called meta_value).

Wordpress works by only accepting certain known parameters like s for searching and p for viewing pages. In order to get Wordpress to accept new search parameters and then include these as part of a search you will have to edit the core Wordpress files. The two main files you need are classes.php and query.php in the wp-includes folder. There are no other files to edit so you only need to look at these ones. The more "tinkering" you do with the core Wordpress files, the more difficult it is to update the system when new versions come out so I will keep this editing to a minimum.

First off, find the following line in classes.php, it should be very near the top.

var $public_query_vars = array('m', 'p', 'posts', 'w', 'cat', 'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence', 'debug', 'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order', 'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 'name', 'category_name', 'tag', 'feed', 'author_name', 'static', 'pagename', 'page_id', 'error', 'comments_popup', 'attachment', 'attachment_id', 'subpost', 'subpost_id', 'preview', 'robots');

This is a list of the known variables that Wordpress will accept as either a GET or a POST request. In this example I will be calling the variable meta_key, to fit in with the name of the value in the table. So to pass the variable meta_key you need to add it to this list of parameters.

var $public_query_vars = array('m', 'p', 'posts', 'w', 'cat', 'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence', 'debug', 'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order', 'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 'name', 'category_name', 'tag', 'feed', 'author_name', 'static', 'pagename', 'page_id', 'error', 'comments_popup', 'attachment', 'attachment_id', 'subpost', 'subpost_id', 'preview', 'robots', 'meta_key');

That's it for classes.php, you can now close it and open up query.php. We still need to get Wordpress to accept our custom variable as part of a search so find the function called fill_query_vars(), around line 380. This function starts with the definition of an array, add our variable name to the end of this array in the same way as we did with in the classes.php file.

function fill_query_vars($array){
  $keys = array(
    'error'
    , 'm'
    , 'p'
    // code removed for brevity...
    , 'preview'
    , 'meta_key'
  );

Next, you will have to change the search function to allow you to construct the new search string. Find the line that starts with the following code, on around line 900.

// If a search pattern is specified, load the posts that match
if ( !empty($q['s']) ) {

Comment out this if statement and paste the following code underneath it.

// If a search pattern is specified, load the posts that match
if ( !empty($q['s']) ) {
  // added slashes screw with quote grouping when done early, so done later
  $q['s'] = stripslashes($q['s']);
  if ($q['sentence']) {
    $q['search_terms'] = array($q['s']);
  }
  else {
    preg_match_all('/".*?("|$)|((?&lt=[\\s",+])|^)[^\\s",+]+/', $q[s], $matches);
    $q['search_terms'] = array_map(create_function('$a', 'return trim($a, "\\"\'\\n\\r ");'), $matches[0]);
  }
  $n = ($q['exact']) ? '' : '%';
  $searchand = '';
 
  if($q['meta_key']){		
    $q['meta_key'] = stripslashes($q['meta_key']);
    $join = " INNER JOIN ".$wpdb->postmeta." ON ".$wpdb->posts.".ID = ".$wpdb->postmeta.".post_id " ;
    $distinct = 'DISTINCT';
    foreach((array)$q['search_terms'] as $term) {
      $term = addslashes_gpc($term);
      $search .= "{$searchand}((post_title LIKE '{$n}{$term}{$n}') OR (post_content LIKE '{$n}{$term}{$n}') OR (meta_value LIKE '{$n}{$term}{$n}'))";
      $searchand = ' AND ';
    }
      $term = addslashes_gpc($q['s']);
      if (!$q['sentence'] && count($q['search_terms']) > 1 && $q['search_terms'][0] != $q['s'] ){
        $search .= " OR (post_title LIKE '{$n}{$term}{$n}') OR (post_content LIKE '{$n}{$term}{$n}') OR (meta_value LIKE '{$n}{$term}{$n}')";
      }
      $search .= " AND meta_key = '".$q['meta_key']."'";
    }else{
      foreach((array)$q['search_terms'] as $term) {
        $term = addslashes_gpc($term);
        $search .= "{$searchand}((post_title LIKE '{$n}{$term}{$n}') OR (post_content LIKE '{$n}{$term}{$n}'))";
        $searchand = ' AND ';
      }
      $term = addslashes_gpc($q['s']);
      if (!$q['sentence'] && count($q['search_terms']) > 1 && $q['search_terms'][0] != $q['s'] )
      $search .= " OR (post_title LIKE '{$n}{$term}{$n}') OR (post_content LIKE '{$n}{$term}{$n}')";
    }
    if ( !empty($search) )
      $search = " AND ({$search}) ";
}

This does a full text search on the post title, content and custom fields, which can lead to some odd results. To make Wordpress search for only the post title and custom fields you can take out the post_content section of the SQl string like this.

foreach((array)$q['search_terms'] as $term) {
  $term = addslashes_gpc($term);
  $search .= "{$searchand}((post_title LIKE '{$n}{$term}{$n}') OR (meta_value LIKE '{$n}{$term}{$n}'))";
  $searchand = ' AND ';
}
$term = addslashes_gpc($q['s']);
if (!$q['sentence'] && count($q['search_terms']) > 1 && $q['search_terms'][0] != $q['s'] ){
  $search .= " OR (post_title LIKE '{$n}{$term}{$n}') OR (meta_value LIKE '{$n}{$term}{$n}')";
}

You can now close the query.php file as we will not need it from here. In order to allow you to actually use what we have just done you need to alter the template files. If your template doesn't have a functions.php file then create one. This file contains functions that will be used with the template. Add the following code to this file.

function get_meta_search_query() {
  return apply_filters( 'get_meta_search_query', stripslashes( get_query_var( 'meta_key' ) ) );
}
 
function the_meta_search_query() {
  echo attribute_escape( apply_filters( 'the_meta_search_query', get_meta_search_query() ) );
}
 
function generateSelect($name,$options,$default=''){
  $html = '<select name="'.$name.'">';
  foreach($options as $value=>$label){
    $html .= '<option ';
    if($value==$default){
      $html .= 'selected="selected" ';
    };
    $html .= 'value="'.$value.'">'.$label.'</option>';
  };
  $html .= '</select>';
  return $html;
}

Next, edit the file that contains your search form. In the default template with Wordpress 2.3 this is contained in a file called searchForm.php. It has the following footprint.

<form method="get" id="searchform" action="http://www.example.com/wordpress/">
<div><input type="text" value="" name="s" id="s" />
<div>
<input type="submit" id="searchsubmit" value="Search" />
</div>
</form>

The custom field search will be done with a drop down box. Insert the following code in the form.

<div>
<?php 
$valuleArr = array('','author'=>'Author','mood'=>'Mood');
echo generateSelect('meta_key',$valuleArr,get_meta_search_query());
?>
</div>

You can now search by custom fields!

To allow the user to see these custom fields you can do one of two things, both of which must be done within the post loop. The first one is to just use the Wordpress function the_meta(). This will print a list of the meta tags out.

the_meta();

The second is to allow users to click on those links and search by those parameters. So if a user is viewing a post they can click on the mood (to use a silly example) and see all posts that also contain that mood.

Mood:<?php 
  $moods = get_post_meta($post->ID, "mood", true); 
  if($moods != ""){
    $moods = explode(",", $moods);
    foreach($moods as $mood){
      echo "<a href=\"http://www.example.com/wordpress/index.php?s=" . trim($mood) . "&amp;meta_key=mood\">" . trim($mood) . "</a> ";
    };
  };
?>

Of course this might be easier to do with some sort of plugin. Using the Wordpress hook init might be an option.

UPDATE

This only works for versions of Wordpress below 2.5 as the structure of the file has changed since then. You shouldn't really do any core hacks so this solution is only for illustration purposes. If you want to know to do it without hacking core then have a look at Enable Custom Field Searching With Wordpress 2.6.

Comments

Wow!!! Good job. Could I take some of yours triks to build my own site?.
Permalink
Oh course you can, that is what they are here for!
Name
Philip Norton
Permalink
Your site is very interesting and useful.
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
6 + 12 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.