How to search all user meta from users.php in the admin

Hi @user2041:

Clearly as you know you need to modify the search that’s performed which you can do by modifying the values in the instance of the WP_User_Search class used for the search (you can find the source code at /wp-admin/includes/user.php if you’d like to study it.)

The WP_User_Search Object

Here’s what a print_r() of that object looks like with WordPress 3.0.3 when searching for the term TEST and without any other plugins that might affect it:

WP_User_Search Object
(
  [results] => 
  [search_term] => TEST
  [page] => 1
  [role] => 
  [raw_page] => 
  [users_per_page] => 50
  [first_user] => 0
  [last_user] => 
  [query_limit] =>  LIMIT 0, 50
  [query_orderby] =>  ORDER BY user_login
  [query_from] =>  FROM wp_users
  [query_where] =>  WHERE 1=1 AND (user_login LIKE '%TEST%' OR user_nicename LIKE '%TEST%' OR user_email LIKE '%TEST%' OR user_url LIKE '%TEST%' OR display_name LIKE '%TEST%')
  [total_users_for_query] => 0
  [too_many_total_users] => 
  [search_errors] => 
  [paging_text] => 
)

The pre_user_search Hook

To modify the values of the WP_User_Search object you’ll use the 'pre_user_search' hook which receives the current instance of the object; I called print_r() from within that hook to get access to its values which I displayed above.

The following example which you can copy to your theme’s functions.php file or you can use in a PHP file for a plugin you are writing adds the ability to search on the user’s description in addition to being able to search on the other fields. The function modifies the query_from and the query_where properties of the $user_search object which you need to be comfortable with SQL to understand.

Careful Modifying SQL in Hooks

The code in the yoursite_pre_user_search() function assumes that no other plugin has modified the query_where clause prior to it; if another plugin has modified the where clause such that replacing 'WHERE 1=1 AND (' with "WHERE 1=1 AND ({$description_where} OR" no longer works then this will break too. It’s much harder to write a robust addition that cannot be broken by another plugin when modifying SQL like this, but it is what it is.

Add Leading and Trailing Spaces when Inserting SQL in Hooks

Also note that when using SQL like this in WordPress it’s always a good idea to include leading and trailing spaces such a with " INNER JOIN {$wpdb->usermeta} ON " otherwise your SQL query might contain the following where there is no space before "INNER", which would of course fail: " FROM wp_postsINNER JOIN {$wpdb->usermeta} ON ".

Use "{$wpdb->table_name"} instead of Hardcoding Table Names

Next be sure to always using the $wpdb properties to reference table names in case the site has changed the table prefix from 'wp_' to something else. Thus it is better to refer to "{$wpdb->users}.ID" (with double quotes, not single ones) instead of hardcoding "wp_users.ID".

Limit the Query to Only When Search Terms Exist

Lastly be to only modify the query when there is a search term which you can test by inspecting search_term property of the WP_User_Search object.

The yoursite_pre_user_search() Function for 'pre_user_search'

add_action('pre_user_search','yoursite_pre_user_search');
function yoursite_pre_user_search($user_search) {
  global $wpdb;
  if (!is_null($user_search->search_term)) {
    $user_search->query_from .= " INNER JOIN {$wpdb->usermeta} ON " . 
      "{$wpdb->users}.ID={$wpdb->usermeta}.user_id AND " .
      "{$wpdb->usermeta}.meta_key='description' ";
    $description_where = $wpdb->prepare("{$wpdb->usermeta}.meta_value LIKE '%s'",
      "%{$user_search->search_term}%");
    $user_search->query_where = str_replace('WHERE 1=1 AND (',
      "WHERE 1=1 AND ({$description_where} OR ",$user_search->query_where);    
  }
}

Searching Each Meta Key-Value Pair Requires a SQL JOIN

Of course the likely reason WordPress doesn’t let you search on usermeta fields is that each one adds a SQL JOIN to the query and to a query with too many joins can be slow indeed. If you really need to search on many fields then I’d create a '_search_cache' field in usermeta that collects all the other information into one usermeta field to require only one join to search it all.

Leading Underscores in Meta Keys tell WordPress Not to Display

Note that leading underscore in '_search_cache' tells WordPress that this is an internal value and not something to ever display to the user.

