From 9c9cdf28ca784f9e17604dd08a5f8c116a75bd8e Mon Sep 17 00:00:00 2001 From: Boone Gorges Date: Thu, 8 Oct 2015 03:17:22 +0000 Subject: [PATCH] Allow excluded keywords when searching posts. Pass a keyword with a leading hyphen to exclude posts containing that keyword. For example, 'taco -onions' will return posts that contain the word 'taco' but do not contain the word 'onions'. Props akibjorklund. Fixes #33988. git-svn-id: https://develop.svn.wordpress.org/trunk@34934 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/query.php | 38 +++++++++++++--- tests/phpunit/tests/query/search.php | 67 ++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php index 94e48391d9..f3287dae9d 100644 --- a/src/wp-includes/query.php +++ b/src/wp-includes/query.php @@ -1509,7 +1509,8 @@ class WP_Query { * @since 1.5.0 * @since 4.2.0 Introduced the ability to order by specific clauses of a `$meta_query`, by passing the clause's * array key to `$orderby`. - * @since 4.4.0 Introduced `$post_name__in` and `$title` parameters. + * @since 4.4.0 Introduced `$post_name__in` and `$title` parameters. `$s` was updated to support excluded + * search terms, by prepending a hyphen. * @access public * * @param string|array $query { @@ -1587,7 +1588,9 @@ class WP_Query { * @type int $posts_per_archive_page The number of posts to query for by archive page. Overrides * 'posts_per_page' when is_archive(), or is_search() are true. * @type array $post_name__in An array of post slugs that results must match. - * @type string $s Search keyword. + * @type string $s Search keyword(s). Prepending a term with a hyphen will + * exclude posts matching that term. Eg, 'pillow -sofa' will + * return posts containing 'pillow' but not 'sofa'. * @type int $second Second of the minute. Default empty. Accepts numbers 0-60. * @type bool $sentence Whether to search by phrase. Default false. * @type bool $suppress_filters Whether to suppress filters. Default false. @@ -2158,13 +2161,24 @@ class WP_Query { $searchand = ''; $q['search_orderby_title'] = array(); foreach ( $q['search_terms'] as $term ) { - if ( $n ) { + // Terms prefixed with '-' should be excluded. + $include = '-' !== substr( $term, 0, 1 ); + if ( $include ) { + $like_op = 'LIKE'; + $andor_op = 'OR'; + } else { + $like_op = 'NOT LIKE'; + $andor_op = 'AND'; + $term = substr( $term, 1 ); + } + + if ( $n && $include ) { $like = '%' . $wpdb->esc_like( $term ) . '%'; $q['search_orderby_title'][] = $wpdb->prepare( "$wpdb->posts.post_title LIKE %s", $like ); } $like = $n . $wpdb->esc_like( $term ) . $n; - $search .= $wpdb->prepare( "{$searchand}(($wpdb->posts.post_title LIKE %s) OR ($wpdb->posts.post_content LIKE %s))", $like, $like ); + $search .= $wpdb->prepare( "{$searchand}(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like ); $searchand = ' AND '; } @@ -2264,11 +2278,19 @@ class WP_Query { if ( $q['search_terms_count'] > 1 ) { $num_terms = count( $q['search_orderby_title'] ); - $like = '%' . $wpdb->esc_like( $q['s'] ) . '%'; + + // If the search terms contain negative queries, don't bother ordering by sentence matches. + $like = ''; + if ( ! preg_match( '/(?:\s|^)\-/', $q['s'] ) ) { + $like = '%' . $wpdb->esc_like( $q['s'] ) . '%'; + } $search_orderby = '(CASE '; + // sentence match in 'post_title' - $search_orderby .= $wpdb->prepare( "WHEN $wpdb->posts.post_title LIKE %s THEN 1 ", $like ); + if ( $like ) { + $search_orderby .= $wpdb->prepare( "WHEN $wpdb->posts.post_title LIKE %s THEN 1 ", $like ); + } // sanity limit, sort as sentence when more than 6 terms // (few searches are longer than 6 terms and most titles are not) @@ -2281,7 +2303,9 @@ class WP_Query { } // sentence match in 'post_content' - $search_orderby .= $wpdb->prepare( "WHEN $wpdb->posts.post_content LIKE %s THEN 4 ", $like ); + if ( $like ) { + $search_orderby .= $wpdb->prepare( "WHEN $wpdb->posts.post_content LIKE %s THEN 4 ", $like ); + } $search_orderby .= 'ELSE 5 END)'; } else { // single word or sentence search diff --git a/tests/phpunit/tests/query/search.php b/tests/phpunit/tests/query/search.php index 9ef912667d..b81bef3ee1 100644 --- a/tests/phpunit/tests/query/search.php +++ b/tests/phpunit/tests/query/search.php @@ -58,4 +58,71 @@ class Tests_Query_Search extends WP_UnitTestCase { function filter_wp_search_stopwords() { return array(); } + + /** + * @ticket 33988 + */ + public function test_s_should_exclude_term_prefixed_with_dash() { + $p1 = $this->factory->post->create( array( + 'post_status' => 'publish', + 'post_content' => 'This post has foo but also bar', + ) ); + $p2 = $this->factory->post->create( array( + 'post_status' => 'publish', + 'post_content' => 'This post has only foo', + ) ); + + $q = new WP_Query( array( + 's' => 'foo -bar', + 'fields' => 'ids', + ) ); + + $this->assertEqualSets( array( $p2 ), $q->posts ); + } + + /** + * @ticket 33988 + */ + public function test_s_should_exclude_first_term_if_prefixed_with_dash() { + $p1 = $this->factory->post->create( array( + 'post_status' => 'publish', + 'post_content' => 'This post has foo but also bar', + ) ); + $p2 = $this->factory->post->create( array( + 'post_status' => 'publish', + 'post_content' => 'This post has only bar', + ) ); + + $q = new WP_Query( array( + 's' => '-foo bar', + 'fields' => 'ids', + ) ); + + $this->assertEqualSets( array( $p2 ), $q->posts ); + } + + /** + * @ticket 33988 + */ + public function test_s_should_not_exclude_for_dashes_in_the_middle_of_words() { + $p1 = $this->factory->post->create( array( + 'post_status' => 'publish', + 'post_content' => 'This post has foo but also bar', + ) ); + $p2 = $this->factory->post->create( array( + 'post_status' => 'publish', + 'post_content' => 'This post has only bar', + ) ); + $p3 = $this->factory->post->create( array( + 'post_status' => 'publish', + 'post_content' => 'This post has only foo-bar', + ) ); + + $q = new WP_Query( array( + 's' => 'foo-bar', + 'fields' => 'ids', + ) ); + + $this->assertEqualSets( array( $p3 ), $q->posts ); + } }