From 15ff6c8533b1c250dabb3c4659d5ebe56526890b Mon Sep 17 00:00:00 2001 From: gikaragia Date: Tue, 30 Apr 2024 10:20:02 +0300 Subject: [PATCH] Expand job listing search This expands job listing meta and term search to have similar functionality to WP core's search. As the core's search includes clauses that are aggregated with ORs and ANDs, the only way to include meta fields in this search was to rebuild the search query from scratch. With this change, get_job_listings_keyword_search generates the clauses that are generated in WP_Query::parse_search and adds additional clauses that search the post meta and terms. --- includes/class-wp-job-manager-post-types.php | 4 +- wp-job-manager-functions.php | 197 ++++++++++++++----- 2 files changed, 154 insertions(+), 47 deletions(-) diff --git a/includes/class-wp-job-manager-post-types.php b/includes/class-wp-job-manager-post-types.php index daf98ad0b..3d094140d 100644 --- a/includes/class-wp-job-manager-post-types.php +++ b/includes/class-wp-job-manager-post-types.php @@ -792,7 +792,7 @@ public function job_feed() { if ( ! empty( $job_manager_keyword ) ) { $query_args['s'] = $job_manager_keyword; - add_filter( 'posts_search', 'get_job_listings_keyword_search' ); + add_filter( 'posts_search', 'get_job_listings_keyword_search', 10, 2 ); } if ( empty( $query_args['meta_query'] ) ) { @@ -808,7 +808,7 @@ public function job_feed() { add_action( 'rss2_ns', [ $this, 'job_feed_namespace' ] ); add_action( 'rss2_item', [ $this, 'job_feed_item' ] ); do_feed_rss2( false ); - remove_filter( 'posts_search', 'get_job_listings_keyword_search' ); + remove_filter( 'posts_search', 'get_job_listings_keyword_search', 10 ); } /** diff --git a/wp-job-manager-functions.php b/wp-job-manager-functions.php index 82a39c32c..51d8e370d 100644 --- a/wp-job-manager-functions.php +++ b/wp-job-manager-functions.php @@ -196,7 +196,7 @@ function get_job_listings( $args = [] ) { if ( ! empty( $job_manager_keyword ) && strlen( $job_manager_keyword ) >= apply_filters( 'job_manager_get_listings_keyword_length_threshold', 2 ) ) { $query_args['s'] = $job_manager_keyword; - add_filter( 'posts_search', 'get_job_listings_keyword_search' ); + add_filter( 'posts_search', 'get_job_listings_keyword_search', 10, 2 ); } $query_args = apply_filters( 'job_manager_get_listings', $query_args, $args ); @@ -266,7 +266,7 @@ function get_job_listings( $args = [] ) { do_action( 'after_get_job_listings', $query_args, $args ); - remove_filter( 'posts_search', 'get_job_listings_keyword_search' ); + remove_filter( 'posts_search', 'get_job_listings_keyword_search', 10 ); return $result; } @@ -303,66 +303,173 @@ function _wpjm_shuffle_featured_post_results_helper( $a, $b ) { * @since 1.21.0 * @since 1.26.0 Moved from the `posts_clauses` filter to the `posts_search` to use WP Query's keyword * search for `post_title` and `post_content`. - * @param string $search + * @since $$next-version$$ Reimplemented to provide the same functionality with WP core search: + * - Support for double quotes and negating terms (-). + * - Breaks down terms into individual words. + * - Meta and taxonomy name search happens together with search in title, excerpt and post content. + * + * @param string $search The search string. + * @param WP_Query $wp_query The query. + * * @return string */ - function get_job_listings_keyword_search( $search ) { - global $wpdb, $job_manager_keyword; - - // Searchable Meta Keys: set to empty to search all meta keys. - $searchable_meta_keys = [ - '_job_location', - '_company_name', - '_application', - '_company_name', - '_company_tagline', - '_company_website', - '_company_twitter', - ]; + function get_job_listings_keyword_search( $search, $wp_query ) { + global $wpdb; + + if ( ! function_exists( 'job_manager_construct_secondary_conditions' ) && ! function_exists( 'job_manager_construct_post_conditions' ) ) { + /** + * Constructs SQL clauses that return posts which have metas and terms that include or exclude the search term. + * + * @param string $search_term The search term. + * @param bool $is_excluding Whether posts should be excluded if they match the search terms. + * @param string $wildcard_search The wildcard character or empty string for exact matches. + * + * @return array The SQL clauses. + */ + function job_manager_construct_secondary_conditions( $search_term, $is_excluding, $wildcard_search ) { + global $wpdb; + + if ( empty( $search_term ) ) { + return []; + } - $searchable_meta_keys = apply_filters( 'job_listing_searchable_meta_keys', $searchable_meta_keys ); + $searchable_meta_keys = [ + '_application', + '_company_name', + '_company_tagline', + '_company_website', + '_company_twitter', + '_job_location', + ]; - // Set Search DB Conditions. - $conditions = []; + /** + * Filters the meta keys that are used in job search. + * + * @param array $searchable_meta_keys The meta keys. + */ + $searchable_meta_keys = apply_filters( 'job_listing_searchable_meta_keys', $searchable_meta_keys ); + + $not_string = $is_excluding ? 'NOT ' : ''; + $conditions = []; + + /** + * Can be used to disable searching post meta for job searches. + * + * @param bool $enable_meta_search Return false to disable meta search. + */ + if ( apply_filters( 'job_listing_search_post_meta', true ) ) { + + $meta_value = $wildcard_search . $wpdb->esc_like( $search_term ) . $wildcard_search; + // Only selected meta keys. + if ( $searchable_meta_keys ) { + $meta_keys = implode( "','", array_map( 'esc_sql', $searchable_meta_keys ) ); + //phpcs:disabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Variables are safe or escaped. + $conditions[] = $wpdb->prepare( "{$wpdb->posts}.ID {$not_string}IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ( '${meta_keys}' ) AND meta_value LIKE %s )", $meta_value ); + } else { + // No meta keys defined, search all post meta value. + $conditions[] = $wpdb->prepare( "{$wpdb->posts}.ID {$not_string}IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_value LIKE %s )", $meta_value ); + //phpcs:enabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + } - // Search Post Meta. - if ( apply_filters( 'job_listing_search_post_meta', true ) ) { + // Search taxonomy. + $conditions[] = $wpdb->prepare( "{$wpdb->posts}.ID ${not_string}IN ( SELECT object_id FROM {$wpdb->term_relationships} AS tr LEFT JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id WHERE t.name LIKE %s )", $meta_value ); - // Only selected meta keys. - if ( $searchable_meta_keys ) { - $conditions[] = "{$wpdb->posts}.ID IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ( '" . implode( "','", array_map( 'esc_sql', $searchable_meta_keys ) ) . "' ) AND meta_value LIKE '%" . esc_sql( $job_manager_keyword ) . "%' )"; - } else { - // No meta keys defined, search all post meta value. - $conditions[] = "{$wpdb->posts}.ID IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_value LIKE '%" . esc_sql( $job_manager_keyword ) . "%' )"; + return $conditions; } - } - // Search taxonomy. - $conditions[] = "{$wpdb->posts}.ID IN ( SELECT object_id FROM {$wpdb->term_relationships} AS tr LEFT JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id WHERE t.name LIKE '%" . esc_sql( $job_manager_keyword ) . "%' )"; + /** + * Constructs SQL clauses that return posts which include or exclude the search term in the provided columns. + * + * @see WP_Query::parse_search() + * + * @param string $search_term The search term to match. + * @param bool $is_excluding Whether posts that match the search term should be excluded. + * @param string $wildcard_search The wildcard character or empty string for exact matches. + * @param array $search_columns The columns to check. + * + * @return array The SQL clauses. + */ + function job_manager_construct_post_conditions( $search_term, $is_excluding, $wildcard_search, $search_columns ) { + global $wpdb; + + if ( $is_excluding ) { + $like_op = 'NOT LIKE'; + } else { + $like_op = 'LIKE'; + } + + $like = $wildcard_search . $wpdb->esc_like( $search_term ) . $wildcard_search; + + $conditions = []; + foreach ( $search_columns as $search_column ) { + //phpcs:disabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Variables are safe or escaped. + $conditions[] = $wpdb->prepare( "( {$wpdb->posts}.$search_column $like_op %s )", $like ); + } + + $allow_query_attachment_by_filename = apply_filters( 'wp_allow_query_attachment_by_filename', false ); + if ( ! empty( $allow_query_attachment_by_filename ) ) { + $conditions[] = $wpdb->prepare( "(sq1.meta_value $like_op %s)", $like ); + //phpcs:enabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + return $conditions; + } + } /** - * Filters the conditions to use when querying job listings. Resulting array is joined with OR statements. - * - * @since 1.26.0 - * - * @param array $conditions Conditions to join by OR when querying job listings. - * @param string $job_manager_keyword Search query. + * This function aims to provide similar search functionality with WP core while also including meta and taxonomy terms + * in the searched columns. The functionality of WP_Query::parse_search is replicated but with additional SQL + * clauses which are generated in the job_manager_construct_secondary_conditions function. */ - $conditions = apply_filters( 'job_listing_search_conditions', $conditions, $job_manager_keyword ); - if ( empty( $conditions ) ) { - return $search; + $default_search_columns = [ 'post_title', 'post_excerpt', 'post_content' ]; + $search_columns = ! empty( $wp_query->query_vars['search_columns'] ) ? $wp_query->query_vars['search_columns'] : $default_search_columns; + if ( ! is_array( $search_columns ) ) { + $search_columns = [ $search_columns ]; } - $conditions_str = implode( ' OR ', $conditions ); + $search_columns = (array) apply_filters( 'post_search_columns', $search_columns, $wp_query->query_vars['s'], $wp_query ); + + // Use only supported search columns. + $search_columns = array_intersect( $search_columns, $default_search_columns ); + if ( empty( $search_columns ) ) { + $search_columns = $default_search_columns; + } + + // Search terms starting with the exclusion prefix should be removed from the job search results. + $exclusion_prefix = apply_filters( 'wp_query_search_exclusion_prefix', '-' ); + $wildcard_search = ! empty( $wp_query->query_vars['exact'] ) ? '' : '%'; + $new_search = ''; + $searchand = ''; + + foreach ( $wp_query->query_vars['search_terms'] as $search_term ) { + $is_excluding = $exclusion_prefix && str_starts_with( $search_term, $exclusion_prefix ); - if ( ! empty( $search ) ) { - $search = preg_replace( '/^ AND /', '', $search ); - $search = " AND ( {$search} OR ( {$conditions_str} ) )"; + if ( $is_excluding ) { + $search_term = substr( $search_term, 1 ); + $andor_op = 'AND'; + } else { + $andor_op = 'OR'; + } + + $conditions = job_manager_construct_post_conditions( $search_term, $is_excluding, $wildcard_search, $search_columns ); + $conditions = array_merge( $conditions, job_manager_construct_secondary_conditions( $search_term, $is_excluding, $wildcard_search ) ); + + $new_search .= "$searchand(" . implode( " $andor_op ", $conditions ) . ')'; + + $searchand = ' AND '; + } + + if ( ! empty( $new_search ) ) { + $new_search = " AND ({$new_search}) "; + if ( ! is_user_logged_in() ) { + $new_search .= " AND ({$wpdb->posts}.post_password = '') "; + } } else { - $search = " AND ( {$conditions_str} )"; + return $search; } - return $search; + return $new_search; } endif;