Index: src/wp-includes/deprecated.php
===================================================================
--- src/wp-includes/deprecated.php	(revision 25628)
+++ src/wp-includes/deprecated.php	(working copy)
@@ -3317,6 +3317,18 @@
 function _save_post_hook() {}
 
 /**
+ * Formerly used internally to tidy up the search terms.
+ *
+ * @access private
+ * @since 2.9.0
+ * @deprecated 3.7.0
+ */
+function _search_terms_tidy( $t ) {
+	_deprecated_function( __FUNCTION__, '3.5', '' );
+	return trim( $t, "\"'\n\r " );
+}
+
+/**
  * Check if the installed version of GD supports particular image type
  *
  * @since 2.9.0
Index: src/wp-includes/functions.php
===================================================================
--- src/wp-includes/functions.php	(revision 25628)
+++ src/wp-includes/functions.php	(working copy)
@@ -3703,19 +3703,6 @@
 }
 
 /**
- * Used internally to tidy up the search terms.
- *
- * @access private
- * @since 2.9.0
- *
- * @param string $t
- * @return string
- */
-function _search_terms_tidy($t) {
-	return trim($t, "\"'\n\r ");
-}
-
-/**
  * Returns true.
  *
  * Useful for returning true to filters easily.
Index: src/wp-includes/query.php
===================================================================
--- src/wp-includes/query.php	(revision 25628)
+++ src/wp-includes/query.php	(working copy)
@@ -1290,6 +1290,14 @@
 	 var $thumbnails_cached = false;
 
 	/**
+	 * Cached list of search stopwords.
+	 *
+	 * @since 3.7.0
+	 * @var array
+	 */
+	private $stopwords;
+
+	/**
 	 * Resets query flags to false.
 	 *
 	 * The query flags are what page info WordPress was able to figure out.
@@ -1895,6 +1903,166 @@
 	}
 
 	/**
+	 * Generate SQL for the WHERE clause based on passed search terms.
+	 *
+	 * @since 3.7.0
+	 *
+	 * @global type $wpdb
+	 * @param array $q
+	 * @param string $search
+	 */
+	function parse_search( &$q, $search ) {
+		global $wpdb;
+
+		// added slashes screw with quote grouping when done early, so done later
+		$q['s'] = stripslashes( $q['s'] );
+		if ( empty( $_GET['s'] ) && $this->is_main_query() )
+			$q['s'] = urldecode( $q['s'] );
+		// there are no line breaks in <input /> fields
+		$q['s'] = str_replace( array( "\r", "\n" ), '', $q['s'] );
+		$q['num_all_terms'] = 1;
+		if ( ! empty( $q['sentence'] ) ) {
+			$q['search_terms'] = array( $q['s'] );
+		} else {
+			if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $q['s'], $matches ) ) {
+				$q['num_all_terms'] = count( $matches[0] );
+				$q['search_terms'] = $this->parse_search_terms( $matches[0] );
+				// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence
+				if ( empty( $q['search_terms'] ) || count( $q['search_terms'] ) > 9 )
+					$q['search_terms'] = array( $q['s'] );
+			} else {
+				$q['search_terms'] = array( $q['s'] );
+			}
+		}
+
+		$n = ! empty( $q['exact'] ) ? '' : '%';
+		$searchand = '';
+		$q['search_orderby_title'] = array();
+		foreach ( $q['search_terms'] as $term ) {
+			$term = esc_sql( like_escape( $term ) );
+			if ( $n )
+				$q['search_orderby_title'][] = "$wpdb->posts.post_title LIKE '%$term%'";
+
+			$search .= "{$searchand}(($wpdb->posts.post_title LIKE '{$n}{$term}{$n}') OR ($wpdb->posts.post_content LIKE '{$n}{$term}{$n}'))";
+			$searchand = ' AND ';
+		}
+
+		if ( ! empty( $search ) ) {
+			$search = " AND ({$search}) ";
+			if ( ! is_user_logged_in() )
+				$search .= " AND ($wpdb->posts.post_password = '') ";
+		}
+
+		// Allow plugins to contextually add/remove/modify the search section of the database query
+		return apply_filters( 'posts_search', $search, $this );
+	}
+
+	/**
+	 * Check if the terms are suitable for searching.
+	 *
+	 * Includes array of stopwords (terms) that are excluded from the separate term matching when searching for posts.
+	 * The list of English stopwords is the approximate search engines list. MySQL has a much longer default list of full-text stopwords.
+	 *
+	 * @since 3.7.0
+	 *
+	 * @param array Terms to check
+	 * @return array
+	 */
+	protected function parse_search_terms( $terms ) {
+		$strtolower_func = function_exists( 'mb_strtolower' ) ? 'mb_strtolower' : 'strtolower';
+		$checked = $stopwords = array();
+
+		if ( ! isset( $this->stopwords ) ) {
+			// @todo translator comment
+			$_words = explode( ',', _x( 'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www',
+				'Comma separated list of common words to exclude when searching (stopwords).' ) );
+
+			foreach( $_words as $word ) { 
+				$word = trim( $word, "\r\n\t " ); 
+
+				if ( ! $word ) 
+					continue; 
+
+				$stopwords[] = $word; 
+			}
+
+			$this->stopwords = apply_filters( 'wp_search_stopwords', $stopwords );
+		}
+
+		foreach ( $terms as $term ) {
+			// keep before/after spaces when term is for exact match
+			if ( preg_match( '/^".+"$/', $term ) )
+				$term = trim( $term, "\"'" );
+			else
+				$term = trim( $term, "\"' " );
+
+			// \p{L} matches a single letter that is not a Chinese, Japanese, etc. char
+			if ( ! $term || preg_match( '/^\p{L}$/u', $term ) )
+				continue;
+
+			if ( in_array( $strtolower_func( $term ), $this->stopwords, true ) )
+				continue;
+
+			$checked[] = $term;
+		}
+
+		return $checked;
+	}
+
+	/**
+	 * Generate SQL for the ORDER BY condition based on passed search terms.
+	 *
+	 * @global wpdb $wpdb
+	 * @param array $q
+	 * @return string
+	 */
+	function parse_search_order( &$q ) {
+		global $wpdb;
+
+		$search_orderby = '';
+		// Allow plugins to override sorting on 'post_title' and/or 'post_content'.
+		// Passing an empty array would disable sorting.
+		$search_orderby_on = apply_filters( 'posts_search_orderby_on', array( 'post_title', 'post_content' ), $this );
+
+		if ( ! empty( $search_orderby_on ) ) {
+			if ( $q['num_all_terms'] > 1 ) {
+				$num_terms = count( $q['search_orderby_title'] );
+				$search_orderby_s = esc_sql( like_escape( $q['s'] ) );
+
+				if ( in_array( 'post_title', $search_orderby_on, true ) ) {
+					$search_orderby = '(CASE ';
+					// sentence match in 'post_title'
+					$search_orderby .= "WHEN $wpdb->posts.post_title LIKE '%{$search_orderby_s}%' THEN 1 ";
+
+					// sanity limit, sort as sentence when more than 6 terms
+					// (few searches are longer than 6 terms and most titles are not)
+					if ( $num_terms < 7 ) {
+						// all words in title
+						$search_orderby .= 'WHEN ' . implode( ' AND ', $q['search_orderby_title'] ) . ' THEN 2 ';
+						// any word in title, not needed when $num_terms == 1
+						if ( $num_terms > 1 )
+							$search_orderby .= 'WHEN ' . implode( ' OR ', $q['search_orderby_title'] ) . ' THEN 3 ';
+					}
+
+					// sentence match in 'post_content'
+					if ( in_array( 'post_content', $search_orderby_on, true ) )
+						$search_orderby .= "WHEN $wpdb->posts.post_content LIKE '%{$search_orderby_s}%' THEN 4 ";
+
+					$search_orderby .= 'ELSE 5 END)';
+				} elseif ( in_array( 'post_content', $search_orderby_on, true ) ) {
+					// Not sorting on 'post_title', order by sentence matches in 'post_content'
+					$search_orderby = "$wpdb->posts.post_content LIKE '%{$search_orderby_s}%' DESC";
+				}
+			} elseif ( in_array( 'post_title', $search_orderby_on, true ) ) {
+				// single word or sentence search
+				$search_orderby = reset( $q['search_orderby_title'] ) . ' DESC';
+			}
+		}
+		// Allow plugins to add/remove/modify the 'order by' for the search section of the database query
+		return apply_filters( 'posts_search_orderby', $search_orderby, $this );
+	}
+
+	/**
 	 * Sets the 404 property and saves whether query is feed.
 	 *
 	 * @since 2.0.0
@@ -2236,35 +2404,11 @@
 		}
 
 		// If a search pattern is specified, load the posts that match
-		if ( !empty($q['s']) ) {
-			// added slashes screw with quote grouping when done early, so done later
-			$q['s'] = stripslashes($q['s']);
-			if ( empty( $_GET['s'] ) && $this->is_main_query() )
-				$q['s'] = urldecode($q['s']);
-			if ( !empty($q['sentence']) ) {
-				$q['search_terms'] = array($q['s']);
-			} else {
-				preg_match_all('/".*?("|$)|((?<=[\r\n\t ",+])|^)[^\r\n\t ",+]+/', $q['s'], $matches);
-				$q['search_terms'] = array_map('_search_terms_tidy', $matches[0]);
-			}
-			$n = !empty($q['exact']) ? '' : '%';
-			$searchand = '';
-			foreach( (array) $q['search_terms'] as $term ) {
-				$term = esc_sql( like_escape( $term ) );
-				$search .= "{$searchand}(($wpdb->posts.post_title LIKE '{$n}{$term}{$n}') OR ($wpdb->posts.post_content LIKE '{$n}{$term}{$n}'))";
-				$searchand = ' AND ';
-			}
+		// Sanity check: search string shouldn't be more than 1600 characters.
+		// See ticket #21688 for more info.
+		if ( ! empty( $q['s'] ) && strlen( $q['s'] ) < 1600 )
+			$search = $this->parse_search( $q, $search );
 
-			if ( !empty($search) ) {
-				$search = " AND ({$search}) ";
-				if ( !is_user_logged_in() )
-					$search .= " AND ($wpdb->posts.post_password = '') ";
-			}
-		}
-
-		// Allow plugins to contextually add/remove/modify the search section of the database query
-		$search = apply_filters_ref_array('posts_search', array( $search, &$this ) );
-
 		// Taxonomies
 		if ( !$this->is_singular ) {
 			$this->parse_tax_query( $q );
@@ -2461,6 +2605,13 @@
 				$orderby .= " {$q['order']}";
 		}
 
+		// Order search results by relevance when another "orderby" is not specified in the query
+		if ( ! empty( $q['search_orderby_title'] ) && empty( $q['orderby'] ) ) {
+			$search_orderby = $this->parse_search_order( $q );
+			if ( $search_orderby )
+				$orderby = $orderby ? $search_orderby . ', ' . $orderby : $search_orderby;
+		}
+
 		if ( is_array( $post_type ) && count( $post_type ) > 1 ) {
 			$post_type_cap = 'multiple_post_type';
 		} else {
Index: tests/phpunit/tests/query/search.php
===================================================================
--- tests/phpunit/tests/query/search.php	(revision 0)
+++ tests/phpunit/tests/query/search.php	(working copy)
@@ -0,0 +1,80 @@
+<?php
+/**
+ *
+ * @group query
+ * @group search
+ */
+class Tests_Search_Results extends WP_UnitTestCase {
+	protected $q;
+	protected $post_type;
+
+	function setUp() {
+		parent::setUp();
+
+		$this->post_type = rand_str( 12 );
+		register_post_type( $this->post_type );
+
+		$this->q = new WP_Query();
+	}
+
+	function tearDown() {
+		parent::tearDown();
+
+		_unregister_post_type( $this->post_type );
+		unset( $this->q );
+	}
+
+	function get_search_results( $terms ) {
+		$args = http_build_query( array( 's' => $terms, 'post_type' => $this->post_type ) );
+		return $this->q->query( $args );
+	}
+
+	function test_search_order_title_relevance() {
+		foreach ( range( 1, 7 ) as $i )
+			$this->factory->post->create( array( 'post_content' => $i . rand_str() . ' about', 'post_type' => $this->post_type ) );
+		$post_id = $this->factory->post->create( array( 'post_title' => 'About', 'post_type' => $this->post_type ) );
+
+		$posts = $this->get_search_results( 'About' );
+		$this->assertEquals( $post_id, reset( $posts )->ID );
+	}
+
+	function test_search_order_content_relevance() {
+		$post_id1 = $this->factory->post->create( array( 'post_title' => 'About', 'post_type' => $this->post_type ) );
+		$post_id2 = $this->factory->post->create( array( 'post_content' => 'About', 'post_type' => $this->post_type ) );
+
+		$posts1 = $this->get_search_results( 'About' );
+		$this->assertEquals( $post_id1, reset( $posts1 )->ID );
+
+		add_filter( 'posts_search_orderby_on', array( $this, 'filter_posts_search_orderby_on' ) );
+
+		$posts2 = $this->get_search_results( 'About' );
+		$this->assertEquals( $post_id2, reset( $posts2 )->ID );
+
+		remove_filter( 'posts_search_orderby_on', array( $this, 'filter_posts_search_orderby_on' ) );
+	}
+
+	function filter_posts_search_orderby_on() {
+		return array( 'post_content' );
+	}
+
+	function test_search_terms_query_var() {
+		$terms = 'This is a search term';
+		$query = new WP_Query( array( 's' => 'This is a search term' ) );
+		$this->assertNotEquals( explode( ' ', $terms ), $query->get( 'search_terms' ) );
+		$this->assertEquals( array( 'search', 'term' ), $query->get( 'search_terms' ) );
+	}
+
+	function test_filter_stopwords() {
+		$terms = 'This is a search term';
+		add_filter( 'wp_search_stopwords', array( $this, 'filter_wp_search_stopwords' ) );
+		$query = new WP_Query( array( 's' => $terms ) );
+		remove_filter( 'wp_search_stopwords', array( $this, 'filter_wp_search_stopwords' ) );
+
+		$this->assertNotEquals( array( 'search', 'term' ), $query->get( 'search_terms' ) );
+		$this->assertEquals( array( 'This', 'is', 'search', 'term' ), $query->get( 'search_terms' ) );
+	}
+
+	function filter_wp_search_stopwords() {
+		return array();
+	}
+}
\ No newline at end of file

Property changes on: tests/phpunit/tests/query/search.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
