include user profiles in search results?

I have a need to include user profiles, along with posts & pages, in front-end searches.

I’ve searched for solutions to this problem (both here on WSE and elsewhere on the web) and haven’t found must help. The closest I found, as far as my requirement, are Include Author profile in search results and Extend WordPress search to include user search, neither of which have reasonable answers.

To be more clear about the requirement: the site has posts, pages and (of course) users. When an end-user performs a search on the front-end, I need the search results to contain posts and pages whose titles or post_content contains the search terms (as is normal for front-end searches) plus the profiles of users that match the search terms (see below about what is meant by a user matching the search terms).

The important point is that I’m not talking about searching for posts that have been authored by a given user!

Rationale behind requirement

To help understand why I have this requirement, it might help to know that a part of the site lists all user profiles and an end-user can click on the name of any user to see that user’s profile. On the user profile, various usermeta‘s are displayed. From the end-user’s perspective, there is no difference between the user profiles and posts/pages they can reach on the front-end.

Suppose the user profile for a given user displays the string “foo” (i.e., one of the displayed usermeta‘s for that user contains “foo”). When an end-user searches for “foo” then they would expect that user’s profile to show up in the search results.

Provisional solution

The following is the solution that I’ve come up with. This solution works but I feel there’s got to be a better/easier/less-brittle way to accomplish this:

  1. register a non-public custom post type (say, ‘my_user_profile’)
  2. hook into user_register. insert a post of type ‘my_user_profile’ and add a postmeta (say ‘my_user_id’) whose value is the ID of the newly registered user
  3. hook into the_posts. When is_search() is true, then do a get_users() that searches various usermeta for the search terms in the end-user’s search; then do a get_posts() for posts of type ‘my_user_profile’ with postmeta ‘my_user_id’ “IN” the user IDs found in the get_users(); and then return the merge of the posts found by the original search with the posts of type ‘my_user_profile’ found by my get_posts().
  4. hook into post_type_link (which is called by get_permalink()). When the $post->post_type is ‘my_user_profile’, return get_author_posts_url() on the user whose ID is in the ‘my_user_id’ postmeta of $post. This is so that the theme’s search.php doesn’t have to contain code with specific knowledge of how the above steps have “augmented” the search results
  5. hook into get_the_excerpt. When $post->post_type is ‘my_user_profile’, then return the value of a specific usermeta (say ‘my_excerpt’) for the user whose ID is in the ‘my_user_id’ postmeta of $post. This is so that the theme’s search.php doesn’t have to contain code with specific knowledge of how the above steps have “augmented” the search results

Code for provisional solution

