Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand job listing search #2821

Merged
merged 4 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions includes/class-wp-job-manager-post-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] ) ) {
Expand All @@ -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 );
}

/**
Expand Down
197 changes: 152 additions & 45 deletions wp-job-manager-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 );
gikaragia marked this conversation as resolved.
Show resolved Hide resolved

// 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 );
gikaragia marked this conversation as resolved.
Show resolved Hide resolved
}

$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
}
gikaragia marked this conversation as resolved.
Show resolved Hide resolved

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' ];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more big improvement to the search would be to prioritize matches in the title — and generally to sort results by relevance. Maybe we could do multiple searches, first just in the post title, then in content, metas? It could actually be faster for the first pages in some cases, since if there are enough results in the title, we don't need to do the content/meta searches.

Just bringing it up to discuss here, it'd be its own PR probably as there would be some extra complexity, like handling pagination. Could also do some weighting maybe?

$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 );
gikaragia marked this conversation as resolved.
Show resolved Hide resolved

// 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'] ) ? '' : '%';
gikaragia marked this conversation as resolved.
Show resolved Hide resolved
$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;

Expand Down
Loading