diff --git modules/node/node.module modules/node/node.module
index 19b2d9f..d68597f 100644
--- modules/node/node.module
+++ modules/node/node.module
@@ -1261,138 +1261,160 @@ function _node_rankings() {
   return $rankings;
 }
 
+/**
+ * Implement hook_search_info().
+ */
+function node_search_info() {
+  return array(
+    'title' => 'Content',
+    'path' => 'node',
+  );
+}
 
 /**
- * Implement hook_search().
+ * Implement hook_search_access().
  */
-function node_search($op = 'search', $keys = NULL) {
-  switch ($op) {
-    case 'name':
-      return t('Content');
+function node_search_access() {
+  return user_access('access content');
+}
 
-    case 'reset':
-      db_update('search_dataset')
-        ->fields(array('reindex' => REQUEST_TIME))
-        ->condition('type', 'node')
-        ->execute();
-      return;
-
-    case 'status':
-      $total = db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')->fetchField();
-      $remaining = db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0")->fetchField();
-      return array('remaining' => $remaining, 'total' => $total);
-
-    case 'admin':
-      $form = array();
-      // Output form for defining rank factor weights.
-      $form['content_ranking'] = array(
-        '#type' => 'fieldset',
-        '#title' => t('Content ranking'),
-      );
-      $form['content_ranking']['#theme'] = 'node_search_admin';
-      $form['content_ranking']['info'] = array(
-        '#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>'
-      );
+/**
+ * Implement hook_search_reset().
+ */
+function node_search_reset() {
+  db_update('search_dataset')
+    ->fields(array('reindex' => REQUEST_TIME))
+    ->condition('type', 'node')
+    ->execute();
+  return;
+}
 
-      // Note: reversed to reflect that higher number = higher ranking.
-      $options = drupal_map_assoc(range(0, 10));
-      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('node_rank_' . $var, 0),
-        );
-      }
-      return $form;
-
-    case 'search':
-      // Build matching conditions
-      list($join1, $where1) = _db_rewrite_sql();
-      $arguments1 = array();
-      $conditions1 = 'n.status = 1';
-
-      if ($type = search_query_extract($keys, 'type')) {
-        $types = array();
-        foreach (explode(',', $type) as $t) {
-          $types[] = "n.type = '%s'";
-          $arguments1[] = $t;
-        }
-        $conditions1 .= ' AND (' . implode(' OR ', $types) . ')';
-        $keys = search_query_insert($keys, 'type');
-      }
+/**
+ * Implement hook_search_status().
+ */
+function node_search_status() {
+  $total = db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')->fetchField();
+  $remaining = db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0")->fetchField();
+  return array('remaining' => $remaining, 'total' => $total);
+}
 
-      if ($term = search_query_extract($keys, 'term')) {
-        $terms = array();
-        foreach (explode(',', $term) as $c) {
-          $terms[] = "tn.tid = %d";
-          $arguments1[] = $c;
-        }
-        $conditions1 .= ' AND (' . implode(' OR ', $terms) . ')';
-        $join1 .= ' INNER JOIN {taxonomy_term_node} tn ON n.vid = tn.vid';
-        $keys = search_query_insert($keys, 'term');
-      }
+/**
+ * Implement hook_search_admin().
+ */
+function node_search_admin() {
+  $form = array();
+  // Output form for defining rank factor weights.
+  $form['content_ranking'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Content ranking'),
+  );
+  $form['content_ranking']['#theme'] = 'node_search_admin';
+  $form['content_ranking']['info'] = array(
+    '#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>'
+  );
+  
+  // Note: reversed to reflect that higher number = higher ranking.
+  $options = drupal_map_assoc(range(0, 10));
+  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('node_rank_' . $var, 0),
+    );
+  }
+  return $form;
+}
 