[note: code for step #3 edited to correct a bug I introduced when transcribing (and sanitizing) my working code]

Here’s the code for my provisional solution:

// step #1
add_action( 'init', 'wpse_register_post_type' );
function wpse_register_post_type() {
    $args = array(
        'public' => false,
        'show_ui' => false,
        'supports' => array(
            'title',
            'author',
        ),
    );
    register_post_type( 'my_user_profile', $args );
}

// step #2
add_action( 'user_register', 'wpse_add_user_profile_post' );
function wpse_add_user_profile_post( $my_user_id ) {
    $user = get_user_by( 'ID', $my_user_id );
    $args = array(
        'post_type' => 'my_user_profile',
        // so that I can find them easier when manually looking thru the wp_posts table
        'post_title' => $user->display_name,
        'post_status' => 'publish',
    );
    $post_id = wp_insert_post( $args );

    if ( ! is_wp_error( $post_id ) ) {
        update_post_meta( $post_id, 'my_user_id', $my_user_id );
    }

    return;
}

// step #3
add_filter( 'the_posts', array( $this, 'wpse_user_search' ), 10, 2 );
function wpse_user_search( $posts, $query ) {
    if ( ! is_search() ) {
        return $posts;
    }

    $search_terms = explode( ' ', $query->get( 's' ) );
    $user_meta_keys = array( /* my usermeta keys */ );

    $args = array(
        'fields' => 'ID',
        'meta_query' => array( 'relation' => 'OR' ),
    );
    // build the meta_query
    foreach ( $user_meta_keys as $meta_key ) {
        foreach ( $search_terms as $search_term ) {
            $args['meta_query'][] = array(
                'key' => $meta_key,
                'value' => $search_term,
                'compare' => 'LIKE',
            );
        }
    }
    $users = get_users( $args );

    // get the my_user_profile posts associated with $users
    $args = array(
        'post_type' => 'my_user_profile',
        'meta_query' => array(
            array(
                'key' => 'my_user_id',
                'value' => $users,
                'compare' => 'IN',
            ),
        )
    );

    // make sure we don't call ourselves in the get_posts() below
    remove_filter( 'the_posts', array( $this, 'user_search' ) );

    $user_posts = get_posts( $args );

    add_filter( 'the_posts', array( $this, 'user_search' ), 10, 2 );

    $posts = array_merge( $posts, $user_posts );

    return $posts;
}

// step 4
add_filter( 'post_type_link', array( $this, 'wpse_user_profile_permalink' ), 10, 2 );
function wpse_user_profile_permalink( $post_link, $post ) {
    if ( 'my_user_profile' !== $post->post_type ) {
        return $post_link;
    }

    // rely on WP_Post::__get() magic method to get the postmeta
    return get_author_posts_url( $post->my_user_id );
}

// step 5
add_filter( 'get_the_excerpt', array( $this, 'wpse_user_profile_excerpt' ), 10, 2 );
function wpse_user_profile_excerpt( $excerpt, $post ) {
    if ( 'my_user_profile' !== $post->post_type ) {
        return $excerpt;
    }

    // rely on WP_Post::__get() magic method to get the postmeta
    $user = get_user_by( 'ID', $post->my_user_id );

    // rely on WP_User::__get() magic method to get the usermeta
    return $user->my_excerpt;
}

Like I said, the above works but I can’t help but think that there’s an easier way that I just haven’t thought of.

Alternate (rejected) solution

One alternative I thought of, but rejected because it seems more complex/brittle than the above solution, is:

  1. same as #1 above
  2. same as #2 above
  3. hook into personal_options_update. For each usermeta that I’m already storing for a user, add them as postmeta to the post of type ‘my_user_profile’ associated with the given user
  4. hook into posts_join and posts_where to search the various postmeta added in step #3
  5. same as #4 above
  6. same as #5 above

Does anyone have a simpler/less-brittle solution?

1 Answer
1

I haven’t an ready to use solution. However I think you should enhance the query, so that you have the field of the users inside this. I think the follow example demonstrate it more.

The two filter hooks are necessary and get an result for the query like this:

SELECT SQL_CALC_FOUND_ROWS wpbeta_posts.ID
FROM wpbeta_posts JOIN wpbeta_users
WHERE 1=1 
AND (((wpbeta_posts.post_title LIKE '%search_string%')
OR (wpbeta_posts.post_excerpt LIKE '%search_string%')
OR (wpbeta_posts.post_content LIKE '%search_string%'))) 
AND wpbeta_posts.post_type IN ('post', 'page', 'attachment')
AND (wpbeta_posts.post_status="publish"
OR wpbeta_posts.post_status="private")
OR (wpbeta_users.display_name LIKE '%search_string%') 
ORDER BY wpbeta_posts.post_title LIKE '%search_string%' DESC, wpbeta_posts.post_date DESC
LIMIT 0, 10

You can also read this query on your tests really easy via plugin, like Debug Objects or Query Monitor. The sql query is only an example and sure not the result to use it. You must play with them and include the hooks below to get the right result. The source get als only an example to add one field from the users table, the display_name. Maybe a sql “nerd” can help.

// Enhance the JOIN clause of the WP Query with table users.
add_filter( 'posts_join', function( $join, $wp_query ) {

    // No search string, exit.
    if ( empty( $wp_query->query_vars['s'] ) ) {
        return $join;
    }

    global $wpdb;
    $join .= ' JOIN ' . $wpdb->users;

    return $join;
}, 10, 2 );

// Enhance WHERE clause of the WP Query with user display name. 
add_filter( 'posts_where', function( $where, $wp_query ) {

    // No search, exit.
    if ( ! is_search() ) {
        return $where ;
    }

    global $wpdb;
    $where .= ' OR (' . $wpdb->users . '.display_name LIKE \'%' . esc_sql( $wp_query->query_vars['s'] ) . '%\')';

    return $where ;
}, 10, 2 );

Leave a Comment