Create a Search Cache with the 'profile_update' and 'user_register' Hooks

So you’ll need to hook both 'profile_update' and 'user_register' that are triggered on saving a user and registering a new user, respectively. You can grab all the meta keys and their values in those hooks (but omit those with values that are serialized or URL encoded arrays) and then concatenate them to store as one long meta value using the '_search_cache' key.

Store Meta as '|' Delimited Key-Value Pairs

I decided to grab all the key names and all their values and concatenate them into one big string with colons (“:”) separating the keys from the values and vertical bars (“|”) separating the key-value pairs like this (I’ve wrapped them across multiple lines so you can them without scrolled to the right):

nickname:mikeschinkel|first_name:mikeschinkel|description:This is my bio|
rich_editing:true|comment_shortcuts:false|admin_color:fresh|use_ssl:null|
wp_user_level:10|last_activity:2010-07-28 01:25:46|screen_layout_dashboard:2|
plugins_last_view:recent|screen_layout_post:2|screen_layout_page:2|
business_name:NewClarity LLC|business_description:WordPress Plugin Consulting|
phone:null|last_name:null|aim:null|yim:null|jabber:null|
people_lists_linkedin_url:null

Enables Specialized Searches on Meta Using key:value

Adding the key and values as we did allows you to do searches like “rich_editing:true” to find everyone who has rich editing, or search for “phone:null” to find those with no phone number.

But Beware of Search Artifacts

Of course using this technique creates possibly unwanted search artifacts such as search for “business” and everyone will be listed. If this a problem then you might not want to use such a elaborate cache.

The yoursite_profile_update() Function for 'profile_update' and 'user_register'

For function yoursite_profile_update(), like yoursite_pre_user_search() above can be copied to your theme’s functions.php file or you can use in a PHP file for a plugin you are writing:

add_action('profile_update','yoursite_profile_update');
add_action('user_register','yoursite_profile_update');
function yoursite_profile_update($user_id) {
  $metavalues = get_user_metavalues(array($user_id));
  $skip_keys = array(
    'wp_user-settings-time',
    'nav_menu_recently_edited',
    'wp_dashboard_quick_press_last_post_id',
  );
  foreach($metavalues[$user_id] as $index => $meta) {
    if (preg_match('#^a:[0-9]+:{.*}$#ms',$meta->meta_value))
      unset($metavalues[$index]); // Remove any serialized arrays
    else if (preg_match_all('#[^=]+=[^&]\&#',"{$meta->meta_value}&",$m)>0)
      unset($metavalues[$index]); // Remove any URL encoded arrays
    else if (in_array($meta->meta_key,$skip_keys))
      unset($metavalues[$index]); // Skip and uninteresting keys
    else if (empty($meta->meta_value)) // Allow searching for empty
      $metavalues[$index] = "{$meta->meta_key }:null";
    else if ($meta->meta_key!='_search_cache') // Allow searching for everything else
      $metavalues[$index] = "{$meta->meta_key }:{$meta->meta_value}";
  }
  $search_cache = implode('|',$metavalues);
  update_user_meta($user_id,'_search_cache',$search_cache);
}

Updated yoursite_pre_user_search() Function enabling a Single SQL JOIN for Searching All Interesting Meta Values

Of course for yoursite_profile_update() to have any effect you’ll need to modify yoursite_pre_user_search() to use the '_search_cache' meta key instead of the description, which we have here (with the same caveats as mentioned above):

add_action('pre_user_search','yoursite_pre_user_search');
function yoursite_pre_user_search($user_search) {
  global $wpdb;
  if (!is_null($user_search->search_term)) {
    $user_search->query_from .= " INNER JOIN {$wpdb->usermeta} ON " . 
      "{$wpdb->users}.ID={$wpdb->usermeta}.user_id AND " . 
      "{$wpdb->usermeta}.meta_key='_search_cache' ";
    $meta_where = $wpdb->prepare("{$wpdb->usermeta}.meta_value LIKE '%s'",
      "%{$user_search->search_term}%");
    $user_search->query_where = str_replace('WHERE 1=1 AND (',
      "WHERE 1=1 AND ({$meta_where} OR ",$user_search->query_where);
  }
}

Leave a Comment