Index: modules/node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.46 diff -u -p -r1.46 node.install --- modules/node/node.install 28 Mar 2010 11:16:29 -0000 1.46 +++ modules/node/node.install 22 Apr 2010 06:16:56 -0000 @@ -673,6 +673,13 @@ function node_update_7010() { } /** + * Save the oldest and newest changed nodes for 'recent' node rankings. + */ +function node_update_7011() { + node_rank_recent_update_times(); +} + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */ Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1263 diff -u -p -r1.1263 node.module --- modules/node/node.module 20 Apr 2010 09:30:21 -0000 1.1263 +++ modules/node/node.module 22 Apr 2010 06:16:56 -0000 @@ -160,12 +160,23 @@ function node_theme() { * Implements hook_cron(). */ function node_cron() { + node_rank_recent_update_times(); db_delete('history') ->condition('timestamp', NODE_NEW_LIMIT, '<') ->execute(); } /** + * Save the oldest and newest changed nodes for 'recent' node rankings. + */ +function node_rank_recent_update_times() { + if ($changed = db_query("SELECT MIN(changed) AS oldest, MAX(changed) AS newest FROM {node}")->fetchObject()) { + variable_set('node_rank_recent_oldest_time', $changed->oldest); + variable_set('node_rank_recent_newest_time', $changed->newest); + } +} + +/** * Implements hook_entity_info(). */ function node_entity_info() { @@ -1604,6 +1615,8 @@ function node_search_execute($keys = NUL */ function node_ranking() { // Create the ranking array and add the basic ranking options. + $oldest_time = variable_get('node_rank_recent_oldest_time', 0); + $newest_time = variable_get('node_rank_recent_newest_time', REQUEST_TIME); $ranking = array( 'relevance' => array( 'title' => t('Keyword relevance'), @@ -1620,17 +1633,18 @@ function node_ranking() { // The promote flag is either 0 or 1, which is automatically normalized. 'score' => 'n.promote', ), - ); - - // Add relevance based on creation or changed date. - if ($node_cron_last = variable_get('node_cron_last', 0)) { - $ranking['recent'] = array( + 'recent' => array( 'title' => t('Recently posted'), - // Exponential decay with half-life of 6 months, starting at last indexed node - 'score' => 'POW(2.0, (GREATEST(n.created, n.changed) - :node_cron_last) * 6.43e-8)', - 'arguments' => array(':node_cron_last' => $node_cron_last), - ); - } + // Weight newest items highest and oldest items lowest, in the range + // 0.0 to 1.0, with most older items scoring very low, and only the very + // newest items scoring high. This is an exponential decay algorithm. + 'score' => 'POW(2.718, -5 * (1 - (n.changed - :oldest_time) / :time_range))', + 'arguments' => array( + ':oldest_time' => $oldest_time, + ':time_range' => max($newest_time - $oldest_time, 1), + ), + ), + ); return $ranking; } Index: modules/search/search.test =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.test,v retrieving revision 1.59 diff -u -p -r1.59 search.test --- modules/search/search.test 16 Apr 2010 13:53:43 -0000 1.59 +++ modules/search/search.test 22 Apr 2010 06:16:56 -0000 @@ -7,7 +7,57 @@ define('SEARCH_TYPE', '_test_'); define('SEARCH_TYPE_2', '_test2_'); define('SEARCH_TYPE_JPN', '_test3_'); -class SearchMatchTestCase extends DrupalWebTestCase { +/** + * Helper class for search tests. + */ +class SearchWebTestCase extends DrupalWebTestCase { + /** + * Test the matching abilities of the engine. + * + * Verify if a query produces the correct results. + */ + function _testQueryMatching($query, $set, $results) { + // Get result IDs. + $found = array(); + foreach ($set as $item) { + $found[] = $item->sid; + } + + // Compare $results and $found. + sort($found); + sort($results); + $this->assertEqual($found, $results, "Query matching '$query'"); + } + + /** + * Test the scoring abilities of the engine. + * + * Verify if a query produces normalized, monotonous scores. + */ + function _testQueryScores($query, $set, $key = 'calculated_score') { + if (!$set) { + return; + } + + // Get result scores. + $scores = array(); + foreach ($set as $item) { + $item = (array) $item; + $scores[] = $item[$key]; + } + + // Check order. + $sorted = $scores; + sort($sorted); + $this->assertEqual($scores, array_reverse($sorted), "Query order '$query'"); + + // Check range. + $this->assertTrue(min($scores) >= 0.0, "Query scoring min '$query' = " . min($scores)); + $this->assertTrue(max($scores) <= 1.0001, "Query scoring max '$query' = " . max($scores)); + } +} + +class SearchMatchTestCase extends SearchWebTestCase { public static function getInfo() { return array( 'name' => 'Search engine queries', @@ -149,7 +199,7 @@ class SearchMatchTestCase extends Drupal $set = $result ? $result->fetchAll() : array(); $this->_testQueryMatching($query, $set, $results); - $this->_testQueryScores($query, $set, $results); + $this->_testQueryScores($query, $set); } // These queries are run against the second index type, SEARCH_TYPE_2. @@ -169,7 +219,7 @@ class SearchMatchTestCase extends Drupal $set = $result ? $result->fetchAll() : array(); $this->_testQueryMatching($query, $set, $results); - $this->_testQueryScores($query, $set, $results); + $this->_testQueryScores($query, $set); } // These queries are run against the third index type, SEARCH_TYPE_JPN. @@ -192,51 +242,12 @@ class SearchMatchTestCase extends Drupal $set = $result ? $result->fetchAll() : array(); $this->_testQueryMatching($query, $set, $results); - $this->_testQueryScores($query, $set, $results); - } - } - - /** - * Test the matching abilities of the engine. - * - * Verify if a query produces the correct results. - */ - function _testQueryMatching($query, $set, $results) { - // Get result IDs. - $found = array(); - foreach ($set as $item) { - $found[] = $item->sid; + $this->_testQueryScores($query, $set); } - - // Compare $results and $found. - sort($found); - sort($results); - $this->assertEqual($found, $results, "Query matching '$query'"); - } - - /** - * Test the scoring abilities of the engine. - * - * Verify if a query produces normalized, monotonous scores. - */ - function _testQueryScores($query, $set, $results) { - // Get result scores. - $scores = array(); - foreach ($set as $item) { - $scores[] = $item->calculated_score; - } - - // Check order. - $sorted = $scores; - sort($sorted); - $this->assertEqual($scores, array_reverse($sorted), "Query order '$query'"); - - // Check range. - $this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'"); } } -class SearchBikeShed extends DrupalWebTestCase { +class SearchBikeShed extends SearchWebTestCase { protected $searching_user; public static function getInfo() { @@ -266,7 +277,7 @@ class SearchBikeShed extends DrupalWebTe } } -class SearchAdvancedSearchForm extends DrupalWebTestCase { +class SearchAdvancedSearchForm extends SearchWebTestCase { protected $node; public static function getInfo() { @@ -329,7 +340,7 @@ class SearchAdvancedSearchForm extends D } } -class SearchRankingTestCase extends DrupalWebTestCase { +class SearchRankingTestCase extends SearchWebTestCase { public static function getInfo() { return array( 'name' => 'Search engine ranking', @@ -356,6 +367,7 @@ class SearchRankingTestCase extends Drup foreach ($node_ranks as $node_rank) { $settings = array('type' => 'page', 'title' => array(LANGUAGE_NONE => array(array('value' => 'Drupal rocks'))), 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks")))); foreach (array(0, 1) as $num) { + $settings['created'] = REQUEST_TIME - mt_rand(60, 3600); if ($num == 1) { switch ($node_rank) { case 'sticky': @@ -366,7 +378,7 @@ class SearchRankingTestCase extends Drup $settings['body'][LANGUAGE_NONE][0]['value'] .= " really rocks"; break; case 'recent': - $settings['created'] = REQUEST_TIME + 3600; + unset($settings['created']); break; case 'comments': $settings['comment'] = 2; @@ -375,9 +387,11 @@ class SearchRankingTestCase extends Drup } $nodes[$node_rank][$num] = $this->drupalCreateNode($settings); } + db_query("UPDATE {node} SET changed = created"); } // Update the search index. + node_rank_recent_update_times(); module_invoke_all('update_index'); search_update_totals(); @@ -409,7 +423,13 @@ class SearchRankingTestCase extends Drup // Do the search and assert the results. $set = node_search_execute('rocks'); - $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.'); + $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, "Search ranking '$node_rank' order."); + + // Ensure every score is within range. + $this->_testQueryScores($node_rank, $set, 'score'); + + // Ensure the top ranking item has a value close to 1.0. + $this->assertTrue($set[0]['score'] > 0.9, "Search ranking '$node_rank' top score = " . $set[0]['score']); } } @@ -453,7 +473,7 @@ class SearchRankingTestCase extends Drup } } -class SearchBlockTestCase extends DrupalWebTestCase { +class SearchBlockTestCase extends SearchWebTestCase { public static function getInfo() { return array( 'name' => 'Block availability', @@ -515,7 +535,7 @@ class SearchBlockTestCase extends Drupal /** * Test integration searching comments. */ -class SearchCommentTestCase extends DrupalWebTestCase { +class SearchCommentTestCase extends SearchWebTestCase { protected $admin_user; public static function getInfo() { @@ -620,7 +640,7 @@ class SearchCommentTestCase extends Drup /** * Test search_simplify() on every Unicode character. */ -class SearchSimplifyTestCase extends DrupalWebTestCase { +class SearchSimplifyTestCase extends SearchWebTestCase { public static function getInfo() { return array( 'name' => 'Search simplify', @@ -653,7 +673,7 @@ class SearchSimplifyTestCase extends Dru /** * Test config page. */ -class SearchConfigSettingsForm extends DrupalWebTestCase { +class SearchConfigSettingsForm extends SearchWebTestCase { public static function getInfo() { return array( 'name' => 'Config settings form',