Index: includes/theme.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/theme.inc,v
retrieving revision 1.525
diff -u -u -p -r1.525 theme.inc
--- includes/theme.inc	15 Sep 2009 20:03:18 -0000	1.525
+++ includes/theme.inc	18 Sep 2009 14:40:21 -0000
@@ -1972,8 +1972,8 @@ function theme_username($object) {
  * @return
  *   A themed HTML string representing the progress bar.
  */
-function theme_progress_bar($percent, $message) {
-  $output = '<div id="progress" class="progress">';
+function theme_progress_bar($percent, $message, $class = '') {
+  $output = '<div id="progress" class="progress' . (empty($class) ? '' : ' ' . $class) . '">';
   $output .= '<div class="bar"><div class="filled" style="width: ' . $percent . '%"></div></div>';
   $output .= '<div class="percentage">' . $percent . '%</div>';
   $output .= '<div class="message">' . $message . '</div>';
Index: modules/search/search.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/search/search.admin.inc,v
retrieving revision 1.11
diff -u -u -p -r1.11 search.admin.inc
--- modules/search/search.admin.inc	29 Aug 2009 21:05:16 -0000	1.11
+++ modules/search/search.admin.inc	18 Sep 2009 14:40:21 -0000
@@ -9,15 +9,32 @@
 /**
  * Menu callback: confirm wiping of the index.
  */
-function search_reindex_confirm() {
-  return confirm_form(array(), t('Are you sure you want to re-index the site?'),
-                  'admin/config/search/settings', t(' The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed. This action cannot be undone.'), t('Re-index site'), t('Cancel'));
+function search_reset_confirm() {
+  // Create the index status form from the status metrics.
+  $metrics = _search_get_status_metrics('@done items will be affected.');
+  $form = array();
+  $form['status'] = array(
+    '#type' => 'fieldset',
+    '#description' =>  t('Search will still work while the remaining items are indexed.'),
+    'progress' => array(
+      '#type' => 'markup',
+      '#markup' => theme('progress_bar', $metrics['percentage'], $metrics['message'], 'inactive'),
+    ),
+  );
+  return confirm_form(
+    $form,
+    t('Are you sure you want to reset the search index?'),
+    'admin/config/search/settings',
+    '',
+    t('Reset index'),
+    t('Cancel')
+  );
 }
 
 /**
- * Handler for wipe confirmation
+ * Handler for wipe confirmation.
  */
-function search_reindex_confirm_submit(&$form, &$form_state) {
+function search_reset_confirm_submit(&$form, &$form_state) {
   if ($form['confirm']) {
     search_reindex();
     drupal_set_message(t('The index will be rebuilt.'));
@@ -30,7 +47,6 @@ function search_reindex_confirm_submit(&
  * Helper function to get real module names.
  */
 function _search_get_module_names() {
-
   $search_info = search_get_info();
   $modules = db_select('system', 's')
     ->fields('s', array('name', 'info'))
@@ -48,30 +64,74 @@ function _search_get_module_names() {
 }
 
 /**
- * Menu callback; displays the search module settings page.
- *
- * @ingroup forms
- * @see system_settings_form()
- * @see search_admin_settings_submit()
- * @see search_admin_reindex_submit()
+ * Return the search status metrics, which is used by several of the search forms.
  */
-function search_admin_settings() {
-  // Collect some stats
+function _search_get_status_metrics($message = array()) {
+  // Collect some stats.
   $remaining = 0;
   $total = 0;
-  foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
+  foreach (variable_get('search_active_modules', array('node', 'user')) as $module) {
     if ($status = module_invoke($module, 'search_status')) {
       $remaining += $status['remaining'];
       $total += $status['total'];
     }
   }
 
-  $count = format_plural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.');
-  $percentage = ((int)min(100, 100 * ($total - $remaining) / max(1, $total))) . '%';
-  $status = '<p><strong>' . t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '</strong></p>';
-  $form['status'] = array('#type' => 'fieldset', '#title' => t('Indexing status'));
-  $form['status']['status'] = array('#markup' => $status);
-  $form['status']['wipe'] = array('#type' => 'submit', '#value' => t('Re-index site'), '#submit' => array('search_admin_reindex_submit'));
+  // Create the count message to display.
+  if (empty($message)) {
+    $message = array(
+      'none' => 'No items indexed, @remaining items remaining.',
+      'partial' => '@done of @total items indexed.',
+      'all' => 'All @done items indexed.',
+    );
+  }
+  if (is_array($message)) {
+    $state = $remaining == 0 ? 'all' : (($remaining == $total) ? 'none' : 'partial');
+    $message = $message[$state];
+  }
+  $count = t($message, array('@remaining' => number_format($remaining), '@total' => number_format($total), '@done' => number_format($total - $remaining)));
+
+  // Return the metrics array.
+  $percentage = $total ? (int) min(100, 100 * ($total - $remaining) / $total) : 100;
+  return array('remaining' => $remaining, 'total' => $total, 'percentage' => $percentage, 'message' => $count);
+}
+
+/**
+ * Menu callback; displays the search module settings page.
+ *
+ * @ingroup forms
+ * @see system_settings_form()
+ * @see search_admin_settings_submit()
+ * @see search_admin_reset_submit()
+ * @see search_admin_index_submit()
+ */
+function search_admin_settings() {
+  $form = array();
+  // Create the index status form from the status metrics.
+  $metrics = _search_get_status_metrics();
+  $form = array();
+  $form['status'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Index Status'),
+    'progress' => array(
+      '#type' => 'markup',
+      '#markup' => theme('progress_bar', $metrics['percentage'], $metrics['message'], 'inactive'),
+    ),
+  );
+  if ($metrics['remaining'] != $metrics['total']) {
+    $form['status']['wipe'] = array(
+      '#type' => 'submit',
+      '#value' => t('Reset index'),
+      '#submit' => array('search_admin_reset_submit'),
+    );
+  }
+  if ($metrics['remaining'] > 0) {
+    $form['status']['build'] = array(
+      '#type' => 'submit',
+      '#value' => t('Index remaining items'),
+      '#submit' => array('search_admin_index_submit'),
+    );
+  }
 
   $items = drupal_map_assoc(array(10, 20, 50, 100, 200, 500));
 
@@ -156,9 +216,17 @@ function search_admin_settings_submit($f
 }
 
 /**
- * Submit callback.
+ * Submit callback to wipe the search index.
  */
-function search_admin_reindex_submit($form, &$form_state) {
+function search_admin_reset_submit($form, &$form_state) {
   // send the user to the confirmation page
-  $form_state['redirect'] = 'admin/config/search/settings/reindex';
-}
\ No newline at end of file
+  $form_state['redirect'] = 'admin/config/search/settings/reset';
+}
+
+/**
+ * Submit callback to index the search index.
+ */
+function search_admin_index_submit($form, &$form_state) {
+  // send the user to the confirmation page
+  $form_state['redirect'] = 'admin/config/search/settings/index';
+}
Index: modules/search/search.batch.inc
===================================================================
RCS file: modules/search/search.batch.inc
diff -N modules/search/search.batch.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/search/search.batch.inc	18 Sep 2009 14:40:21 -0000
@@ -0,0 +1,163 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Batch page callbacks for the search module.
+ */
+
+/**
+ * Menu callback: confirm wiping of the index.
+ */
+function search_batch_index_confirm() {
+  module_load_include('inc', 'search', 'search.admin');
+  // Create the index status form from the status metrics.
+  $metrics = _search_get_status_metrics('@remaining items will be affected.');
+  $form = array();
+  $form['status'] = array(
+    '#type' => 'fieldset',
+    '#description' =>  t('Search will still work while the remaining items are indexed. This action is resource intensive and may slow down your site. You do not need to do this. You typically let cron index your content slowly over time.'),
+    'progress' => array(
+      '#type' => 'markup',
+      '#markup' => theme('progress_bar', $metrics['percentage'], $metrics['message'], 'inactive'),
+    ),
+  );
+  return confirm_form(
+    $form,
+    t('Are you sure you want to build the search index now?'),
+    'admin/config/search/settings',
+    '',
+    t('Confirm, this may take a long time'),
+    t('Cancel')
+  );
+}
+
+/**
+ * Handler for build confirmation.
+ */
+function search_batch_index_confirm_submit(&$form, &$form_state) {
+  if ($form_state['values']['confirm']) {
+    // When done redirect to the settings page.
+    $form_state['redirect'] = 'admin/config/search/settings';
+
+    // Clear the cancel flag.
+    unset($_SESSION['_search_batch_index_cancel']);
+
+    // Start the batch.
+    $batch = array(
+      'title' => t('Building Search Index'),
+      'operations' => array(
+        array('_search_batch_index', array()),
+      ),
+      'finished' => '_search_batch_finished',
+      'file' => drupal_get_path('module', 'search') . '/search.batch.inc',
+      'progress_message' => '',
+    );
+    batch_set($batch);
+  }
+}
+
+/**
+ * Batch callback to index a set of nodes.
+ */
+function _search_batch_index(&$context) {
+  if (empty($context['sandbox'])) {
+    // Initiate multistep processing.
+    $context['sandbox'] = array(
+      'current' => 0,
+      'total' => db_query("SELECT COUNT(DISTINCT n.nid) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0")->fetchField(),
+    );
+  }
+
+  // This query retrieves different nodes with each batch call, 
+  // because _node_index_node() calls search_index() which calls search_touch_node() which clears the reindex flag.
+  // This is the same query and logic used by search_cron().
+  $result = db_query_range("SELECT DISTINCT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", array(), 0, 100);
+  foreach ($result as $node) {
+    // Stop the batch if the user cancelled.
+    if (isset($_SESSION['_search_batch_index_cancel'])) {
+      break;
+    }
+
+    // Acquire a lock so that a single node can be indexed without being interrupted.
+    while (!lock_acquire('search_batch_index')) {
+      lock_wait('search_batch_index');
+    }
+
+    // Index one node.
+    _node_index_node($node);
+    $context['sandbox']['current'] ++;
+
+    // Release the lock.  Use locking per node instead of per batch, so that the cancel is more responsive.
+    lock_release('search_batch_index');
+  }
+
+  // Multistep processing : report progress.
+  if ($context['sandbox']['current'] != $context['sandbox']['total']) {
+    $context['finished'] = $context['sandbox']['current'] / $context['sandbox']['total'];
+  }
+
+  // Update progress message.
+  $context['message'] = t('Completed @current of @total.', array('@current' => $context['sandbox']['current'], '@total' => $context['sandbox']['total'])) . ' '. drupal_render(drupal_get_form('search_batch_index_cancel'));
+}
+
+/**
+ * "Cancel" Form that is appended to the batch index progress message.
+ */
+function search_batch_index_cancel() {
+  $form = array();
+  $form['cancel'] = array(
+    '#type' => 'submit',
+    '#value' => t('Cancel'),
+  );
+  $form['#action'] = url('admin/config/search/settings/index/cancel');
+  return $form;
+}
+
+/**
+ * Batch cancel form submit handler.
+ * Get a lock, update the totals, notify the batch thread to stop, and redirect user to the settings page.
+ */
+function search_batch_index_cancel_submit(&$form, &$form_state) {
+  // Acquire a lock so that a single node can be indexed without being interrupted.
+  while (!lock_acquire('search_batch_index')) {
+    lock_wait('search_batch_index');
+  }
+
+  // Complete the scoring calculations by updating the totals.
+  _search_batch_update_totals();
+
+  // Tell the batch index thread to stop.
+  $_SESSION['_search_batch_index_cancel'] = TRUE;
+
+  // Release the lock.
+  lock_release('search_batch_index');
+
+  // Return to the settings page.
+  drupal_set_message(t('User cancelled indexing.'));
+  drupal_goto('admin/config/search/settings');
+}
+
+/**
+ * Update word IDF (Inverse Document Frequency) counts for all words.
+ */
+function _search_batch_update_totals() {
+  db_query('DELETE FROM {search_total}');
+  db_query("INSERT INTO {search_total} (word, count) SELECT word, LOG10(1+1/GREATEST(1, SUM(score))) FROM {search_index} GROUP BY word");
+}
+
+/**
+ * Batch callback when search building is finished.
+ */
+function _search_batch_finished($success, $results, $operations) {
+  // Complete the scoring calculations by updating the totals.
+  _search_batch_update_totals();
+
+  // Display message to user.
+  if ($success) {
+    drupal_set_message(t('Finished indexing.'));
+  }
+  else {
+    drupal_set_message(t('The search indexing finished with an error.'), 'error');
+  }
+}
Index: modules/search/search.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/search/search.install,v
retrieving revision 1.23
diff -u -u -p -r1.23 search.install
--- modules/search/search.install	10 Sep 2009 06:38:19 -0000	1.23
+++ modules/search/search.install	18 Sep 2009 14:40:21 -0000
@@ -16,6 +16,34 @@ function search_uninstall() {
 }
 
 /**
+ * Implementation of hook_requirements.
+ */
+function search_requirements($phase) {
+  $requirements = array();
+
+  if ($phase == 'runtime') {
+    $t = get_t();
+    module_load_include('inc', 'search', 'search.admin');
+    $metrics = _search_get_status_metrics();
+    $requirements['search_index'] = array(
+      'title' => $t('Search index'),
+      'value' => $metrics['message'],
+    );
+    if ($metrics['remaining'] > 0) {
+      $requirements['search_index']['description'] =
+        $metrics['remaining'] == $metrics['total']
+            ? ($t('No items have been indexed yet. You can either wait for the automatic indexing process or')
+                . ' ' . l(t('index manually'), 'admin/config/search/settings/index') . '.')
+            : ($t('Search results are not complete, because some content has yet to be indexed.')
+                . ' ' . l(t('Index manually'), 'admin/config/search/settings/index'));
+      $requirements['search_index']['severity'] = REQUIREMENT_WARNING;
+    }
+  }
+
+  return $requirements;
+}
+
+/**
  * Implement hook_schema().
  */
 function search_schema() {
Index: modules/search/search.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/search/search.module,v
retrieving revision 1.314
diff -u -u -p -r1.314 search.module
--- modules/search/search.module	5 Sep 2009 10:53:09 -0000	1.314
+++ modules/search/search.module	18 Sep 2009 14:40:21 -0000
@@ -202,14 +202,30 @@ function search_menu() {
     'type' => MENU_NORMAL_ITEM,
     'file' => 'search.admin.inc',
   );
-  $items['admin/config/search/settings/reindex'] = array(
-    'title' => 'Clear index',
+  $items['admin/config/search/settings/reset'] = array(
+    'title' => 'Reset index',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('search_reindex_confirm'),
+    'page arguments' => array('search_reset_confirm'),
     'access arguments' => array('administer search'),
     'type' => MENU_CALLBACK,
     'file' => 'search.admin.inc',
   );
+  $items['admin/config/search/settings/index'] = array(
+    'title' => 'Build index',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_batch_index_confirm'),
+    'access arguments' => array('administer search'),
+    'type' => MENU_CALLBACK,
+    'file' => 'search.batch.inc',
+  );
+  $items['admin/config/search/settings/index/cancel'] = array(
+    'title' => 'Cancel index build',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_batch_index_cancel'),
+    'access arguments' => array('administer search'),
+    'type' => MENU_CALLBACK,
+    'file' => 'search.batch.inc',
+  );
   $items['admin/reports/search'] = array(
     'title' => 'Top search phrases',
     'description' => 'View most popular search phrases.',
@@ -678,7 +694,7 @@ function search_index($sid, $type, $text
       // Unset the link to mark it as processed.
       unset($links[$nid]);
     }
-    else {
+    else if ($nid != $sid) {
       // Insert the existing link and mark the node for reindexing.
       db_insert('search_node_links')
         ->fields(array(
Index: modules/system/system.css
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.css,v
retrieving revision 1.62
diff -u -u -p -r1.62 system.css
--- modules/system/system.css	5 Sep 2009 06:07:58 -0000	1.62
+++ modules/system/system.css	18 Sep 2009 14:40:21 -0000
@@ -436,6 +436,9 @@ html.js .no-js {
   height: 1.5em;
   margin: 0 0.2em;
 }
+.progress.inactive .bar {
+  background: none;
+}
 .progress .filled {
   background: #0072b9;
   height: 1em;
