WordPress query search 's' exact word

1k views Asked by At

I want to limit the search by keywords to only search for exact word not a phrase or mixed content, so if I search for cheese it needs to be limited only for word cheese, and right now it will also return content with the word cheeseburger.

The part of the search query in my modified template query is right here:

if ( ! function_exists( 'get_job_listings' ) ) :
    if ( 'top_rating' === $args['orderby'] ) {
        $query_args['meta_query'] = [
            'relation' => 'OR',
            [
                'key'     => 'rating',
                'compare' => 'EXISTS',
            ],
            [
                'key'     => 'rating',
                'compare' => 'NOT EXISTS',
            ],
        ];
        $query_args['orderby'] = [
            'meta_value_num' => 'DESC',
            'comment_count' => 'DESC',
        ];
    }

    $job_manager_keyword = sanitize_text_field( $args['search_keywords'] );
    if ( ! empty( $job_manager_keyword ) && strlen( $job_manager_keyword ) >= apply_filters( 'job_manager_get_listings_keyword_length_threshold', 2 ) ) {
        $search_query = $job_manager_keyword;
    $query_args['s'] = $search_query;
        // $query_args['s'] = '"' . $search_query . '"';
        // $query_args['s'] = '/\b' . preg_quote($search_query, '/') . '\b/';
        $query_args['sentence'] = true;
        add_filter( 'posts_search', 'get_job_listings_keyword_search' );
    }

    $query_args = apply_filters( 'job_manager_get_listings', $query_args, $args );

    if ( empty( $query_args['meta_query'] ) ) {
        unset( $query_args['meta_query'] );
    }

    if ( empty( $query_args['tax_query'] ) ) {
        unset( $query_args['tax_query'] );
    }

    /** This filter is documented in wp-job-manager.php */
    $query_args['lang'] = apply_filters( 'wpjm_lang', null );

    // Filter args.
    $query_args = apply_filters( 'get_job_listings_query_args', $query_args, $args );
    do_action( 'before_get_job_listings', $query_args, $args );

    // Cache results.
    if ( apply_filters( 'get_job_listings_cache_results', true ) ) {
        $to_hash              = wp_json_encode( $query_args );
        $query_args_hash      = 'jm_' . md5( $to_hash . JOB_MANAGER_VERSION ) . WP_Job_Manager_Cache_Helper::get_transient_version( 'get_job_listings' );
        $result               = false;
        $cached_query_results = true;
        $cached_query_posts   = get_transient( $query_args_hash );

        if ( is_string( $cached_query_posts ) ) {
            $cached_query_posts = json_decode( $cached_query_posts, false );

            if (
                $cached_query_posts
                && is_object( $cached_query_posts )
                && isset( $cached_query_posts->max_num_pages )
                && isset( $cached_query_posts->found_posts )
                && isset( $cached_query_posts->posts )
                && is_array( $cached_query_posts->posts )
            ) {
                if ( in_array( $query_args['fields'], [ 'ids', 'id=>parent' ], true ) ) {
                    // For these special requests, just return the array of results as set.
                    $posts = $cached_query_posts->posts;
                } else {
                    $posts = array_map( 'get_post', $cached_query_posts->posts );
                }

                $result = new WP_Query();
                $result->parse_query( $query_args );
                $result->posts         = $posts;
                $result->found_posts   = intval( $cached_query_posts->found_posts );
                $result->max_num_pages = intval( $cached_query_posts->max_num_pages );
                $result->post_count    = count( $posts );
            }
        }

        if ( false === $result ) {
            $result               = new WP_Query( $query_args );
            $cached_query_results = false;
            $cacheable_result                  = [];
            $cacheable_result['posts']         = array_values( $result->posts );
            $cacheable_result['found_posts']   = $result->found_posts;
            $cacheable_result['max_num_pages'] = $result->max_num_pages;
            set_transient( $query_args_hash, wp_json_encode( $cacheable_result ), DAY_IN_SECONDS );
        }

        if ( $cached_query_results ) {
            // random order is cached so shuffle them.
            if ( 'rand_featured' === $args['orderby'] ) {
                usort( $result->posts, '_wpjm_shuffle_featured_post_results_helper' );
            } elseif ( 'rand' === $args['orderby'] ) {
                shuffle( $result->posts );
            }
        }
    } else {
        $result = new WP_Query( $query_args );
    }
    do_action( 'after_get_job_listings', $query_args, $args );

    //remove_filter( 'posts_search', 'get_job_listings_keyword_search' );
    return $result;
}

endif;

I can see that $query_args['s'] is a standard query for search keywords, but some standard query modifications I've tried are like these examples:

And there are other examples too, but none of them are working for me.

How can I modify it, so it can search only for the exact word in content?

I see this question has been posted multiple times, but all of the examples I've tried are not working.

2

There are 2 answers

