Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.631
diff -u -u -p -r1.631 comment.module
--- modules/comment/comment.module	6 May 2008 12:18:47 -0000	1.631
+++ modules/comment/comment.module	10 May 2008 17:28:48 -0000
@@ -1970,3 +1970,17 @@ function comment_unpublish_by_keyword_ac
     }
   }
 }
+
+/**
+ * Implementation of hook_ranking
+ */
+function comment_ranking() {
+  return array(
+    'comments' => array(
+      'title' => t('Number of comments'),
+      'join' => 'LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid',
+      'score' => '2.0 - 2.0 / (1.0 + c.comment_count * %f)',
+      'arguments' => array(variable_get('node_cron_comments_scale', 0)),
+    ),
+  );
+}
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.963
diff -u -u -p -r1.963 node.module
--- modules/node/node.module	10 May 2008 13:19:50 -0000	1.963
+++ modules/node/node.module	10 May 2008 17:28:49 -0000
@@ -1141,6 +1141,36 @@ function node_perm() {
   return $perms;
 }
 
+function _node_rankings() {
+  $rankings = array(
+    'total' => 0, 'join' => array(), 'score' => array(), 'args' => array(),
+  );
+  if ($ranking = module_invoke_all('ranking')) {
+    foreach ($ranking as $rank => $values) {
+      if ($node_rank = variable_get('node_rank_'. $rank, 5)) {
+        // if the join doesn't already exist, add it
+        if (isset($values['join']) && !isset($rankings['join'][$values['join']])) {
+          $rankings['join'][$values['join']] = $values['join'];
+        }
+
+        // add the weighted score multiplier value, handle NULL gracefully
+        $rankings['score'][] = '%f * COALESCE(('. $values['score'] .'), 0)';
+
+        // add the the weighted score multiplier value
+        $rankings['total'] += $node_rank;
+        $rankings['arguments'][] = $node_rank;
+
+        // add the other terms
+        if (isset($values['arguments'])) {
+          $rankings['arguments'] = array_merge($rankings['arguments'], $values['arguments']);
+        }
+      }
+    }
+  }
+  return $rankings;
+}
+
+
 /**
  * Implementation of hook_search().
  */
@@ -1170,23 +1200,14 @@ function node_search($op = 'search', $ke
         '#value' => '<em>' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em>'
       );
 
-      $ranking = array('node_rank_relevance' => t('Keyword relevance'),
-                       'node_rank_recent' => t('Recently posted'));
-      if (module_exists('comment')) {
-        $ranking['node_rank_comments'] = t('Number of comments');
-      }
-      if (module_exists('statistics') && variable_get('statistics_count_content_views', 0)) {
-        $ranking['node_rank_views'] = t('Number of views');
-      }
-
       // Note: reversed to reflect that higher number = higher ranking.
       $options = drupal_map_assoc(range(0, 10));