-      if ($languages = search_query_extract($keys, 'language')) {
-        $terms = array();
-        foreach (explode(',', $languages) as $l) {
-          $terms[] = "n.language = '%s'";
-          $arguments1[] = $l;
-        }
-        $conditions1 .= ' AND (' . implode(' OR ', $terms) . ')';
-        $keys = search_query_insert($keys, 'language');
-      }
+/**
+ * Implement hook_search_execute().
+ */
+function node_search_execute($keys = NULL) {
+  // Build matching conditions
+  list($join1, $where1) = _db_rewrite_sql();
+  $arguments1 = array();
+  $conditions1 = 'n.status = 1';
+
+  if ($type = search_query_extract($keys, 'type')) {
+    $types = array();
+    foreach (explode(',', $type) as $t) {
+      $types[] = "n.type = '%s'";
+      $arguments1[] = $t;
+    }
+    $conditions1 .= ' AND (' . implode(' OR ', $types) . ')';
+    $keys = search_query_insert($keys, 'type');
+  }
 
-      // Get the ranking expressions.
-      $rankings = _node_rankings();
+  if ($term = search_query_extract($keys, 'term')) {
+    $terms = array();
+    foreach (explode(',', $term) as $c) {
+      $terms[] = "tn.tid = %d";
+      $arguments1[] = $c;
+    }
+    $conditions1 .= ' AND (' . implode(' OR ', $terms) . ')';
+    $join1 .= ' INNER JOIN {taxonomy_term_node} tn ON n.vid = tn.vid';
+    $keys = search_query_insert($keys, 'term');
+  }
 
-      // When all search factors are disabled (ie they have a weight of zero),
-      // The default score is based only on keyword relevance.
-      if ($rankings['total'] == 0) {
-        $total = 1;
-        $arguments2 = array();
-        $join2 = '';
-        $select2 = 'SUM(i.relevance) AS calculated_score';
-      }
-      else {
-        $total = $rankings['total'];
-        $arguments2 = $rankings['arguments'];
-        $join2 = implode(' ', $rankings['join']);
-        $select2 = 'SUM(' . implode(' + ', $rankings['score']) . ') AS calculated_score';
-      }
+  if ($languages = search_query_extract($keys, 'language')) {
+    $terms = array();
+    foreach (explode(',', $languages) as $l) {
+      $terms[] = "n.language = '%s'";
+      $arguments1[] = $l;
+    }
+    $conditions1 .= ' AND (' . implode(' OR ', $terms) . ')';
+    $keys = search_query_insert($keys, 'language');
+  }
 
-      // Do search.
-      $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid ' . $join1, $conditions1 . (empty($where1) ? '' : ' AND ' . $where1), $arguments1, $select2, $join2, $arguments2);
-
-      // Load results.
-      $results = array();
-      foreach ($find as $item) {
-        // Render the node.
-        $node = node_load($item->sid);
-        $node = node_build_content($node, 'search_result');
-        $node->rendered = drupal_render($node->content);
-
-        // Fetch comments for snippet.
-        $node->rendered .= module_invoke('comment', 'node_update_index', $node);
-        // Fetch terms for snippet.
-        $node->rendered .= module_invoke('taxonomy', 'node_update_index', $node);
-
-        $extra = module_invoke_all('node_search_result', $node);
-
-        $results[] = array(
-          'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
-          'type' => check_plain(node_type_get_name($node)),
-          'title' => $node->title,
-          'user' => theme('username', $node),
-          'date' => $node->changed,
-          'node' => $node,
-          'extra' => $extra,
-          'score' => $total ? ($item->calculated_score / $total) : 0,
-          'snippet' => search_excerpt($keys, $node->rendered),
-        );
-      }
-      return $results;
+  // 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.
+  if ($rankings['total'] == 0) {
+    $total = 1;
+    $arguments2 = array();
+    $join2 = '';
+    $select2 = 'SUM(i.relevance) AS calculated_score';
+  }
+  else {
+    $total = $rankings['total'];
+    $arguments2 = $rankings['arguments'];
+    $join2 = implode(' ', $rankings['join']);
+    $select2 = 'SUM(' . implode(' + ', $rankings['score']) . ') AS calculated_score';
+  }
+
+  // Do search.
+  $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid ' . $join1, $conditions1 . (empty($where1) ? '' : ' AND ' . $where1), $arguments1, $select2, $join2, $arguments2);
+
+  // Load results.
+  $results = array();
+  foreach ($find as $item) {
+    // Render the node.
+    $node = node_load($item->sid);
+    $node = node_build_content($node, 'search_result');
+    $node->rendered = drupal_render($node->content);
+
+    // Fetch comments for snippet.
+    $node->rendered .= module_invoke('comment', 'node_update_index', $node);
+    // Fetch terms for snippet.
+    $node->rendered .= module_invoke('taxonomy', 'node_update_index', $node);
+
+    $extra = module_invoke_all('node_search_result', $node);
+
+    $results[] = array(
+      'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
+      'type' => check_plain(node_type_get_name($node)),
+      'title' => $node->title,
+      'user' => theme('username', $node),
+      'date' => $node->changed,
+      'node' => $node,
+      'extra' => $extra,
+      'score' => $total ? ($item->calculated_score / $total) : 0,
+      'snippet' => search_excerpt($keys, $node->rendered),
+    );
   }
+  return $results;
 }
 
 /**
diff --git modules/search/search.admin.inc modules/search/search.admin.inc
index addbb1e..e4a75ca 100644
--- modules/search/search.admin.inc
+++ modules/search/search.admin.inc
@@ -27,6 +27,27 @@ function search_reindex_confirm_submit(&$form, &$form_state) {
 }
 
 /**
+ * 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'))
+    ->condition('s.status', 1)
+    ->condition('s.type', 'module')
+    ->condition('s.name', array_keys($search_info), 'IN')
+    ->orderBy('s.name')
+    ->execute();
+  $names = array();
+  foreach ($modules as $item) {
+    $info = unserialize($item->info);
+    $names[$item->name] = $info['name'];
+  }
+  return $names;
+}
+
+/**
  * Menu callback; displays the search module settings page.
  *
  * @ingroup forms
@@ -38,12 +59,13 @@ function search_admin_settings() {
   // Collect some stats
   $remaining = 0;
   $total = 0;
-  foreach (module_implements('search') as $module) {
-    $function = $module . '_search';
-    $status = $function('status');
-    $remaining += $status['remaining'];
-    $total += $status['total'];
+  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>';
@@ -88,10 +110,24 @@ function search_admin_settings() {
     '#description' => t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.')
   );
 
+  $form['search_active_modules'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Active search modules'),
+    '#default_value' => array('node', 'user'),
+    '#options' => _search_get_module_names(),
+    '#description' => t('Determine which search modules are active from the available modules.')
+  );
+
   $form['#submit'][] = 'search_admin_settings_submit';
 
   // Per module settings
-  $form = array_merge($form, module_invoke_all('search', 'admin'));
+  foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
+    $added_form = module_invoke($module, 'search_admin');
+    if (is_array($added_form)) {
+      $form = array_merge($form, $added_form);
+    }
+  }
+
   return system_settings_form($form, TRUE);
 }
 
@@ -105,6 +141,18 @@ function search_admin_settings_submit($form, &$form_state) {
     drupal_set_message(t('The index will be rebuilt.'));
     search_reindex();
   }
+  $current_modules = variable_get('search_active_modules', array('node', 'user'));
+  // Check whether we are resetting the values.
+  if ($form_state['clicked_button']['#value'] == t('Reset to defaults')) {
+    $new_modules = array('node', 'user');
+  }
+  else {
+    $new_modules = array_filter($form_state['values']['search_active_modules']);
+  }
+  if (array_diff($current_modules, $new_modules)) {
+    drupal_set_message(t('The active search modules have been changed.'));
+    variable_set('menu_rebuild_needed', TRUE);
+  }
 }
 
 /**
diff --git modules/search/search.api.php modules/search/search.api.php
index a47ca76..2a3049a 100644
--- modules/search/search.api.php
+++ modules/search/search.api.php
@@ -16,7 +16,7 @@
  *
  * This hook allows a module to perform searches on content it defines
  * (custom node types, users, or comments, for example) when a site search
- * is performed.
+ * is performed. 
  *
  * Note that you can use form API to extend the search. You will need to use
  * hook_form_alter() to add any additional required form elements. You can
@@ -29,173 +29,203 @@
  * capabilities. To do this, node module also implements hook_update_index()
  * which is used to create and maintain the index.
  *
+ * @return
+ *   Array with the optional keys 'title' for the tab title and 'path' for
+ *   the path component after 'search/'.  Both will default to the module
+ *   name.
+ *
+ * @ingroup search
+ */
+function hook_search_info() {
+  return array(
+    'title' => 'Content',
+    'path' => 'node',
+  );
+}
+
+/**
+ * Define access to a custom search routine.
+ *
+ * This hook allows a module to deny access to a user to a search tab.
+ *
+ * @ingroup search
+ */
+function hook_search_access() {
+  return user_access('access content');
+}
+
+/**
+ * The search index is going to be rebuilt.
+ *
+ * Modules which use  hook_update_index() should update their indexing
+ * bookkeeping so that it starts from scratch the next time
+ * hook_update_index() is called.
+ *
+ * @ingroup search
+ */
+function hook_search_reset() {
+  db_query("UPDATE {search_dataset} SET reindex = %d WHERE type = 'node'", REQUEST_TIME);
+  return;
+}
+
+/**
+ * Report the stutus of indexing.
+ *
+ * @return
+ *  An associative array with the key-value pairs:
+ *  - 'remaining': The number of items left to index.
+ *  - 'total': The total number of items to index.
+ *
+ * @ingroup search
+ */
+function hook_search_status() {
+  $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
+  $remaining = db_result(db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0"));
+  return array('remaining' => $remaining, 'total' => $total);
+}
+
+/**
+ * Add elements to the search administration form.
+ *
+ * @return
+ *   The form array for the Search settings page at admin/config/search/settings.
+ *
+ * @ingroup search
+ */
+function hook_search_admin() {
+  $form = array();
+  // Output form for defining rank factor weights.
+  $form['content_ranking'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Content ranking'),
+  );
+  $form['content_ranking']['#theme'] = 'node_search_admin';
+  $form['content_ranking']['info'] = array(
+    '#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>'
+  );
+
+  // Note: reversed to reflect that higher number = higher ranking.
+  $options = drupal_map_assoc(range(0, 10));
+  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('node_rank_' . $var, 0),
+    );
+  }
+  return $form;
+}
+
+/**
+ * Execute a search for a set of key words.
+ *
  * We call do_search() with the keys, the module name, and extra SQL fragments
  * to use when searching. See hook_update_index() for more information.
  *
- * @param $op
- *   A string defining which operation to perform:
- *   - 'admin': The hook should return a form array containing any fieldsets the
- *     module wants to add to the Search settings page at 
- *     admin/config/search/settings.
- *   - 'name': The hook should return a translated name defining the type of
- *     items that are searched for with this module ('content', 'users', ...).
- *   - 'reset': The search index is going to be rebuilt. Modules which use
- *     hook_update_index() should update their indexing bookkeeping so that it
- *     starts from scratch the next time hook_update_index() is called.
- *   - 'search': The hook should perform a search using the keywords in $keys.
- *   - 'status': If the module implements hook_update_index(), it should return
- *     an array containing the following keys:
- *     - remaining: The amount of items that still need to be indexed.
- *     - total: The total amount of items (both indexed and unindexed).
  * @param $keys
  *   The search keywords as entered by the user.
+ *
  * @return
- *   This varies depending on the operation.
- *   - 'admin': The form array for the Search settings page at
- *     admin/config/search/settings.
- *   - 'name': The translated string of 'Content'.
- *   - 'reset': None.
- *   - 'search': An array of search results. To use the default search result
- *     display, each item should have the following keys':
- *     - 'link': Required. The URL of the found item.
- *     - 'type': The type of item.
- *     - 'title': Required. The name of the item.
- *     - 'user': The author of the item.
- *     - 'date': A timestamp when the item was last modified.
- *     - 'extra': An array of optional extra information items.
- *     - 'snippet': An excerpt or preview to show with the result (can be
+ *   An array of search results. To use the default search result
+ *   display, each item should have the following keys':
+ *   - 'link': Required. The URL of the found item.
+ *   - 'type': The type of item.
+ *   - 'title': Required. The name of the item.
+ *   - 'user': The author of the item.
+ *   - 'date': A timestamp when the item was last modified.
+ *   - 'extra': An array of optional extra information items.
+ *   - 'snippet': An excerpt or preview to show with the result (can be
  *     generated with search_excerpt()).
- *   - 'status': An associative array with the key-value pairs:
- *     - 'remaining': The number of items left to index.
- *     - 'total': The total number of items to index.
  *
  * @ingroup search
  */