1
silver On

Familiarize yourself with SQL syntax and its much easier to cutomize any query perform by wordpress

You can modify ANY query directly using posts_clauses filter including the search query

by default, wordpress perform the search on Post Title, Post Content and Post Excerot using LIKE and % wrap around the search term, a similar syntax below

WHERE 1=1 
AND (
    post_table.post_title LIKE '%hello%' OR 
    post_table.post_excerpt LIKE '%hello%' OR
    post_table.content LIKE '%hello%'
)

It even break each word into a separate LIKE statement,

But since you want to do an exact match, you can add a space between the search term, then another match if its the first or last, something like below search a string in post_content column

WHERE 1=1 
AND (
    post_table.post_content LIKE 'hello %' OR  //hello is first word, match on -> hello world
    post_table.post_content LIKE '% hello %' OR //hello is in between words, match on -> world hello world
    post_table.post_content LIKE '% hello' //hello is the last word, match on -> world hello
)

Or if you're on > MYSQL 8 that support regex, you can perform string search with boundaries, some like ;

WHERE 1=1 
AND (
    post_table.post_content RLIKE "[[:<:]]hello[[:>:]]"
) 

Now to modify the seach query is really simply with code reference below

add_filter( 'posts_clauses', function ($clauses, $query) {
    
    // prevent query modification if its not search page or an admin page or not main query
    if ( !$query->is_search || is_admin() || !$query->is_main_query()) 
            return $clauses;
    
    // get the search term
    $searchTerm = sanitize_text_field( $query->query['s'] );
    
    //build the search query, and perfom the search on post_title and post_content column
    //refer to two functions below
    $searchQuery = _custom_search_query($searchTerm, ['post_title', 'post_content']);
    
    //override wordpress default search query by defining the where key on its clauses
    $clauses['where'] = "AND ( $searchQuery )";
    
    // additional filter on what you want to search
    $clauses['where'] .= " AND wp_posts.post_type = 'post' AND wp_posts.post_status = 'publish'";
    
    //add the default order
    $clauses['orderby'] = "wp_posts.post_title DESC, wp_posts.post_date DESC";
    
    return $clauses;
}, 10, 2 );

to avoid manually typing all the LIKES and OR's, we can use these two functions below based on how you want the query,

For conditionally building LIKE and OR statement on different fields specified to search

function _custom_search_query( $term, $fields = [] ) {
    
    $query = '';
    $compares = ["$term %", "% $term", "% $term %"];
    foreach ($fields as $key => $field) {
        foreach ($compares as $k => $comp) {
            $query .= ($key > 0 || $k > 0 ? ' OR' : '' ). " wp_posts.$field LIKE '$comp'";
        }
    }
    return $query;
}

For conditionally building RLIKE and OR statement on different fields specified to search

function _regex_custom_search_query(  $term, $fields = [] ) {
    $query = '';
    foreach ($fields as $key => $field) {
        $query .= ($key > 0 ? ' OR' : '' ). ' wp_posts.'.$field.' RLIKE "[[:<:]]'.$term.'[[:>:]]"' ;
    }
    return $query;
}

Also, print the $wp_query->request on the front-end search page, you'll see the SQL perform on the current page, this will help you debug any issue e.i.

add_action('wp_head', function() {
    global $wp_query;

    if ( !$wp_query->is_search )
        return;
    
    echo '<pre>', print_r($wp_query->request, 1), '</pre>';
    
});

If you also want to create your own search page, you can simply apply the same query using $wpdb

0
Jenny On

To achieve this you need to modify the posts_search hook to a regular expression. So That will match the whole words. And you need to do a custom SQL query for this.

function keyword_search( $search, $wp_query ) {
    global $wpdb;

    if ( empty( $search ) )
        return $search;

    $q = $wp_query->query_vars;
    $n = ! empty( $q['exact'] ) ? '' : '%';
    $search = $searchand = '';

    foreach ( (array) $q['search_terms'] as $term ) {
        $term = $n . $wpdb->esc_like( $term ) . $n;
        $search .= $wpdb->prepare( "{$searchand}($wpdb->posts.post_title REGEXP '[[:<:]]{$term}[[:>:]]'", $term );
        $searchand = ' AND ';
    }
    return $search;
}

and then replace your get_job_listing function condition

if ( ! empty( $job_manager_keyword ) && strlen( $job_manager_keyword ) >= apply_filters( 'job_manager_get_listings_keyword_length_threshold', 2 ) ) {
    $search_query = $job_manager_keyword;
    $query_args['s'] = $search_query;
    add_filter( 'posts_search', 'keyword_search', 500, 2 );
}

and remove your search filter after the search. using this

do_action( 'after_get_job_listings', $query_args, $args );
remove_filter( 'posts_search', 'keyword_search', 500 );

Tested and worked for me. I hope this will work for you as well.