-      foreach ($ranking as $var => $title) {
-        $form['content_ranking']['factors'][$var] = array(
-          '#title' => $title,
+      foreach (module_invoke_all('ranking') as $var => $values) {
+        $form['content_ranking']['factors']['node_rank_'. $var] = array(
+          '#title' => $values['title'],
           '#type' => 'select',
           '#options' => $options,
-          '#default_value' => variable_get($var, 5),
+          '#default_value' => variable_get('node_rank_'. $var, 5),
         );
       }
       return $form;
@@ -1228,60 +1249,20 @@ function node_search($op = 'search', $ke
         $keys = search_query_insert($keys, 'language');
       }
 
-      // Build ranking expression (we try to map each parameter to a
-      // uniform distribution in the range 0..1).
-      $ranking = array();
-      $arguments2 = array();
-      $join2 = '';
-      // Used to avoid joining on node_comment_statistics twice
-      $stats_join = FALSE;
-      $total = 0;
-      if ($weight = (int)variable_get('node_rank_relevance', 5)) {
-        // Average relevance values hover around 0.15
-        $ranking[] = '%d * i.relevance';
-        $arguments2[] = $weight;
-        $total += $weight;
-      }
-      if ($weight = (int)variable_get('node_rank_recent', 5)) {
-        // Exponential decay with half-life of 6 months, starting at last indexed node
-        $ranking[] = '%d * POW(2, (GREATEST(MAX(n.created), MAX(n.changed), MAX(c.last_comment_timestamp)) - %d) * 6.43e-8)';
-        $arguments2[] = $weight;
-        $arguments2[] = (int)variable_get('node_cron_last', 0);
-        $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
-        $stats_join = TRUE;
-        $total += $weight;
-      }
-      if (module_exists('comment') && $weight = (int)variable_get('node_rank_comments', 5)) {
-        // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
-        $scale = variable_get('node_cron_comments_scale', 0.0);
-        $ranking[] = '%d * (2.0 - 2.0 / (1.0 + MAX(c.comment_count) * %f))';
-        $arguments2[] = $weight;
-        $arguments2[] = $scale;
-        if (!$stats_join) {
-          $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
-        }
-        $total += $weight;
-      }
-      if (module_exists('statistics') && variable_get('statistics_count_content_views', 0) &&
-          $weight = (int)variable_get('node_rank_views', 5)) {
-        // Inverse law that maps the highest view count on the site to 1 and 0 to 0.
-        $scale = variable_get('node_cron_views_scale', 0.0);
-        $ranking[] = '%d * (2.0 - 2.0 / (1.0 + MAX(nc.totalcount) * %f))';
-        $arguments2[] = $weight;
-        $arguments2[] = $scale;
-        $join2 .= ' LEFT JOIN {node_counter} nc ON nc.nid = i.sid';
-        $total += $weight;
-      }
+      // Get the ranking expressions.
+      $rankings = _node_rankings();
 
       // When all search factors are disabled (ie they have a weight of zero),
-      // the default score is based only on keyword relevance and there is no need to
-      // adjust the score of each item.
-      if ($total == 0) {
+      // the default score is based only on keyword relevance.
+      if ($rankings['total'] == 0) {
         $select2 = 'i.relevance AS score';
         $total = 1;
       }
       else {
-        $select2 = implode(' + ', $ranking) . ' AS score';
+        $total = $rankings['total'];
+        $arguments2 = $rankings['arguments'];
+        $join2 = implode(' ', $rankings['join']);
+        $select2 = '('. implode(' + ', $rankings['score']) .') AS score';
       }
 
       // Do search.
@@ -1302,6 +1283,7 @@ function node_search($op = 'search', $ke
         $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index');
 
         $extra = node_invoke_nodeapi($node, 'search result');
+
         $results[] = array(
           'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
           'type' => check_plain(node_get_types('name', $node)),
@@ -1310,7 +1292,7 @@ function node_search($op = 'search', $ke
           'date' => $node->changed,
           'node' => $node,
           'extra' => $extra,
-          'score' => $item->score / $total,
+          'score' => $total ? ($item->score / $total) : 0,
           'snippet' => search_excerpt($keys, $node->body),
         );
       }
@@ -1319,6 +1301,37 @@ function node_search($op = 'search', $ke
 }
 
 /**
+ * Implementation of hook_ranking
+ */
+function node_ranking() {
+  // Create the ranking array and add the basic ranking options.
+  $ranking = array(
+    'relevance' => array(
+      'title' => t('Keyword relevance'),
+      'score' => 'i.relevance',
+    ),
+    'sticky' => array(
+      'title' => t('Node is Sticky'),
+      'score' => 'n.sticky',
+    ),
+    'promote' => array(
+      'title' => t('Node is Promoted'),
+      'score' => 'n.promote',
+    ),
+  );
+  
+  // Add relevance based on creation or changed date.
+  if ($node_cron_last = variable_get('node_cron_last', 0)) {
+    $ranking['recent'] = array(
+      'title' => t('Recently posted'),
+      'score' => '(POW(2, GREATEST(n.created, n.changed) - %d) * 6.43e-8)',
+      'arguments' => array($node_cron_last),
+    );
+  }
+  return $ranking;
+}
+
+/**
  * Implementation of hook_user().
  */
 function node_user($op, &$edit, &$user) {
Index: modules/search/search.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/search/search.test,v
retrieving revision 1.1
diff -u -u -p -r1.1 search.test
--- modules/search/search.test	20 Apr 2008 18:23:29 -0000	1.1
+++ modules/search/search.test	10 May 2008 17:28:49 -0000
@@ -160,3 +160,82 @@ class SearchMatchTestCase extends Drupal
     $this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
   }
 }
+
+/**
+ * Base class for simpletests to verify search ranking results
+ */
+class SearchRankingTestCase extends DrupalWebTestCase {
+  /**
+   * Implementation of getInfo().
+   */
+  function getInfo() {
+    return array(
+      'name' => t('Search engine ranking'),
+      'description' => t('Indexes content and tests ranking factors.'),
+      'group' => t('Search'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp('search');
+  }
+
+  function testRankings() {
+    $this->drupalLogin($this->drupalCreateUser(array('access comments', 'post comments without approval', 'create page content')));
+
+    $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
+
+    // Create nodes for testing.
+    foreach ($node_ranks as $node_rank) {
+      $settings = array('type' => 'page', 'title' => 'Drupal rocks', 'body' => "Drupal's search rocks", 'changed' => time());
+      foreach (array(0, 1) as $num) {
+        if ($num == 1) {
+          switch ($node_rank) {
+            case 'sticky':
+            case 'promote':
+              $settings[$node_rank] = 1;
+              break;
+            case 'relevance':
+              $settings['body'] .= " really rocks";
+              break;
+            case 'recent':
+              $settings['changed'] ++;
+              break;
+            case 'comments':
+              $settings['comment'] = 2;
+              break;
+          }
+        }
+        $nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
+        _node_index_node($nodes[$node_rank][$num]);
+      }
+    }
+    search_update_totals();
+
+    // Add a comment to one of the nodes.
+    $edit = array('subject' => 'my comment title', 'comment' => 'some random comment');
+    $this->drupalGet('comment/reply/'. $nodes['comments'][1]->nid);
+    $this->drupalPost(NULL, $edit, t('Preview'));
+    $this->drupalPost(NULL, $edit, t('Save'));
+
+    // View one of the nodes a bunch of times.
+    for ($i = 0; $i < 5; $i ++) {
+      $this->drupalGet('node/' . $nodes['views'][1]->nid);
+    }
+
+    // Test each of the possible rankings.
+    foreach ($node_ranks as $node_rank) {
+      // Disable all relevancy rankings except the one we are testing.
+      foreach ($node_ranks as $var) {
+        variable_set('node_rank_'. $var, $var == $node_rank ? 10 : 0);
+      }
+
+      // @NOTE: there's a bug right now that you need to have relevance.
+//    variable_set('node_rank_relevance', 1);
+
+      // Do the search and assert the results.
+      $set = node_search('search', 'rocks');
+      $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "'. $node_rank .'" order.');
+    }
+  }
+}
Index: modules/statistics/statistics.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.module,v
retrieving revision 1.277
diff -u -u -p -r1.277 statistics.module
--- modules/statistics/statistics.module	6 May 2008 12:18:50 -0000	1.277
+++ modules/statistics/statistics.module	10 May 2008 17:28:49 -0000
@@ -314,3 +314,19 @@ function statistics_nodeapi(&$node, $op,
       db_query('DELETE FROM {node_counter} WHERE nid = %d', $node->nid);
   }
 }
+
+/**
+ * Implementation of hook_ranking
+ */
+function statistics_ranking() {
+  if (variable_get('statistics_count_content_views', 0)) {
+    return array(
+      'views' => array(
+        'title' => t('Number of views'),
+        'join' => 'LEFT JOIN {node_counter} nc ON nc.nid = i.sid',
+        'score' => '2.0 - 2.0 / (1.0 + nc.totalcount * %f)',
+        'arguments' => array(variable_get('node_cron_views_scale', 0)),
+      ),
+    );
+  }
+}