-function hook_search($op = 'search', $keys = NULL) {
-  switch ($op) {
-    case 'name':
-      return t('Content');
-
-    case 'reset':
-      db_query("UPDATE {search_dataset} SET reindex = %d WHERE type = 'node'", REQUEST_TIME);
-      return;
+function hook_search_execute($keys = NULL) {
+  // Build matching conditions
+  list($join1, $where1) = _db_rewrite_sql();
+  $arguments1 = array();
+  $conditions1 = 'n.status = 1';
 
-    case 'status':
-      $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
-      $remaining = db_result(db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0"));
-      return array('remaining' => $remaining, 'total' => $total);
-
-    case 'admin':
-      $form = array();
-      // Output form for defining rank factor weights.
-      $form['content_ranking'] = array(
-        '#type' => 'fieldset',
-        '#title' => t('Content ranking'),
-      );
-      $form['content_ranking']['#theme'] = 'node_search_admin';
-      $form['content_ranking']['info'] = array(
-        '#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>'
-      );
-
-      // Note: reversed to reflect that higher number = higher ranking.
-      $options = drupal_map_assoc(range(0, 10));
-      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('node_rank_' . $var, 0),
-        );
-      }
-      return $form;
-
-    case 'search':
-      // Build matching conditions
-      list($join1, $where1) = _db_rewrite_sql();
-      $arguments1 = array();
-      $conditions1 = 'n.status = 1';
-
-      if ($type = search_query_extract($keys, 'type')) {
-        $types = array();
-        foreach (explode(',', $type) as $t) {
-          $types[] = "n.type = '%s'";
-          $arguments1[] = $t;
-        }
-        $conditions1 .= ' AND (' . implode(' OR ', $types) . ')';
-        $keys = search_query_insert($keys, 'type');
-      }
+  if ($type = search_query_extract($keys, 'type')) {
+    $types = array();
+    foreach (explode(',', $type) as $t) {
+      $types[] = "n.type = '%s'";
+      $arguments1[] = $t;
+    }
+    $conditions1 .= ' AND (' . implode(' OR ', $types) . ')';
+    $keys = search_query_insert($keys, 'type');
+  }
 
-      if ($category = search_query_extract($keys, 'category')) {
-        $categories = array();
-        foreach (explode(',', $category) as $c) {
-          $categories[] = "tn.tid = %d";
-          $arguments1[] = $c;
-        }
-        $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')';
-        $join1 .= ' INNER JOIN {taxonomy_term_node} tn ON n.vid = tn.vid';
-        $keys = search_query_insert($keys, 'category');
-      }
+  if ($category = search_query_extract($keys, 'category')) {
+    $categories = array();
+    foreach (explode(',', $category) as $c) {
+      $categories[] = "tn.tid = %d";
+      $arguments1[] = $c;
+    }
+    $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')';
+    $join1 .= ' INNER JOIN {taxonomy_term_node} tn ON n.vid = tn.vid';
+    $keys = search_query_insert($keys, 'category');
+  }
 
-      if ($languages = search_query_extract($keys, 'language')) {
-        $categories = array();
-        foreach (explode(',', $languages) as $l) {
-          $categories[] = "n.language = '%s'";
-          $arguments1[] = $l;
-        }
-        $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')';
-        $keys = search_query_insert($keys, 'language');
-      }
+  if ($languages = search_query_extract($keys, 'language')) {
+    $categories = array();
+    foreach (explode(',', $languages) as $l) {
+      $categories[] = "n.language = '%s'";
+      $arguments1[] = $l;
+    }
+    $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')';
+    $keys = search_query_insert($keys, 'language');
+  }
 
-      // Get the ranking expressions.
-      $rankings = _node_rankings();
+  // 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.
-      if ($rankings['total'] == 0) {
-        $total = 1;
-        $arguments2 = array();
-        $join2 = '';
-        $select2 = 'i.relevance AS score';
-      }
-      else {
-        $total = $rankings['total'];
-        $arguments2 = $rankings['arguments'];
-        $join2 = implode(' ', $rankings['join']);
-        $select2 = '(' . implode(' + ', $rankings['score']) . ') AS score';
-      }
+  // When all search factors are disabled (ie they have a weight of zero),
+  // The default score is based only on keyword relevance.
+  if ($rankings['total'] == 0) {
+    $total = 1;
+    $arguments2 = array();
+    $join2 = '';
+    $select2 = 'i.relevance AS score';
+  }
+  else {
+    $total = $rankings['total'];
+    $arguments2 = $rankings['arguments'];
+    $join2 = implode(' ', $rankings['join']);
+    $select2 = '(' . implode(' + ', $rankings['score']) . ') AS score';
+  }
 
-      // Do search.
-      $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid ' . $join1, $conditions1 . (empty($where1) ? '' : ' AND ' . $where1), $arguments1, $select2, $join2, $arguments2);
+  // Do search.
+  $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid ' . $join1, $conditions1 . (empty($where1) ? '' : ' AND ' . $where1), $arguments1, $select2, $join2, $arguments2);
 
-      // Load results.
-      $results = array();
-      foreach ($find as $item) {
-        // Build the node body.
-        $node = node_load($item->sid);
-        $node = node_build_content($node, 'search_result');
-        $node->body = drupal_render($node->content);
+  // Load results.
+  $results = array();
+  foreach ($find as $item) {
+    // Build the node body.
+    $node = node_load($item->sid);
+    $node = node_build_content($node, 'search_result');
+    $node->body = drupal_render($node->content);
 
-        // Fetch comments for snippet.
-        $node->body .= module_invoke('comment', 'node', $node, 'update_index');
-        // Fetch terms for snippet.
-        $node->body .= module_invoke('taxonomy', 'node', $node, 'update_index');
+    // Fetch comments for snippet.
+    $node->body .= module_invoke('comment', 'node', $node, 'update_index');
+    // Fetch terms for snippet.
+    $node->body .= module_invoke('taxonomy', 'node', $node, 'update_index');
 
-        $extra = module_invoke_all('node_search_result', $node);
+    $extra = module_invoke_all('node_search_result', $node);
 
-        $results[] = array(
-          'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
-          'type' => check_plain(node_type_get_name($node)),
-          'title' => $node->title,
-          'user' => theme('username', $node),
-          'date' => $node->changed,
-          'node' => $node,
-          'extra' => $extra,
-          'score' => $total ? ($item->score / $total) : 0,
-          'snippet' => search_excerpt($keys, $node->body),
-        );
-      }
-      return $results;
+    $results[] = array(
+      'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
+      'type' => check_plain(node_type_get_name($node)),
+      'title' => $node->title,
+      'user' => theme('username', $node),
+      'date' => $node->changed,
+      'node' => $node,
+      'extra' => $extra,
+      'score' => $total ? ($item->score / $total) : 0,
+      'snippet' => search_excerpt($keys, $node->body),
+    );
   }
+  return $results;
 }
 
 /**
@@ -214,6 +244,8 @@ function hook_search($op = 'search', $keys = NULL) {
  *   extracted from between two HTML tags. Will not contain any HTML entities.
  * @return
  *   The text after processing.
+ *
+ * @ingroup search
  */
 function hook_search_preprocess($text) {
   // Do processing on $text
diff --git modules/search/search.module modules/search/search.module
index d8b09cb..0983b98 100644
--- modules/search/search.module
+++ modules/search/search.module
@@ -218,25 +218,45 @@ function search_menu() {
     'file path' => drupal_get_path('module', 'dblog'),
     'file' => 'dblog.admin.inc',
   );
-
-  foreach (module_implements('search') as $module) {
-    $items['search/' . $module . '/%menu_tail'] = array(
-      'title callback' => 'module_invoke',
-      'title arguments' => array($module, 'search', 'name', TRUE),
-      'page callback' => 'search_view',
-      'page arguments' => array($module),
-      'access callback' => '_search_menu',
-      'access arguments' => array($module),
-      'type' => MENU_LOCAL_TASK,
-      'parent' => 'search',
-      'file' => 'search.pages.inc',
-    );
+  drupal_static_reset('search_get_info');
+  $search_hooks = search_get_info();
+  foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
+    if (isset($search_hooks[$module])) {
+      $items['search/' . $search_hooks[$module]['path'] . '/%menu_tail'] = array(
+        'title' => $search_hooks[$module]['title'],
+        'page callback' => 'search_view',
+        'page arguments' => array($module),
+        'access callback' => '_search_menu_access',
+        'access arguments' => array($module),
+        'type' => MENU_LOCAL_TASK,
+        'file' => 'search.pages.inc',
+      );
+    }
   }
   return $items;
 }
 
-function _search_menu($name) {
-  return user_access('search content') && module_invoke($name, 'search', 'name');
+/**
+ * Get information about all available search hooks.
+ */
+function search_get_info() {
+  $search_hooks = &drupal_static(__FUNCTION__);
+
+  if (!isset($search_hooks)) {
+    foreach (module_implements('search_info') as $module) {
+      $search_hooks[$module] = call_user_func($module . '_search_info');
+      // Use module name as the default.
+      $search_hooks[$module] += array('title' => $module, 'path' => $module);
+    }
+  }
+  return $search_hooks;
+}
+
+/**
+ * Access callback for search tabs.
+ */
+function _search_menu_access($name) {
+  return user_access('search content') && (!function_exists($name . '_search_access') || module_invoke($name, 'search_access'));
 }
 
 /**
@@ -288,8 +308,10 @@ function search_cron() {
   // to date.
   register_shutdown_function('search_update_totals');
 
-  // Update word index
-  module_invoke_all('update_index');
+  foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
+    // Update word index
+    module_invoke($module, 'update_index');
+  }
 }
 
 /**
@@ -642,6 +664,10 @@ function search_touch_node($nid) {
   db_query("UPDATE {search_dataset} SET reindex = %d WHERE sid = %d AND type = 'node'", REQUEST_TIME, $nid);
 }
 
+function search_touch_data($type, $sid) {
+  db_query("UPDATE {search_dataset} SET reindex = :time WHERE sid = :sid AND type = :type", array(':time' => REQUEST_TIME, ':sid' => $sid, ':type' => $type));
+}
+
 /**
  * Implement hook_node_update_index().
  */
@@ -1193,8 +1219,8 @@ function template_preprocess_search_block_form(&$variables) {
 function search_data($keys = NULL, $type = 'node') {
 
   if (isset($keys)) {
-    if (module_hook($type, 'search')) {
-      $results = module_invoke($type, 'search', 'search', $keys);
+    if (module_hook($type, 'search_execute')) {
+      $results = module_invoke($type, 'search_execute', $keys);
       if (isset($results) && is_array($results) && count($results)) {
         if (module_hook($type, 'search_page')) {
           return module_invoke($type, 'search_page', $results);
diff --git modules/search/search.test modules/search/search.test
index 120bdee..23f6f9d 100644
--- modules/search/search.test
+++ modules/search/search.test
@@ -364,7 +364,7 @@ class SearchRankingTestCase extends DrupalWebTestCase {
       }
 
       // Do the search and assert the results.
-      $set = node_search('search', 'rocks');
+      $set = node_search_execute('rocks');
       $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
     }
   }
diff --git modules/user/user.module modules/user/user.module
index 5f92858..e530942 100644
--- modules/user/user.module
+++ modules/user/user.module
@@ -795,39 +795,46 @@ function user_file_delete($file) {
 }
 
 /**
- * Implement hook_search().
+ * Implement hook_search_info().
  */
-function user_search($op = 'search', $keys = NULL, $skip_access_check = FALSE) {
-  switch ($op) {
-    case 'name':
-      if ($skip_access_check || user_access('access user profiles')) {
-        return t('Users');
-      }
-    case 'search':
-      if (user_access('access user profiles')) {
-        $find = array();
-        // Replace wildcards with MySQL/PostgreSQL wildcards.
-        $keys = preg_replace('!\*+!', '%', $keys);
-        $query = db_select('users')->extend('PagerDefault');
-        $query->fields('users', array('name', 'uid', 'mail'));
-        if (user_access('administer users')) {
-          // Administrators can also search in the otherwise private email field.
-          $query->condition(db_or()->
-            where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"))->
-            where('LOWER(mail) LIKE LOWER(:mail)', array(':mail' => "%$keys%")));
-        }
-        else {
-          $query->where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"));
-        }
-        $result = $query
-          ->limit(15)
-          ->execute();
-        foreach ($result as $account) {
-          $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
-        }
-        return $find;
-      }
+function user_search_info() {
+  return array(
+    'title' => 'Users',
+  );
+}
+
+/**
+ * Implement hook_search_access().
+ */
+function user_search_access() {
+  return user_access('access user profiles');
+}
+
+/**
+ * Implement hook_search_execute().
+ */
+function user_search_execute($keys = NULL) {
+  $find = array();
+  // Replace wildcards with MySQL/PostgreSQL wildcards.
+  $keys = preg_replace('!\*+!', '%', $keys);
+  $query = db_select('users')->extend('PagerDefault');
+  $query->fields('users', array('name', 'uid', 'mail'));
+  if (user_access('administer users')) {
+    // Administrators can also search in the otherwise private email field.
+    $query->condition(db_or()->
+      where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"))->
+      where('LOWER(mail) LIKE LOWER(:mail)', array(':mail' => "%$keys%")));
+  }
+  else {
+    $query->where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"));
+  }
+  $result = $query
+    ->limit(15)
+    ->execute();
+  foreach ($result as $account) {
+    $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
   }
+  return $find;
 }
 
 /**
