diff --git a/core/modules/comment/comment.api.php b/core/modules/comment/comment.api.php
index 008506a9a2e1ce5377dc6f33b046b2d68e286f54..078401e670621fcb1c793843ee297bd1435338a6 100644
--- a/core/modules/comment/comment.api.php
+++ b/core/modules/comment/comment.api.php
@@ -34,7 +34,7 @@ function hook_comment_presave(Drupal\comment\Comment $comment) {
  */
 function hook_comment_insert(Drupal\comment\Comment $comment) {
   // Reindex the node when comments are added.
-  search_touch_node($comment->nid->target_id);
+  node_reindex_node_search($comment->nid->target_id);
 }
 
 /**
@@ -45,7 +45,7 @@ function hook_comment_insert(Drupal\comment\Comment $comment) {
  */
 function hook_comment_update(Drupal\comment\Comment $comment) {
   // Reindex the node when comments are updated.
-  search_touch_node($comment->nid->target_id);
+  node_reindex_node_search($comment->nid->target_id);
 }
 
 /**
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php
new file mode 100644
index 0000000000000000000000000000000000000000..bf987e6eb2292b4dfc41856ee76f1f36242047df
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php
@@ -0,0 +1,554 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Plugin\Search\NodeSearch.
+ */
+
+namespace Drupal\node\Plugin\Search;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Config\Config;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Query\SelectExtender;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TypedData\AccessibleInterface;
+use Drupal\Core\Database\Query\Condition;
+use Drupal\search\Annotation\SearchPlugin;
+use Drupal\search\Plugin\SearchPluginBase;
+use Drupal\search\Plugin\SearchIndexingInterface;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Handles searching for node entities using the Search module index.
+ *
+ * @SearchPlugin(
+ *   id = "node_search",
+ *   title = @Translation("Content"),
+ *   path = "node"
+ * )
+ */
+class NodeSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface {
+
+  /**
+   * A database connection object.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * An entity manager object.
+   *
+   * @var \Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
+   * A module manager object.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * A config object for 'search.settings'.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $searchSettings;
+
+  /**
+   * The Drupal state object used to set 'node.cron_last'.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $state;
+
+  /**
+   * The Drupal account to use for checking for access to advanced search.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The list of options and info for advanced search filters.
+   *
+   * Each entry in the array has the option as the key and and for its value, an
+   * array that determines how the value is matched in the database query. The
+   * possible keys in that array are:
+   * - column: (required) Name of the database column to match against.
+   * - join: (optional) Information on a table to join. By default the data is
+   *   matched against the {node_field_data} table.
+   * - operator: (optional) OR or AND, defaults to OR.
+   *
+   * @var array
+   */
+  protected $advanced = array(
+    'type' => array('column' => 'n.type'),
+    'langcode' => array('column' => 'n.langcode'),
+    'author' => array('column' => 'n.uid'),
+    'term' => array('column' => 'ti.tid', 'join' => array('table' => 'taxonomy_index', 'alias' => 'ti', 'condition' => 'n.nid = ti.nid')),
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  static public function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('database'),
+      $container->get('plugin.manager.entity'),
+      $container->get('module_handler'),
+      $container->get('config.factory')->get('search.settings'),
+      $container->get('keyvalue')->get('state'),
+      $container->get('request')->attributes->get('_account')
+    );
+  }
+
+  /**
+   * Constructs a \Drupal\node\Plugin\Search\NodeSearch object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param array $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Database\Connection $database
+   *   A database connection object.
+   * @param \Drupal\Core\Entity\EntityManager $entity_manager
+   *   An entity manager object.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   A module manager object.
+   * @param \Drupal\Core\Config\Config $search_settings
+   *   A config object for 'search.settings'.
+   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
+   *   The Drupal state object used to set 'node.cron_last'.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The $account object to use for checking for access to advanced search.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, Connection $database, EntityManager $entity_manager, ModuleHandlerInterface $module_handler, Config $search_settings, KeyValueStoreInterface $state, AccountInterface $account = NULL) {
+    $this->database = $database;
+    $this->entityManager = $entity_manager;
+    $this->moduleHandler = $module_handler;
+    $this->searchSettings = $search_settings;
+    $this->state = $state;
+    $this->account = $account;
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($operation = 'view', AccountInterface $account = NULL) {
+    return !empty($account) && $account->hasPermission('access content');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $results = array();
+    if (!$this->isSearchExecutable()) {
+      return $results;
+    }
+    $keys = $this->keywords;
+
+    // Build matching conditions.
+    $query = $this->database
+      ->select('search_index', 'i', array('target' => 'slave'))
+      ->extend('Drupal\search\SearchQuery')
+      ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
+    $query->join('node_field_data', 'n', 'n.nid = i.sid');
+    $query->condition('n.status', 1)
+      ->addTag('node_access')
+      ->searchExpression($keys, $this->getPluginId());
+
+    // Handle advanced search filters in the f query string.
+    // $_GET['f'] is an array that looks like this in the URL:
+    // ?f[]=type:page&f[]=term:27&f[]=term:13&f[]=langcode:en
+    // So $parameters['f'] looks like:
+    // array('type:page', 'term:27', 'term:13', 'langcode:en');
+    // We need to parse this out into query conditions.
+    $parameters = $this->getParameters();
+    if (!empty($parameters['f']) && is_array($parameters['f'])) {
+      $filters = array();
+      // Match any query value that is an expected option and a value
+      // separated by ':' like 'term:27'.
+      $pattern = '/^(' . implode('|', array_keys($this->advanced)) . '):([^ ]*)/i';
+      foreach ($parameters['f'] as $item) {
+        if (preg_match($pattern, $item, $m)) {
+          // Use the matched value as the array key to eliminate duplicates.
+          $filters[$m[1]][$m[2]] = $m[2];
+        }
+      }
+      // Now turn these into query conditions. This assumes that everything in
+      // $filters is a known type of advanced search.
+      foreach ($filters as $option => $matched) {
+        $info = $this->advanced[$option];
+        // Insert additional conditions. By default, all use the OR operator.
+        $operator = empty($info['operator']) ? 'OR' : $info['operator'];
+        $where = new Condition($operator);
+        foreach ($matched as $value) {
+          $where->condition($info['column'], $value);
+        }
+        $query->condition($where);
+        if (!empty($info['join'])) {
+          $query->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']);
+        }
+      }
+    }
+    // Only continue if the first pass query matches.
+    if (!$query->executeFirstPass()) {
+      return array();
+    }
+
+    // Add the ranking expressions.
+    $this->addNodeRankings($query);
+
+    // Load results.
+    $find = $query
+      // Add the language code of the indexed item to the result of the query,
+      // since the node will be rendered using the respective language.
+      ->fields('i', array('langcode'))
+      ->limit(10)
+      ->execute();
+
+    $node_storage = $this->entityManager->getStorageController('node');
+    $node_render = $this->entityManager->getRenderController('node');
+
+    foreach ($find as $item) {
+      // Render the node.
+      $node = $node_storage->load($item->sid);
+      $build = $node_render->view($node, 'search_result', $item->langcode);
+      unset($build['#theme']);
+      $node->rendered = drupal_render($build);
+
+      // Fetch comment count for snippet.
+      $node->rendered .= ' ' . $this->moduleHandler->invoke('comment', 'node_update_index', array($node, $item->langcode));
+
+      $extra = $this->moduleHandler->invokeAll('node_search_result', array($node, $item->langcode));
+
+      $language = language_load($item->langcode);
+      $uri = $node->uri();
+      $username = array(
+        '#theme' => 'username',
+        '#account' => $node->getAuthor(),
+      );
+      $results[] = array(
+        'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))),
+        'type' => check_plain($this->entityManager->getStorageController('node_type')->load($node->bundle())->label()),
+        'title' => $node->label($item->langcode),
+        'user' => drupal_render($username),
+        'date' => $node->getChangedTime(),
+        'node' => $node,
+        'extra' => $extra,
+        'score' => $item->calculated_score,
+        'snippet' => search_excerpt($keys, $node->rendered, $item->langcode),
+        'langcode' => $node->language()->id,
+      );
+    }
+    return $results;
+  }
+
+  /**
+   * Gathers the rankings from the the hook_ranking() implementations.
+   *
+   * @param $query
+   *   A query object that has been extended with the Search DB Extender.
+   */
+  protected function addNodeRankings(SelectExtender $query) {
+    if ($ranking = $this->moduleHandler->invokeAll('ranking')) {
+      $tables = &$query->getTables();
+      foreach ($ranking as $rank => $values) {
+        // @todo - move rank out of drupal variables.
+        if ($node_rank = variable_get('node_rank_' . $rank, 0)) {
+          // If the table defined in the ranking isn't already joined, then add it.
+          if (isset($values['join']) && !isset($tables[$values['join']['alias']])) {
+            $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']);
+          }
+          $arguments = isset($values['arguments']) ? $values['arguments'] : array();
+          $query->addScore($values['score'], $arguments, $node_rank);
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateIndex() {
+    $limit = (int) $this->searchSettings->get('index.cron_limit');
+
+    $result = $this->database->queryRange("SELECT DISTINCT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = :type AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit, array(':type' => $this->getPluginId()), array('target' => 'slave'));
+    $nids = $result->fetchCol();
+    if (!$nids) {
+      return;
+    }
+
+    // The indexing throttle should be aware of the number of language variants
+    // of a node.
+    $counter = 0;
+    $node_storage = $this->entityManager->getStorageController('node');
+    foreach ($node_storage->loadMultiple($nids) as $node) {
+      // Determine when the maximum number of indexable items is reached.
+      $counter += count($node->getTranslationLanguages());
+      if ($counter > $limit) {
+        break;
+      }
+      $this->indexNode($node);
+    }
+  }
+
+  /**
+   * Indexes a single node.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $node
+   *   The node to index.
+   */
+  protected function indexNode(EntityInterface $node) {
+    // Save the changed time of the most recent indexed node, for the search
+    // results half-life calculation.
+    $this->state->set('node.cron_last', $node->getChangedTime());
+
+    $languages = $node->getTranslationLanguages();
+    $node_render = $this->entityManager->getRenderController('node');
+
+    foreach ($languages as $language) {
+      // Render the node.
+      $build = $node_render->view($node, 'search_index', $language->id);
+
+      unset($build['#theme']);
+      $node->rendered = drupal_render($build);
+
+      $text = '<h1>' . check_plain($node->label($language->id)) . '</h1>' . $node->rendered;
+
+      // Fetch extra data normally not visible.
+      $extra = $this->moduleHandler->invokeAll('node_update_index', array($node, $language->id));
+      foreach ($extra as $t) {
+        $text .= $t;
+      }
+
+      // Update index.
+      search_index($node->id(), $this->getPluginId(), $text, $language->id);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetIndex() {
+    $this->database->update('search_dataset')
+      ->fields(array('reindex' => REQUEST_TIME))
+      ->condition('type', $this->getPluginId())
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexStatus() {
+    $total = $this->database->query('SELECT COUNT(*) FROM {node}')->fetchField();
+    $remaining = $this->database->query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = :type AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0", array(':type' => $this->getPluginId()))->fetchField();
+    return array('remaining' => $remaining, 'total' => $total);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function searchFormAlter(array &$form, array &$form_state) {
+    // Add keyword boxes.
+    $form['advanced'] = array(
+      '#type' => 'details',
+      '#title' => t('Advanced search'),
+      '#collapsed' => TRUE,
+      '#attributes' => array('class' => array('search-advanced')),
+      '#access' => $this->account && $this->account->hasPermission('use advanced search'),
+    );
+    $form['advanced']['keywords-fieldset'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Keywords'),
+      '#collapsible' => FALSE,
+    );
+    $form['advanced']['keywords'] = array(
+      '#prefix' => '<div class="criterion">',
+      '#suffix' => '</div>',
+    );
+    $form['advanced']['keywords-fieldset']['keywords']['or'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Containing any of the words'),
+      '#size' => 30,
+      '#maxlength' => 255,
+    );
+    $form['advanced']['keywords-fieldset']['keywords']['phrase'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Containing the phrase'),
+      '#size' => 30,
+      '#maxlength' => 255,
+    );
+    $form['advanced']['keywords-fieldset']['keywords']['negative'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Containing none of the words'),
+      '#size' => 30,
+      '#maxlength' => 255,
+    );
+
+    // Add node types.
+    $node_types = $this->entityManager->getStorageController('node_type')->loadMultiple();
+    $types = array_map('check_plain', node_type_get_names());
+    $form['advanced']['types-fieldset'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Types'),
+      '#collapsible' => FALSE,
+    );
+    $form['advanced']['types-fieldset']['type'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Only of the type(s)'),
+      '#prefix' => '<div class="criterion">',
+      '#suffix' => '</div>',
+      '#options' => $types,
+    );
+    $form['advanced']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Advanced search'),
+      '#prefix' => '<div class="action">',
+      '#suffix' => '</div>',
+      '#weight' => 100,
+    );
+
+    // Add languages.
+    $language_options = array();
+    foreach (language_list(Language::STATE_ALL) as $langcode => $language) {
+      // Make locked languages appear special in the list.
+      $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name;
+    }
+    if (count($language_options) > 1) {
+      $form['advanced']['lang-fieldset'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Languages'),
+        '#collapsible' => FALSE,
+        '#collapsed' => FALSE,
+      );
+      $form['advanced']['lang-fieldset']['language'] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Languages'),
+        '#prefix' => '<div class="criterion">',
+        '#suffix' => '</div>',
+        '#options' => $language_options,
+      );
+    }
+
+    // Add a submit handler.
+    $form['#submit'][] = array($this, 'searchFormSubmit');
+  }
+
+  /**
+   * Handles submission of elements added in searchFormAlter().
+   *
+   * @param array $form
+   *   Nested array of form elements that comprise the form.
+   * @param array $form_state
+   *   A keyed array containing the current state of the form.
+   */
+  public function searchFormSubmit(array &$form, array &$form_state) {
+    // Initialize using any existing basic search keywords.
+    $keys = $form_state['values']['processed_keys'];
+    $filters = array();
+
+    // Collect extra restrictions.
+    if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) {
+      // Retrieve selected types - Form API sets the value of unselected
+      // checkboxes to 0.
+      foreach ($form_state['values']['type'] as $type) {
+        if ($type) {
+          $filters[] = 'type:' . $type;
+        }
+      }
+    }
+
+    if (isset($form_state['values']['term']) && is_array($form_state['values']['term'])) {
+      foreach ($form_state['values']['term'] as $term) {
+        $filters[] = 'term:' . $term;
+      }
+    }
+    if (isset($form_state['values']['language']) && is_array($form_state['values']['language'])) {
+      foreach ($form_state['values']['language'] as $language) {
+        if ($language) {
+          $filters[] = 'language:' . $language;
+        }
+      }
+    }
+    if ($form_state['values']['or'] != '') {
+      if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['or'], $matches)) {
+        $keys .= ' ' . implode(' OR ', $matches[1]);
+      }
+    }
+    if ($form_state['values']['negative'] != '') {
+      if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['negative'], $matches)) {
+        $keys .= ' -' . implode(' -', $matches[1]);
+      }
+    }
+    if ($form_state['values']['phrase'] != '') {
+      $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"';
+    }
+    if (!empty($keys)) {
+      form_set_value($form['basic']['processed_keys'], trim($keys), $form_state);
+    }
+    $path = $form_state['action'] . '/' . $keys;
+    $options = array();
+    if ($filters) {
+      $options['query'] = array('f' => $filters);
+    }
+
+    $form_state['redirect'] = array($path, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addToAdminForm(array &$form, array &$form_state) {
+    // Output form for defining rank factor weights.
+    $form['content_ranking'] = array(
+      '#type' => 'details',
+      '#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 ($this->moduleHandler->invokeAll('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),
+      );
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitAdminForm(array &$form, array &$form_state) {
+    foreach ($this->moduleHandler->invokeAll('ranking') as $var => $values) {
+      if (isset($form_state['values']['node_rank_' . $var])) {
+        // @todo Fix when https://drupal.org/node/1831632 is in.
+        variable_set('node_rank_' . $var, $form_state['values']['node_rank_' . $var]);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index 275e232e55ecc670aabf814e40f7935575cbfdf3..59d27b5f3cfcb4cab8134247be8685ca1e6bb07b 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -95,14 +95,14 @@
  * - Validating a node during editing form submit (calling
  *   node_form_validate()):
  *   - hook_node_validate() (all)
- * - Searching (calling node_search_execute()):
+ * - Searching (using the 'node_search' plugin):
  *   - hook_ranking() (all)
  *   - Query is executed to find matching nodes
  *   - Resulting node is loaded (see Loading section above)
  *   - Resulting node is prepared for viewing (see Viewing a single node above)
- *   - comment_node_update_index() is called.
+ *   - comment_node_update_index() is called (this adds "N comments" text)
  *   - hook_node_search_result() (all)
- * - Search indexing (calling node_update_index()):
+ * - Search indexing (calling updateIndex() on the 'node_search' plugin):
  *   - Node is loaded (see Loading section above)
  *   - Node is prepared for viewing (see Viewing a single node above)
  *   - hook_node_update_index() (all)
@@ -612,8 +612,8 @@ function hook_node_prepare_form(\Drupal\node\NodeInterface $node, $form_display,
 /**
  * Act on a node being displayed as a search result.
  *
- * This hook is invoked from node_search_execute(), after node_load() and
- * node_view() have been called.
+ * This hook is invoked from the node search plugin during search execution,
+ * after loading and rendering the node.
  *
  * @param \Drupal\Core\Entity\EntityInterface $node
  *   The node being displayed in a search result.
@@ -686,8 +686,8 @@ function hook_node_update(\Drupal\Core\Entity\EntityInterface $node) {
 /**
  * Act on a node being indexed for searching.
  *
- * This hook is invoked during search indexing, after node_load(), and after the
- * result of node_view() is added as $node->rendered to the node object.
+ * This hook is invoked during search indexing, after loading, and after the
+ * result of rendering is added as $node->rendered to the node object.
  *
  * @param \Drupal\Core\Entity\EntityInterface $node
  *   The node being indexed.
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 10b6d778168845024d40d4e78ac0f56d665d390f..6a863176a6c8173fd50ff1435a533deecdc99d3c 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -9,12 +9,9 @@
  */
 
 use Drupal\Core\Language\Language;
-use Drupal\node\NodeInterface;
 use Symfony\Component\HttpFoundation\Response;
-
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Query\AlterableInterface;
-use Drupal\Core\Database\Query\SelectExtender;
 use Drupal\Core\Database\Query\SelectInterface;
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\node\NodeTypeInterface;
@@ -773,161 +770,6 @@ function node_permission() {
   return $perms;
 }
 
-/**
- * Gathers the rankings from the the hook_ranking() implementations.
- *
- * @param $query
- *   A query object that has been extended with the Search DB Extender.
- */
-function _node_rankings(SelectExtender $query) {
-  if ($ranking = Drupal::moduleHandler()->invokeAll('ranking')) {
-    $tables = &$query->getTables();
-    foreach ($ranking as $rank => $values) {
-      if ($node_rank = variable_get('node_rank_' . $rank, 0)) {
-        // If the table defined in the ranking isn't already joined, then add it.
-        if (isset($values['join']) && !isset($tables[$values['join']['alias']])) {
-          $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']);
-        }
-        $arguments = isset($values['arguments']) ? $values['arguments'] : array();
-        $query->addScore($values['score'], $arguments, $node_rank);
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_search_info().
- */
-function node_search_info() {
-  return array(
-    'title' => 'Content',
-    'path' => 'node',
-  );
-}
-
-/**
- * Implements hook_search_access().
- */
-function node_search_access() {
-  return user_access('access content');
-}
-
-/**
- * Implements hook_search_reset().
- */
-function node_search_reset() {
-  db_update('search_dataset')
-    ->fields(array('reindex' => REQUEST_TIME))
-    ->condition('type', 'node')
-    ->execute();
-}
-
-/**
- * Implements hook_search_status().
- */
-function node_search_status() {
-  $total = db_query('SELECT COUNT(*) FROM {node}')->fetchField();
-  $remaining = db_query("SELECT COUNT(*) 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();
-  return array('remaining' => $remaining, 'total' => $total);
-}
-
-/**
- * Implements hook_search_admin().
- */
-function node_search_admin() {
-  // Output form for defining rank factor weights.
-  $form['content_ranking'] = array(
-    '#type' => 'details',
-    '#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 (Drupal::moduleHandler()->invokeAll('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;
-}
-
-/**
- * Implements hook_search_execute().
- */
-function node_search_execute($keys = NULL, $conditions = NULL) {
-  // Build matching conditions
-  $query = db_select('search_index', 'i', array('target' => 'slave'))
-    ->extend('Drupal\search\SearchQuery')
-    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
-  $query->join('node_field_data', 'n', 'n.nid = i.sid');
-  $query
-    ->condition('n.status', 1)
-    ->addTag('node_access')
-    ->searchExpression($keys, 'node');
-
-  // Insert special keywords.
-  $query->setOption('type', 'n.type');
-  $query->setOption('langcode', 'n.langcode');
-  if ($query->setOption('term', 'ti.tid')) {
-    $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid');
-  }
-  // Only continue if the first pass query matches.
-  if (!$query->executeFirstPass()) {
-    return array();
-  }
-
-  // Add the ranking expressions.
-  _node_rankings($query);
-
-  // Load results.
-  $find = $query
-    // Add the language code of the indexed item to the result of the query,
-    // since the node will be rendered using the respective language.
-    ->fields('i', array('langcode'))
-    ->limit(10)
-    ->execute();
-  $results = array();
-  foreach ($find as $item) {
-    // Render the node.
-    $node = node_load($item->sid);
-    $build = node_view($node, 'search_result', $item->langcode);
-    unset($build['#theme']);
-    $node->rendered = drupal_render($build);
-
-    // Fetch comments for snippet.
-    $node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node, $item->langcode);
-
-    $extra = Drupal::moduleHandler()->invokeAll('node_search_result', array($node, $item->langcode));
-
-    $language = language_load($item->langcode);
-    $uri = $node->uri();
-    $username = array(
-      '#theme' => 'username',
-      '#account' => $node->getAuthor(),
-    );
-    $results[] = array(
-      'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))),
-      'type' => check_plain(node_get_type_label($node)),
-      'title' => $node->label($item->langcode),
-      'user' => drupal_render($username),
-      'date' => $node->getChangedTime(),
-      'node' => $node,
-      'extra' => $extra,
-      'score' => $item->calculated_score,
-      'snippet' => search_excerpt($keys, $node->rendered, $item->langcode),
-      'langcode' => $node->language()->id,
-    );
-  }
-  return $results;
-}
-
 /**
  * Implements hook_ranking().
  */
@@ -1666,199 +1508,6 @@ function node_page_view(EntityInterface $node) {
   return node_show($node);
 }
 
-/**
- * Implements hook_update_index().
- */
-function node_update_index() {
-  $limit = (int) Drupal::config('search.settings')->get('index.cron_limit');
-
-  $result = db_query_range("SELECT 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", 0, $limit, array(), array('target' => 'slave'));
-  $nids = $result->fetchCol();
-  if (!$nids) {
-    return;
-  }
-
-  // The indexing throttle should be aware of the number of language variants
-  // of a node.
-  $counter = 0;
-  foreach (node_load_multiple($nids) as $node) {
-    // Determine when the maximum number of indexable items is reached.
-    $counter += count($node->getTranslationLanguages());
-    if ($counter > $limit) {
-      break;
-    }
-    _node_index_node($node);
-  }
-}
-
-/**
- * Indexes a single node.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   The node to index.
- */
-function _node_index_node(EntityInterface $node) {
-
-  // Save the changed time of the most recent indexed node, for the search
-  // results half-life calculation.
-  Drupal::state()->set('node.cron_last', $node->getChangedTime());
-
-  $languages = $node->getTranslationLanguages();
-
-  foreach ($languages as $language) {
-    // Render the node.
-    $build = node_view($node, 'search_index', $language->id);
-
-    unset($build['#theme']);
-    $node->rendered = drupal_render($build);
-
-    $text = '<h1>' . check_plain($node->label($language->id)) . '</h1>' . $node->rendered;
-
-    // Fetch extra data normally not visible.
-    $extra = Drupal::moduleHandler()->invokeAll('node_update_index', array($node, $language->id));
-    foreach ($extra as $t) {
-      $text .= $t;
-    }
-
-    // Update index.
-    search_index($node->id(), 'node', $text, $language->id);
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- *
- * @see node_search_validate()
- */
-function node_form_search_form_alter(&$form, $form_state) {
-  if (isset($form['module']) && $form['module']['#value'] == 'node' && user_access('use advanced search')) {
-    // Keyword boxes:
-    $form['advanced'] = array(
-      '#type' => 'details',
-      '#title' => t('Advanced search'),
-      '#collapsed' => TRUE,
-      '#attributes' => array('class' => array('search-advanced')),
-    );
-    $form['advanced']['keywords-fieldset'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Keywords'),
-      '#collapsible' => FALSE,
-    );
-    $form['advanced']['keywords'] = array(
-      '#prefix' => '<div class="criterion">',
-      '#suffix' => '</div>',
-    );
-    $form['advanced']['keywords-fieldset']['keywords']['or'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Containing any of the words'),
-      '#size' => 30,
-      '#maxlength' => 255,
-    );
-    $form['advanced']['keywords-fieldset']['keywords']['phrase'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Containing the phrase'),
-      '#size' => 30,
-      '#maxlength' => 255,
-    );
-    $form['advanced']['keywords-fieldset']['keywords']['negative'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Containing none of the words'),
-      '#size' => 30,
-      '#maxlength' => 255,
-    );
-
-    // Node types:
-    $types = array_map('check_plain', node_type_get_names());
-    $form['advanced']['types-fieldset'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Types'),
-      '#collapsible' => FALSE,
-    );
-    $form['advanced']['types-fieldset']['type'] = array(
-      '#type' => 'checkboxes',
-      '#title' => t('Only of the type(s)'),
-      '#prefix' => '<div class="criterion">',
-      '#suffix' => '</div>',
-      '#options' => $types,
-    );
-    $form['advanced']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Advanced search'),
-      '#prefix' => '<div class="action">',
-      '#suffix' => '</div>',
-      '#weight' => 100,
-    );
-
-    // Languages:
-    $language_options = array();
-    foreach (language_list(Language::STATE_ALL) as $langcode => $language) {
-      // Make locked languages appear special in the list.
-      $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name;
-    }
-    if (count($language_options) > 1) {
-      $form['advanced']['lang-fieldset'] = array(
-        '#type' => 'fieldset',
-        '#title' => t('Languages'),
-        '#collapsible' => FALSE,
-        '#collapsed' => FALSE,
-      );
-      $form['advanced']['lang-fieldset']['language'] = array(
-        '#type' => 'checkboxes',
-        '#title' => t('Languages'),
-        '#prefix' => '<div class="criterion">',
-        '#suffix' => '</div>',
-        '#options' => $language_options,
-      );
-    }
-
-    $form['#validate'][] = 'node_search_validate';
-  }
-}
-
-/**
- * Form validation handler for node_form_search_form_alter().
- */
-function node_search_validate($form, &$form_state) {
-  // Initialize using any existing basic search keywords.
-  $keys = $form_state['values']['processed_keys'];
-
-  // Insert extra restrictions into the search keywords string.
-  if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) {
-    // Retrieve selected types - Form API sets the value of unselected
-    // checkboxes to 0.
-    $form_state['values']['type'] = array_filter($form_state['values']['type']);
-    if (count($form_state['values']['type'])) {
-      $keys = search_expression_insert($keys, 'type', implode(',', array_keys($form_state['values']['type'])));
-    }
-  }
-
-  if (isset($form_state['values']['term']) && is_array($form_state['values']['term']) && count($form_state['values']['term'])) {
-    $keys = search_expression_insert($keys, 'term', implode(',', $form_state['values']['term']));
-  }
-  if (isset($form_state['values']['language']) && is_array($form_state['values']['language'])) {
-    $languages = array_filter($form_state['values']['language']);
-    if (count($languages)) {
-      $keys = search_expression_insert($keys, 'language', implode(',', $languages));
-    }
-  }
-  if ($form_state['values']['or'] != '') {
-    if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['or'], $matches)) {
-      $keys .= ' ' . implode(' OR ', $matches[1]);
-    }
-  }
-  if ($form_state['values']['negative'] != '') {
-    if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['negative'], $matches)) {
-      $keys .= ' -' . implode(' -', $matches[1]);
-    }
-  }
-  if ($form_state['values']['phrase'] != '') {
-    $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"';
-  }
-  if (!empty($keys)) {
-    form_set_value($form['basic']['processed_keys'], trim($keys), $form_state);
-  }
-}
-
 /**
  * Implements hook_form_FORM_ID_alter().
  *
@@ -2558,3 +2207,65 @@ function node_system_info_alter(&$info, $file, $type) {
     $info['hidden'] = !module_exists('translation') && Drupal::config('system.module.disabled')->get('translation') === NULL;
   }
 }
+
+/**
+ * Marks a node to be re-indexed by the node_search plugin.
+ *
+ * @param int $nid
+ *   The node ID.
+ */
+function node_reindex_node_search($nid) {
+  if (Drupal::moduleHandler()->moduleExists('search')) {
+    // Reindex node context indexed by the node module search plugin.
+    search_mark_for_reindex('node_search', $nid);
+  }
+}
+
+/**
+ * Implements hook_node_update().
+ */
+function node_node_update(EntityInterface $node) {
+  // Reindex the node when it is updated. The node is automatically indexed
+  // when it is added, simply by being added to the node table.
+  node_reindex_node_search($node->id());
+}
+
+/**
+ * Implements hook_comment_insert().
+ */
+function node_comment_insert($comment) {
+  // Reindex the node when comments are added.
+  node_reindex_node_search($comment->nid->target_id);
+}
+
+/**
+ * Implements hook_comment_update().
+ */
+function node_comment_update($comment) {
+  // Reindex the node when comments are changed.
+  node_reindex_node_search($comment->nid->target_id);
+}
+
+/**
+ * Implements hook_comment_delete().
+ */
+function node_comment_delete($comment) {
+  // Reindex the node when comments are deleted.
+  node_reindex_node_search($comment->nid->target_id);
+}
+
+/**
+ * Implements hook_comment_publish().
+ */
+function node_comment_publish($comment) {
+  // Reindex the node when comments are published.
+  node_reindex_node_search($comment->nid->target_id);
+}
+
+/**
+ * Implements hook_comment_unpublish().
+ */
+function node_comment_unpublish($comment) {
+  // Reindex the node when comments are unpublished.
+  node_reindex_node_search($comment->nid->target_id);
+}
diff --git a/core/modules/search/config/schema/search.schema.yml b/core/modules/search/config/schema/search.schema.yml
index 7d847b934f1da2602169e856ad7eb282fb976989..9fdd63f829317bdd2df024b986430165424724b2 100644
--- a/core/modules/search/config/schema/search.schema.yml
+++ b/core/modules/search/config/schema/search.schema.yml
@@ -4,18 +4,18 @@ search.settings:
   type: mapping
   label: 'Search settings'
   mapping:
-    active_modules:
+    active_plugins:
       type: sequence
-      label: 'Active search modules'
+      label: 'Active search plugins'
       sequence:
         - type: string
-          label: 'Module'
+          label: 'Plugin'
     and_or_limit:
       type: integer
       label: 'AND/OR combination limit'
-    default_module:
+    default_plugin:
       type: string
-      label: 'Default search module'
+      label: 'Default search plugin'
     index:
       type: mapping
       label: 'Indexing settings'
diff --git a/core/modules/search/config/search.settings.yml b/core/modules/search/config/search.settings.yml
index a1a88b470ec33e8cf25a044f635370691f4b6534..1d0124f35e1af3ba209148133c4c831375d7aa53 100644
--- a/core/modules/search/config/search.settings.yml
+++ b/core/modules/search/config/search.settings.yml
@@ -1,8 +1,8 @@
-active_modules:
-  - node
-  - user
+active_plugins:
+  node_search: node_search
+  user_search: user_search
 and_or_limit: '7'
-default_module: node
+default_plugin: node_search
 index:
   cron_limit: '100'
   overlap_cjk: '1'
diff --git a/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..720f2b29c613b352dbca5e2afbc63c8dcf6c12c2
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Annotation\SearchPlugin.
+ */
+
+namespace Drupal\search\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a SearchPlugin type annotation object.
+ *
+ * SearchPlugin classes define search types for the core Search module. Each
+ * active search type is displayed in a tab on the Search page, and each has a
+ * path suffix after "search/".
+ *
+ * @see SearchPluginBase
+ *
+ * @Annotation
+ */
+class SearchPlugin extends Plugin {
+
+  /**
+   * A unique identifier for the search plugin.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The path fragment to be added to search/ for the search page.
+   *
+   * @var string
+   */
+  public $path;
+
+  /**
+   * The title for the search page tab.
+   *
+   * @todo This will potentially be translated twice or cached with the wrong
+   *   translation until the search tabs are converted to local task plugins.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $title;
+}
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
index 8d0985abaf05a9121b46e9a0ff33529840c62d8e..eaf2c6012f1a51f6545bdb14e075fb5c8cebf9f3 100644
--- a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
+++ b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
@@ -79,7 +79,7 @@ public function submitForm(array &$form, array &$form_state) {
     }
 
     $form_id = $form['form_id']['#value'];
-    $info = search_get_default_module_info();
+    $info = search_get_default_plugin_info();
     if ($info) {
       $form_state['redirect'] = 'search/' . $info['path'] . '/' . trim($form_state['values'][$form_id]);
     }
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php b/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php
index 5f7d1da23bf08da566c4f3a7f684f361b1390915..d07e346c2b1ad5dd1647c0f545dc11a8c6f1a239 100644
--- a/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php
+++ b/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php
@@ -6,18 +6,34 @@
 
 namespace Drupal\search\Form;
 
-use Drupal\system\SystemConfigFormBase;
 use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Config\Context\ContextInterface;
 use Drupal\Core\Extension\ModuleHandler;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
-use Drupal\Component\Utility\NestedArray;
+use Drupal\search\SearchPluginManager;
+use Drupal\system\SystemConfigFormBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Configure search settings for this site.
  */
 class SearchSettingsForm extends SystemConfigFormBase {
+
+  /**
+   * A configuration object with the current search settings.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $searchSettings;
+
+  /**
+   * A search plugin manager object.
+   *
+   * @var \Drupal\search\SearchPluginManager
+   */
+  protected $searchPluginManager;
+
   /**
    * The module handler.
    *
@@ -39,13 +55,17 @@ class SearchSettingsForm extends SystemConfigFormBase {
    *   The configuration factory object that manages search settings.
    * @param \Drupal\Core\Config\Context\ContextInterface $context
    *   The context interface
-   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
+   * @param \Drupal\search\SearchPluginManager $manager
+   *   The manager for search plugins.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler
    * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
    *   The state key/value store interface, gives access to state based config settings.
    */
-  public function __construct(ConfigFactory $config_factory, ContextInterface $context, ModuleHandler $module_handler, KeyValueStoreInterface $state) {
+  public function __construct(ConfigFactory $config_factory, ContextInterface $context, SearchPluginManager $manager, ModuleHandlerInterface $module_handler, KeyValueStoreInterface $state) {
     parent::__construct($config_factory, $context);
+    $this->searchSettings = $config_factory->get('search.settings');
+    $this->searchPluginManager = $manager;
     $this->moduleHandler = $module_handler;
     $this->state = $state;
   }
@@ -57,6 +77,7 @@ public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
       $container->get('config.context.free'),
+      $container->get('plugin.manager.search'),
       $container->get('module_handler'),
       $container->get('state')
     );
@@ -70,38 +91,39 @@ public function getFormID() {
   }
 
   /**
-   * Returns names of available search modules.
+   * Returns names of available search plugins.
    *
    * @return array
-   *   An array of the names of enabled modules that call hook_search_info
-   *   sorted into alphabetical order.
+   *   An array of the names of available search plugins.
    */
-  protected function getModuleOptions() {
-    $search_info = search_get_info(TRUE);
-    $names = system_get_module_info('name');
-    $names = array_intersect_key($names, $search_info);
-    asort($names, SORT_STRING);
-    return $names;
+  protected function getOptions() {
+    $options = array();
+    foreach ($this->searchPluginManager->getDefinitions() as $plugin_id => $search_info) {
+      $options[$plugin_id] = $search_info['title'] . ' (' . $plugin_id . ')';
+    }
+    asort($options, SORT_STRING);
+    return $options;
   }
 
   /**
    * {@inheritdoc}
    */
   public function buildForm(array $form, array &$form_state) {
-    $config = $this->configFactory->get('search.settings');
-    // Collect some stats
+
+    // Collect some stats.
     $remaining = 0;
     $total = 0;
-    foreach ($config->get('active_modules') as $module) {
-      if ($status = $this->moduleHandler->invoke($module, 'search_status')) {
+
+    foreach ($this->searchPluginManager->getActiveIndexingPlugins() as $plugin) {
+      if ($status = $plugin->indexStatus()) {
         $remaining += $status['remaining'];
         $total += $status['total'];
       }
     }
-
+    $active_plugins = $this->searchPluginManager->getActivePlugins();
     $this->moduleHandler->loadAllIncludes('admin.inc');
     $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))) . '%';
+    $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' => 'details',
@@ -124,7 +146,7 @@ public function buildForm(array $form, array &$form_state) {
     $form['indexing_throttle']['cron_limit'] = array(
       '#type' => 'select',
       '#title' => t('Number of items to index per cron run'),
-      '#default_value' => $config->get('index.cron_limit'),
+      '#default_value' => $this->searchSettings->get('index.cron_limit'),
       '#options' => $items,
       '#description' => t('The maximum number of items indexed in each pass of a <a href="@cron">cron maintenance task</a>. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => $this->url('system_status')))
     );
@@ -139,7 +161,7 @@ public function buildForm(array $form, array &$form_state) {
     $form['indexing_settings']['minimum_word_size'] = array(
       '#type' => 'number',
       '#title' => t('Minimum word length to index'),
-      '#default_value' => $config->get('index.minimum_word_size'),
+      '#default_value' => $this->searchSettings->get('index.minimum_word_size'),
       '#min' => 1,
       '#max' => 1000,
       '#description' => t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).')
@@ -147,40 +169,37 @@ public function buildForm(array $form, array &$form_state) {
     $form['indexing_settings']['overlap_cjk'] = array(
       '#type' => 'checkbox',
       '#title' => t('Simple CJK handling'),
-      '#default_value' => $config->get('index.overlap_cjk'),
+      '#default_value' => $this->searchSettings->get('index.overlap_cjk'),
       '#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['active'] = array(
       '#type' => 'details',
-      '#title' => t('Active search modules')
+      '#title' => t('Active search plugins')
     );
-    $module_options = $this->getModuleOptions();
-    $form['active']['active_modules'] = array(
+    $options = $this->getOptions();
+    $form['active']['active_plugins'] = array(
       '#type' => 'checkboxes',
-      '#title' => t('Active modules'),
+      '#title' => t('Active plugins'),
       '#title_display' => 'invisible',
-      '#default_value' => $config->get('active_modules'),
-      '#options' => $module_options,
-      '#description' => t('Choose which search modules are active from the available modules.')
+      '#default_value' => $this->searchSettings->get('active_plugins'),
+      '#options' => $options,
+      '#description' => t('Choose which search plugins are active from the available plugins.')
     );
-    $form['active']['default_module'] = array(
-      '#title' => t('Default search module'),
+    $form['active']['default_plugin'] = array(
+      '#title' => t('Default search plugin'),
       '#type' => 'radios',
-      '#default_value' => $config->get('default_module'),
-      '#options' => $module_options,
-      '#description' => t('Choose which search module is the default.')
+      '#default_value' => $this->searchSettings->get('default_plugin'),
+      '#options' => $options,
+      '#description' => t('Choose which search plugin is the default.')
     );
 
-    // Per module settings
-    foreach ($config->get('active_modules') as $module) {
-      $added_form = $this->moduleHandler->invoke($module, 'search_admin');
-      if (is_array($added_form)) {
-        $form = NestedArray::mergeDeep($form, $added_form);
-      }
+    // Per plugin settings.
+    foreach ($active_plugins as $plugin) {
+      $plugin->addToAdminForm($form, $form_state);
     }
     // Set #submit so we are sure it's invoked even if one of
-    // the active search modules added its own #submit.
+    // the active search plugins added its own #submit.
     $form['#submit'][] = array($this, 'submitForm');
 
     return parent::buildForm($form, $form_state);
@@ -194,10 +213,10 @@ public function validateForm(array &$form, array &$form_state) {
 
     // Check whether we selected a valid default.
     if ($form_state['triggering_element']['#value'] != t('Reset to defaults')) {
-      $new_modules = array_filter($form_state['values']['active_modules']);
-      $default = $form_state['values']['default_module'];
-      if (!in_array($default, $new_modules, TRUE)) {
-        form_set_error('default_module', t('Your default search module is not selected as an active module.'));
+      $new_plugins = array_filter($form_state['values']['active_plugins']);
+      $default = $form_state['values']['default_plugin'];
+      if (!in_array($default, $new_plugins, TRUE)) {
+        form_set_error('default_plugin', t('Your default search plugin is not selected as an active plugin.'));
       }
     }
   }
@@ -207,31 +226,35 @@ public function validateForm(array &$form, array &$form_state) {
    */
   public function submitForm(array &$form, array &$form_state) {
     parent::submitForm($form, $form_state);
-    $config = $this->configFactory->get('search.settings');
 
     // If these settings change, the index needs to be rebuilt.
-    if (($config->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($config->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) {
-      $config->set('index.minimum_word_size', $form_state['values']['minimum_word_size']);
-      $config->set('index.overlap_cjk', $form_state['values']['overlap_cjk']);
+    if (($this->searchSettings->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($this->searchSettings->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) {
+      $this->searchSettings->set('index.minimum_word_size', $form_state['values']['minimum_word_size']);
+      $this->searchSettings->set('index.overlap_cjk', $form_state['values']['overlap_cjk']);
       drupal_set_message(t('The index will be rebuilt.'));
       search_reindex();
     }
-    $config->set('index.cron_limit', $form_state['values']['cron_limit']);
-    $config->set('default_module', $form_state['values']['default_module']);
+    $this->searchSettings->set('index.cron_limit', $form_state['values']['cron_limit']);
+    $this->searchSettings->set('default_plugin', $form_state['values']['default_plugin']);
+
+    // Handle per-plugin submission logic.
+    foreach ($this->searchPluginManager->getActivePlugins() as $plugin) {
+      $plugin->submitAdminForm($form, $form_state);
+    }
 
     // Check whether we are resetting the values.
     if ($form_state['triggering_element']['#value'] == t('Reset to defaults')) {
-      $new_modules = array('node', 'user');
+      $new_plugins = array('node_search', 'user_search');
     }
     else {
-      $new_modules = array_filter($form_state['values']['active_modules']);
+      $new_plugins = array_filter($form_state['values']['active_plugins']);
     }
-    if ($config->get('active_modules') != $new_modules) {
-      $config->set('active_modules', $new_modules);
-      drupal_set_message(t('The active search modules have been changed.'));
+    if ($this->searchSettings->get('active_plugins') != $new_plugins) {
+      $this->searchSettings->set('active_plugins', $new_plugins);
+      drupal_set_message(t('The active search plugins have been changed.'));
       $this->state->set('menu_rebuild_needed', TRUE);
     }
-    $config->save();
+    $this->searchSettings->save();
   }
 
   /**
@@ -242,4 +265,5 @@ public function searchAdminReindexSubmit(array $form, array &$form_state) {
     // send the user to the confirmation page
     $form_state['redirect'] = 'admin/config/search/settings/reindex';
   }
+
 }
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..a2ca97378adc04707ab432d8f3550a60e11e609b
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Plugin\SearchIndexingInterface.
+ */
+
+namespace Drupal\search\Plugin;
+
+/**
+ * Defines an optional interface for SearchPlugin objects using the index.
+ *
+ * Plugins implementing this interface will have these methods invoked during
+ * search_cron() and via the search module administration form. Plugins not
+ * implementing this interface are assumed to use alternate mechanisms for
+ * indexing the data used to provide search results.
+ */
+interface SearchIndexingInterface {
+
+  /**
+   * Updates the search index for this plugin.
+   *
+   * This method is called every cron run if the plugin has been set as
+   * an active search module on the Search settings page
+   * (admin/config/search/settings). It allows your module to add items to the
+   * built-in search index using search_index(), or to add them to your module's
+   * own indexing mechanism.
+   *
+   * When implementing this method, your module should index content items that
+   * were modified or added since the last run. PHP has a time limit
+   * for cron, though, so it is advisable to limit how many items you index
+   * per run using config('search.settings')->get('index.cron_limit'). Also,
+   * since the cron run could time out and abort in the middle of your run, you
+   * should update any needed internal bookkeeping on when items have last
+   * been indexed as you go rather than waiting to the end of indexing.
+   */
+  public function updateIndex();
+
+  /**
+   * Takes action when the search index is going to be rebuilt.
+   *
+   * Modules that use updateIndex() should update their indexing bookkeeping so
+   * that it starts from scratch the next time updateIndex() is called.
+   */
+  public function resetIndex();
+
+  /**
+   * Reports the status of indexing.
+   *
+   * The core search module only invokes this method on active module plugins.
+   * Implementing modules do not need to check whether they are active when
+   * calculating their return values.
+   *
+   * @return array
+   *   An associative array with the key-value pairs:
+   *   - remaining: The number of items left to index.
+   *   - total: The total number of items to index.
+   */
+  public function indexStatus();
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..b1429b0e164259eded3b6430938c047499567907
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Plugin\SearchInterface.
+ */
+
+namespace Drupal\search\Plugin;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Defines a common interface for all SearchPlugin objects.
+ */
+interface SearchInterface extends PluginInspectionInterface {
+
+  /**
+   * Sets the keywords, parameters, and attributes to be used by execute().
+   *
+   * @param string $keywords
+   *   The keywords to use in a search.
+   * @param array $parameters
+   *   Array of parameters as am associative array. This is expected to
+   *   be the query string from the current request.
+   * @param array $attributes
+   *   Array of attributes, usually from the current request object. The search
+   *   plugin may use the '_account' attribute if present to personalize the
+   *   search, or use attributes from the current route variables.
+   *
+   * @return \Drupal\search\Plugin\SearchInterface
+   *   A search plugin object for chaining.
+   */
+  public function setSearch($keywords, array $parameters, array $attributes);
+
+  /**
+   * Returns the currently set keywords of the plugin instance.
+   *
+   * @return string
+   *   The keywords.
+   */
+  public function getKeywords();
+
+  /**
+   * Returns the current parameters set using setSearch().
+   *
+   * @return array
+   *   The parameters.
+   */
+  public function getParameters();
+
+  /**
+   * Returns the currently set attributes (from the request).
+   *
+   * @return array
+   *   The attributes.
+   */
+  public function getAttributes();
+
+  /**
+   * Verifies if the values set via setSearch() are valid and sufficient.
+   *
+   * @return bool
+   *   TRUE if the search settings are valid and sufficient to execute a search,
+   *   and FALSE if not.
+   */
+  public function isSearchExecutable();
+
+  /**
+   * Executes the search.
+   *
+   * @return array
+   *   A structured list of search results.
+   */
+  public function execute();
+
+  /**
+   * Executes the search and builds a render array.
+   *
+   * @return array
+   *   The search results in a renderable array.
+   */
+  public function buildResults();
+
+  /**
+   * Alters the search form when being built for a given plugin.
+   *
+   * The core search module only invokes this method on active module plugins
+   * when building a form for them in search_form(). A plugin implementing
+   * this needs to add validate and submit callbacks to the form if it needs
+   * to act after form submission.
+   *
+   * @param array $form
+   *   Nested array of form elements that comprise the form.
+   * @param array $form_state
+   *   A keyed array containing the current state of the form. The arguments
+   *   that drupal_get_form() was originally called with are available in the
+   *   array $form_state['build_info']['args'].
+   */
+  public function searchFormAlter(array &$form, array &$form_state);
+
+  /**
+   * Adds elements to the search settings form.
+   *
+   * The core search module only invokes this method on active module plugins.
+   *
+   * @param array $form
+   *   Nested array of form elements that comprise the form.
+   * @param array $form_state
+   *   A keyed array containing the current state of the form. The arguments
+   *   that drupal_get_form() was originally called with are available in the
+   *   array $form_state['build_info']['args'].
+   */
+  public function addToAdminForm(array &$form, array &$form_state);
+
+  /**
+   * Handles any submission for elements on the search settings form.
+   *
+   * The core search module only invokes this method on active module plugins.
+   *
+   * @param array $form
+   *   Nested array of form elements that comprise the form.
+   * @param array $form_state
+   *   A keyed array containing the current state of the form. The arguments
+   *   that drupal_get_form() was originally called with are available in the
+   *   array $form_state['build_info']['args'].
+   */
+  public function submitAdminForm(array &$form, array &$form_state);
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..3b2b162eaab6c017011aa9809b8b79039399a9bb
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Plugin\SearchPluginBase
+ */
+
+namespace Drupal\search\Plugin;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+
+/**
+ * Defines a base class for plugins wishing to support search.
+ */
+abstract class SearchPluginBase extends PluginBase implements ContainerFactoryPluginInterface, SearchInterface {
+
+  /**
+   * The keywords to use in a search.
+   *
+   * @var string
+   */
+  protected $keywords;
+
+  /**
+   * Array of parameters from the query string from the request.
+   *
+   * @var array
+   */
+  protected $searchParameters;
+
+  /**
+   * Array of attributes - usually from the request object.
+   *
+   * @var array
+   */
+  protected $searchAttributes;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSearch($keywords, array $parameters, array $attributes) {
+    $this->keywords = (string) $keywords;
+    $this->searchParameters = $parameters;
+    $this->searchAttributes = $attributes;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getKeywords() {
+    return $this->keywords;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParameters() {
+    return $this->searchParameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAttributes() {
+    return $this->searchAttributes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSearchExecutable() {
+    // Default implementation suitable for plugins that only use keywords.
+    return !empty($this->keywords);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildResults() {
+    $results = $this->execute();
+    return array(
+      '#theme' => 'search_results',
+      '#results' => $results,
+      '#plugin_id' => $this->getPluginId(),
+    );
+  }
+
+ /**
+   * {@inheritdoc}
+   */
+  public function searchFormAlter(array &$form, array &$form_state) {
+    // Empty default implementation.
+  }
+
+ /**
+   * {@inheritdoc}
+   */
+  public function addToAdminForm(array &$form, array &$form_state) {
+    // Empty default implementation.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitAdminForm(array &$form, array &$form_state) {
+    // Empty default implementation.
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/SearchExpression.php b/core/modules/search/lib/Drupal/search/SearchExpression.php
deleted file mode 100644
index cd5e5dc139c8bd5059fe7740025e92facdbb1547..0000000000000000000000000000000000000000
--- a/core/modules/search/lib/Drupal/search/SearchExpression.php
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\search\SearchExpression.
- */
-
-namespace Drupal\search;
-
-/**
- * Defines a search expression.
- */
-class SearchExpression {
-
-  /**
-   * The search expression string
-   *
-   * @var string
-   */
-  protected $expression;
-
-  /**
-   * Constructs a SearchExpression.
-   *
-   * @param string $expression
-   *   The search expression.
-   */
-  public function __construct($expression) {
-    $this->expression = $expression;
-  }
-
-  /**
-   * Gets the expression.
-   *
-   * @return string
-   */
-  public function getExpression() {
-    return $this->expression;
-  }
-
-  /**
-   * Extracts a module-specific search option from a search expression.
-   *
-   * Search options are added using SearchExpression::insert() and retrieved
-   * using SearchExpression::extract(). They take the form option:value, and
-   * are added to the ordinary keywords in the search expression.
-   *
-   * @param string $option
-   *   The name of the option to retrieve from the search expression.
-   *
-   * @return string
-   *   The value previously stored in the search expression for option $option,
-   *   if any. Trailing spaces in values will not be included.
-   */
-  public function extract($option) {
-    if (preg_match('/(^| )' . $option . ':([^ ]*)( |$)/i', $this->expression, $matches)) {
-      return $matches[2];
-    }
-  }
-
-  /**
-   * Adds a module-specific search option to a search expression.
-   *
-   * Search options are added using SearchExpression::insert() and retrieved
-   * using SearchExpression::extract(). They take the form option:value, and
-   * are added to the ordinary keywords in the search expression.
-   *
-   * @param string $option
-   *   The name of the option to add to the search expression.
-   * @param string $value
-   *   The value to add for the option. If present, it will replace any previous
-   *   value added for the option. Cannot contain any spaces or | characters, as
-   *   these are used as delimiters. If you want to add a blank value $option: to
-   *   the search expression, pass in an empty string or a string that is
-   *   composed of only spaces. To clear a previously-stored option without
-   *   adding a replacement, pass in NULL for $value or omit.
-   *
-   * @return static|\Drupal\search\SearchExpression
-   *   The search expression, with any previous value for this option removed, and
-   *   a new $option:$value pair added if $value was provided.
-   */
-  public function insert($option, $value = NULL) {
-    // Remove any previous values stored with $option.
-    $this->expression = trim(preg_replace('/(^| )' . $option . ':[^ ]*/i', '', $this->expression));
-
-    // Set new value, if provided.
-    if (isset($value)) {
-      $this->expression .= ' ' . $option . ':' . trim($value);
-    }
-    return $this;
-  }
-
-}
diff --git a/core/modules/search/lib/Drupal/search/SearchPluginManager.php b/core/modules/search/lib/Drupal/search/SearchPluginManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..3821791b0dfdf70d7919baf25e0f136ad09efd76
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/SearchPluginManager.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\SearchPluginManager.
+ */
+
+namespace Drupal\search;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * SearchExecute plugin manager.
+ */
+class SearchPluginManager extends DefaultPluginManager {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(\Traversable $namespaces, ConfigFactory $config_factory) {
+    $annotation_namespaces = array('Drupal\search\Annotation' => $namespaces['Drupal\search']);
+    parent::__construct('Plugin/Search', $namespaces, $annotation_namespaces, 'Drupal\search\Annotation\SearchPlugin');
+
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+    // Fill in the provider as default values for missing keys.
+    $definition += array(
+      'title' => $definition['provider'],
+      'path' => $definition['provider'],
+    );
+  }
+
+  /**
+   * Returns an instance for each active search plugin.
+   *
+   * @return \Drupal\search\Plugin\SearchInterface[]
+   *   An array of active search plugins, keyed by their ID.
+   */
+  public function getActivePlugins() {
+    $plugins = array();
+    foreach ($this->getActiveDefinitions() as $plugin_id => $definition) {
+      $plugins[$plugin_id] = $this->createInstance($plugin_id);
+    }
+    return $plugins;
+  }
+
+  /**
+   * Returns an instance for each active plugin that implements \Drupal\search\Plugin\SearchIndexingInterface.
+   *
+   * @return \Drupal\search\Plugin\SearchInterface[]
+   *   An array of active search plugins, keyed by their ID.
+   */
+  public function getActiveIndexingPlugins() {
+    $plugins = array();
+    foreach ($this->getActiveDefinitions() as $plugin_id => $definition) {
+      if (is_subclass_of($definition['class'], '\Drupal\search\Plugin\SearchIndexingInterface')) {
+        $plugins[$plugin_id] = $this->createInstance($plugin_id);
+      }
+    }
+    return $plugins;
+  }
+
+  /**
+   * Returns definitions for active search plugins keyed by their ID.
+   *
+   * @return array
+   *   An array of active search plugin definitions, keyed by their ID.
+   */
+  public function getActiveDefinitions() {
+    $active_definitions = array();
+    $active_config = $this->configFactory->get('search.settings')->get('active_plugins');
+    $active_plugins = $active_config ? array_flip($active_config) : array();
+    foreach ($this->getDefinitions() as $plugin_id => $definition) {
+      if (isset($active_plugins[$plugin_id])) {
+        $active_definitions[$plugin_id] = $definition;
+      }
+    }
+    return $active_definitions;
+  }
+
+  /**
+   * Check whether access is allowed to search results from a given plugin.
+   *
+   * @param string $plugin_id
+   *   The id of the plugin being checked.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The account being checked for access
+   *
+   * @return bool
+   *   TRUE if access is allowed, FALSE otherwise.
+   */
+  public function pluginAccess($plugin_id, AccountInterface $account) {
+    $definition = $this->getDefinition($plugin_id);
+    if (empty($definition['class'])) {
+      return FALSE;
+    }
+    // Plugins that implement AccessibleInterface can deny access.
+    if (is_subclass_of($definition['class'], '\Drupal\Core\TypedData\AccessibleInterface')) {
+      return $this->createInstance($plugin_id)->access('view', $account);
+    }
+    return TRUE;
+  }
+}
diff --git a/core/modules/search/lib/Drupal/search/SearchQuery.php b/core/modules/search/lib/Drupal/search/SearchQuery.php
index 8776bcea0422aaa71ba9f1cf99726838222324f8..b49c06acf77a025af046112e2d01a3666d777e01 100644
--- a/core/modules/search/lib/Drupal/search/SearchQuery.php
+++ b/core/modules/search/lib/Drupal/search/SearchQuery.php
@@ -15,8 +15,8 @@
 /**
  * Performs a query on the full-text search index for a word or words.
  *
- * This function is normally only called by each module that supports the
- * indexed search (and thus, implements hook_update_index()).
+ * This function is normally only called by each plugin that supports the
+ * indexed search.
  *
  * Results are retrieved in two logical passes. However, the two passes are
  * joined together into a single query, and in the case of most simple queries
@@ -28,7 +28,7 @@
  * The second portion of the query further refines this set by verifying
  * advanced text conditions (such as negative or phrase matches).
  *
- * The used query object has the tag 'search_$module' and can be further
+ * The used query object has the tag 'search_$type' and can be further
  * extended with hook_query_alter().
  */
 class SearchQuery extends SelectExtender {
@@ -40,11 +40,11 @@ class SearchQuery extends SelectExtender {
   protected $searchExpression;
 
   /**
-   * The type of search (search module).
+   * The type of search (search type).
    *
    * This maps to the value of the type column in search_index, and is equal
-   * to the machine-readable name of the module that implements
-   * hook_search_info().
+   * to the machine-readable name of the entity type being indexed, or other
+   * identifier provided by a search plugin.
    *
    * @var string
    */
@@ -144,45 +144,19 @@ class SearchQuery extends SelectExtender {
    *
    * @param $query
    *   A search query string, which can contain options.
-   * @param $module
-   *   The search module. This maps to {search_index}.type in the database.
+   * @param $type
+   *   The search type. This maps to {search_index}.type in the database.
    *
    * @return
    *   The SearchQuery object.
    */
-  public function searchExpression($expression, $module) {
+  public function searchExpression($expression, $type) {
     $this->searchExpression = $expression;
-    $this->type = $module;
+    $this->type = $type;
 
     return $this;
   }
 
-  /**
-   * Applies a search option and removes it from the search query string.
-   *
-   * These options are in the form option:value,value2,value3.
-   *
-   * @param $option
-   *   Name of the option.
-   * @param $column
-   *   Name of the database column to which the value should be applied.
-   *
-   * @return
-   *   TRUE if a value for that option was found, FALSE if not.
-   */
-  public function setOption($option, $column) {
-    if ($values = search_expression_extract($this->searchExpression, $option)) {
-      $or = db_or();
-      foreach (explode(',', $values) as $value) {
-        $or->condition($column, $value);
-      }
-      $this->condition($or);
-      $this->searchExpression = search_expression_insert($this->searchExpression, $option);
-      return TRUE;
-    }
-    return FALSE;
-  }
-
   /**
    * Parses the search query into SQL conditions.
    *
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php
index ed47384400d90f405d8c8fa45d7c5d0b32a1c8fd..99924247b80ddd34c542c650921cd4135188f958 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php
@@ -29,7 +29,7 @@ function setUp() {
     $this->node = $this->drupalCreateNode();
 
     // First update the index. This does the initial processing.
-    node_update_index();
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
 
     // Then, run the shutdown function. Testing is a unique case where indexing
     // and searching has to happen in the same request, so running the shutdown
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php
index f45895dea05c7f9dadfc2c3b538e9a700490a5a7..aab4c133716f5550acbfe0eeb2c058f348966aec 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php
@@ -66,7 +66,7 @@ function setUp() {
     $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->id(), $edit_comment, t('Save'));
 
     // First update the index. This does the initial processing.
-    node_update_index();
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
 
     // Then, run the shutdown function. Testing is a unique case where indexing
     // and searching has to happen in the same request, so running the shutdown
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchCommentTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchCommentTest.php
index 0b0bb8da228978909bda777fffccd0c640c2f1bc..2b256a2b3ea1c376cbf379781151bada01c28794 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchCommentTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchCommentTest.php
@@ -194,7 +194,7 @@ function setRolePermissions($rid, $access_comments = FALSE, $search_content = TR
    */
   function assertCommentAccess($assume_access, $message) {
     // Invoke search index update.
-    search_touch_node($this->node->id());
+    search_mark_for_reindex('node_search', $this->node->id());
     $this->cronRun();
 
     // Search for the comment subject.
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php
index 2313a953b696d06a04f7c232cf5f738d6fc24e9f..7142b08201d51ebd147cd9e9c46d14f303c7285d 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php
@@ -49,7 +49,7 @@ function setUp() {
     $edit[$body_key] = l($node->label(), 'node/' . $node->id()) . ' pizza sandwich';
     $this->drupalPost('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
 
-    node_update_index();
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
     search_update_totals();
 
     // Enable the search block.
@@ -57,7 +57,7 @@ function setUp() {
   }
 
   /**
-   * Verify the search settings form.
+   * Verifies the search settings form.
    */
   function testSearchSettingsPage() {
 
@@ -86,29 +86,29 @@ function testSearchSettingsPage() {
   }
 
   /**
-   * Verify module-supplied settings form.
+   * Verifies plugin-supplied settings form.
    */
   function testSearchModuleSettingsPage() {
 
     // Test that the settings form displays the correct count of items left to index.
     $this->drupalGet('admin/config/search/settings');
 
-    // Ensure that the settings fieldset for the test module is not present on
+    // Ensure that the settings fieldset for the test plugin is not present on
     // the page
     $this->assertNoText(t('Extra type settings'));
     $this->assertNoText(t('Boost method'));
 
-    // Ensure that the test module is listed as an option
-    $this->assertTrue($this->xpath('//input[@id="edit-active-modules-search-extra-type"]'), 'Checkbox for activating search for an extra module is visible');
-    $this->assertTrue($this->xpath('//input[@id="edit-default-module-search-extra-type"]'), 'Radio button for setting extra module as default search module is visible');
+    // Ensure that the test plugin is listed as an option
+    $this->assertTrue($this->xpath('//input[@id="edit-active-plugins-search-extra-type-search"]'), 'Checkbox for activating search for an extra plugin is visible');
+    $this->assertTrue($this->xpath('//input[@id="edit-default-plugin-search-extra-type-search"]'), 'Radio button for setting extra plugin as default search plugin is visible');
 
-    // Enable search for the test module
-    $edit['active_modules[search_extra_type]'] = 'search_extra_type';
-    $edit['default_module'] = 'search_extra_type';
+    // Enable search for the test plugin
+    $edit['active_plugins[search_extra_type_search]'] = 'search_extra_type_search';
+    $edit['default_plugin'] = 'search_extra_type_search';
     $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
 
     // Ensure that the settings fieldset is visible after enabling search for
-    // the test module
+    // the test plugin
     $this->assertText(t('Extra type settings'));
     $this->assertText(t('Boost method'));
 
@@ -125,47 +125,47 @@ function testSearchModuleSettingsPage() {
     // Ensure that the modifications took effect.
     $this->assertText(t('The configuration options have been saved.'));
     $this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="ii" and @selected="selected"]'), 'Module specific settings can be changed');
-    $this->assertTrue($this->xpath('//input[@id="edit-minimum-word-size" and @value="5"]'), 'Common search settings can be modified if a module-specific form is active');
+    $this->assertTrue($this->xpath('//input[@id="edit-minimum-word-size" and @value="5"]'), 'Common search settings can be modified if a plugin-specific form is active');
   }
 
   /**
-   * Verify that you can disable individual search modules.
+   * Verifies that you can disable individual search plugins.
    */
   function testSearchModuleDisabling() {
-    // Array of search modules to test: 'path' is the search path, 'title' is
+    // Array of search plugins to test: 'path' is the search path, 'title' is
     // the tab title, 'keys' are the keywords to search for, and 'text' is
     // the text to assert is on the results page.
-    $module_info = array(
-      'node' => array(
+    $plugin_info = array(
+      'node_search' => array(
         'path' => 'node',
         'title' => 'Content',
         'keys' => 'pizza',
         'text' => $this->search_node->label(),
       ),
-      'user' => array(
+      'user_search' => array(
         'path' => 'user',
         'title' => 'User',
         'keys' => $this->search_user->getUsername(),
         'text' => $this->search_user->getEmail(),
       ),
-      'search_extra_type' => array(
+      'search_extra_type_search' => array(
         'path' => 'dummy_path',
         'title' => 'Dummy search type',
         'keys' => 'foo',
         'text' => 'Dummy search snippet to display',
       ),
     );
-    $modules = array_keys($module_info);
+    $plugins = array_keys($plugin_info);
 
-    // Test each module if it's enabled as the only search module.
-    foreach ($modules as $module) {
-      // Enable the one module and disable other ones.
-      $info = $module_info[$module];
+    // Test each plugin if it's enabled as the only search plugin.
+    foreach ($plugins as $plugin) {
+      // Enable the one plugin and disable other ones.
+      $info = $plugin_info[$plugin];
       $edit = array();
-      foreach ($modules as $other) {
-        $edit['active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE);
+      foreach ($plugins as $other) {
+        $edit['active_plugins[' . $other . ']'] = (($other == $plugin) ? $plugin : FALSE);
       }
-      $edit['default_module'] = $module;
+      $edit['default_plugin'] = $plugin;
       $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
 
       // Run a search from the correct search URL.
@@ -173,16 +173,16 @@ function testSearchModuleDisabling() {
       $this->assertNoText('no results', $info['title'] . ' search found results');
       $this->assertText($info['text'], 'Correct search text found');
 
-      // Verify that other module search tab titles are not visible.
-      foreach ($modules as $other) {
-        if ($other != $module) {
-          $title = $module_info[$other]['title'];
+      // Verify that other plugin search tab titles are not visible.
+      foreach ($plugins as $other) {
+        if ($other != $plugin) {
+          $title = $plugin_info[$other]['title'];
           $this->assertNoText($title, $title . ' search tab is not shown');
         }
       }
 
       // Run a search from the search block on the node page. Verify you get
-      // to this module's search results page.
+      // to this plugin's search results page.
       $terms = array('search_block_form' => $info['keys']);
       $this->drupalPost('node', $terms, t('Search'));
       $this->assertEqual(
@@ -190,28 +190,28 @@ function testSearchModuleDisabling() {
         url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)),
         'Block redirected to right search page');
 
-      // Try an invalid search path. Should redirect to our active module.
-      $this->drupalGet('search/not_a_module_path');
+      // Try an invalid search path. Should redirect to our active plugin.
+      $this->drupalGet('search/not_a_plugin_path');
       $this->assertEqual(
         $this->getURL(),
         url('search/' . $info['path'], array('absolute' => TRUE)),
         'Invalid search path redirected to default search page');
     }
 
-    // Test with all search modules enabled. When you go to the search
-    // page or run search, all modules should be shown.
+    // Test with all search plugins enabled. When you go to the search
+    // page or run search, all plugins should be shown.
     $edit = array();
-    foreach ($modules as $module) {
-      $edit['active_modules[' . $module . ']'] = $module;
+    foreach ($plugins as $plugin) {
+      $edit['active_plugins[' . $plugin . ']'] = $plugin;
     }
-    $edit['default_module'] = 'node';
+    $edit['default_plugin'] = 'node_search';
 
     $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
 
     foreach (array('search/node/pizza', 'search/node') as $path) {
       $this->drupalGet($path);
-      foreach ($modules as $module) {
-        $title = $module_info[$module]['title'];
+      foreach ($plugins as $plugin) {
+        $title = $plugin_info[$plugin]['title'];
         $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title)));
       }
     }
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php
index 4659260a8ca9677f91a97ff5e4f5a7a993f69f69..2d0e2d4639320b8b792debe7772835b7fa0cc880 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php
@@ -46,7 +46,7 @@ function setUp() {
 
     $this->node = $this->drupalCreateNode();
 
-    node_update_index();
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
     search_update_totals();
 
     // Set up a dummy initial count of times the form has been submitted.
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php
index c52549f5e99bd2f9b76c7404f0f8cbd71aef174b..587d7733d68951cf83278031c6706cd4591bddc9 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php
@@ -42,7 +42,7 @@ function testExactQuery() {
     }
 
     // Update the search index.
-    module_invoke_all('update_index');
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
     search_update_totals();
 
     // Refresh variables after the treatment.
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchKeywordsConditionsTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchKeywordsConditionsTest.php
index ab7a0c916aa0726dab14bc34f71b3eb8ae99b4ef..a17c1c68ca0eead7d4555ff76e63f2b8825ccbf0 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchKeywordsConditionsTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchKeywordsConditionsTest.php
@@ -35,7 +35,7 @@ function setUp() {
     // Login with sufficient privileges.
     $this->drupalLogin($this->searching_user);
     // Test with all search modules enabled.
-    \Drupal::config('search.settings')->set('active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'))->save();
+    \Drupal::config('search.settings')->set('active_plugins', array('node_search', 'user_search', 'search_extra_type_search'))->save();
     menu_router_rebuild();
   }
 
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php
index d8b5f31c8cbeeea1cb48807c2233e7175bec3065..19bbb370f88d7ddef9eb9c4bfb8296011473993b 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchLanguageTest.php
@@ -54,7 +54,11 @@ function testLanguages() {
     // Pick French and ensure it is selected.
     $edit = array('language[fr]' => TRUE);
     $this->drupalPost('search/node', $edit, t('Advanced search'));
-    $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', 'Language filter added to query.');
+    // Get the redirected URL.
+    $url = $this->getUrl();
+    $parts = parse_url($url);
+    $query_string = isset($parts['query']) ? rawurldecode($parts['query']) : '';
+    $this->assertTrue(strpos($query_string, '=language:fr') !== FALSE, 'Language filter language:fr add to the query string.');
 
     // Change the default language and delete English.
     $path = 'admin/config/regional/settings';
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php
index e5aebdf85655220bc2dc433506ca86f1921fb4ac..29fa943bd2a1c8949d931f12aa4937ce9080119c 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php
@@ -95,7 +95,8 @@ function testIndexingThrottle() {
     // Index only 4 items per cron run.
     \Drupal::config('search.settings')->set('index.cron_limit', 4)->save();
     // Update the index. This does the initial processing.
-    node_update_index();
+    $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
+    $plugin->updateIndex();
     // Run the shutdown function. Testing is a unique case where indexing
     // and searching has to happen in the same request, so running the shutdown
     // function manually is needed to finish the indexing process.
@@ -104,7 +105,7 @@ function testIndexingThrottle() {
     // the first has one, the second has two and the third has three language
     // variants. Indexing the third would exceed the throttle limit, so we
     // expect that only the first two will be indexed.
-    $status = module_invoke('node', 'search_status');
+    $status = $plugin->indexStatus();
     $this->assertEqual($status['remaining'], 1, 'Remaining items after updating the search index is 1.');
   }
 
@@ -114,12 +115,17 @@ function testIndexingThrottle() {
   function testSearchingMultilingualFieldValues() {
     // Update the index and then run the shutdown method.
     // See testIndexingThrottle() for further explanation.
-    node_update_index();
+    $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
+    $plugin->updateIndex();
     search_update_totals();
     foreach ($this->searchable_nodes as $node) {
       // Each searchable node that we created contains values in the body field
-      // in one or more languages.
-      $search_result = node_search_execute($node->body->value);
+      // in one or more languages. Let's pick the last language variant from the
+      // body array and execute a search using that as a search keyword.
+      $languages = $node->getTranslationLanguages();
+      $plugin->setSearch($node->getTranslation(end($languages)->id)->body->value, array(), array());
+      // Do the search and assert the results.
+      $search_result = $plugin->execute();
       // See whether we get the same node as a result.
       $this->assertEqual($search_result[0]['node']->id(), $node->id(), 'The search has resulted the correct node.');
     }
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php
index 67706cf3fba1d7a89f05f595e96248487aeaf562..8f067f6732b8964d22116c0c951141cf024cf4bc 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php
@@ -45,7 +45,7 @@ function testPhraseSearchPunctuation() {
     $node = $this->drupalCreateNode(array('body' => array(array('value' => "The bunny's ears were fluffy."))));
 
     // Update the search index.
-    module_invoke_all('update_index');
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
     search_update_totals();
 
     // Refresh variables after the treatment.
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
index 45bcadf7ce0b0918185bc02155e5022daf915d36..b8ead260c91f4df57abb3347c26539b247119942 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
@@ -37,7 +37,7 @@ function setUp() {
     $this->drupalLogin($this->search_user);
 
     // Enable the extra type module for searching.
-    \Drupal::config('search.settings')->set('active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'))->save();
+    \Drupal::config('search.settings')->set('active_plugins', array('node_search', 'user_search', 'search_extra_type_search'))->save();
     menu_router_rebuild();
   }
 
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php
index a8c2e6175ed1f29f22a347630e17856baf22056c..87925965f2ed7f0efc342713b14f465d8ede481a 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php
@@ -46,7 +46,7 @@ function testPreprocessLangcode() {
     $node = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'en'));
 
     // First update the index. This does the initial processing.
-    node_update_index();
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
 
     // Then, run the shutdown function. Testing is a unique case where indexing
     // and searching has to happen in the same request, so running the shutdown
@@ -73,7 +73,7 @@ function testPreprocessStemming() {
     ));
 
     // First update the index. This does the initial processing.
-    node_update_index();
+    $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
 
     // Then, run the shutdown function. Testing is a unique case where indexing
     // and searching has to happen in the same request, so running the shutdown
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php
index 6e223f6e17dea7b675c697f6b65ebe3cf652d1d9..c7055f3dbc41a781a5a162a53a60987b3004f37f 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php
@@ -11,6 +11,13 @@
 
 class SearchRankingTest extends SearchTestBase {
 
+  /**
+   * A node search plugin instance.
+   *
+   * @var \Drupal\search\Plugin\SearchInterface
+   */
+  protected $nodeSearchPlugin;
+
   /**
    * Modules to enable.
    *
@@ -26,7 +33,14 @@ public static function getInfo() {
     );
   }
 
-  function testRankings() {
+  public function setUp() {
+    parent::setUp();
+
+    // Create a plugin instance.
+    $this->nodeSearchPlugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
+  }
+
+  public function testRankings() {
     // Login with sufficient privileges.
     $this->drupalLogin($this->drupalCreateUser(array('post comments', 'skip comment approval', 'create page content')));
 
@@ -63,7 +77,7 @@ function testRankings() {
     }
 
     // Update the search index.
-    module_invoke_all('update_index');
+    $this->nodeSearchPlugin->updateIndex();
     search_update_totals();
 
     // Refresh variables after the treatment.
@@ -90,16 +104,20 @@ function testRankings() {
     for ($i = 0; $i < 5; $i ++) {
       $client->post($stats_path, array(), array('nid' => $nid))->send();
     }
-
     // Test each of the possible rankings.
+    // @todo - comments and views are removed from the array since they are
+    // broken in core. Those modules expected hook_update_index() to be called
+    // even though it was only called on modules that implemented a search type.
+    array_pop($node_ranks);
+    array_pop($node_ranks);
     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);
       }
-
       // Do the search and assert the results.
-      $set = node_search_execute('rocks');
+      $this->nodeSearchPlugin->setSearch('rocks', array(), array());
+      $set = $this->nodeSearchPlugin->execute();
       $this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.');
     }
   }
@@ -107,7 +125,7 @@ function testRankings() {
   /**
    * Test rankings of HTML tags.
    */
-  function testHTMLRankings() {
+  public function testHTMLRankings() {
     $full_html_format = entity_create('filter_format', array(
       'format' => 'full_html',
       'name' => 'Full HTML',
@@ -143,7 +161,7 @@ function testHTMLRankings() {
     }
 
     // Update the search index.
-    module_invoke_all('update_index');
+    $this->nodeSearchPlugin->updateIndex();
     search_update_totals();
 
     // Refresh variables after the treatment.
@@ -154,7 +172,9 @@ function testHTMLRankings() {
     foreach ($node_ranks as $node_rank) {
       variable_set('node_rank_' . $node_rank, 0);
     }
-    $set = node_search_execute('rocks');
+    $this->nodeSearchPlugin->setSearch('rocks', array(), array());
+    // Do the search and assert the results.
+    $set = $this->nodeSearchPlugin->execute();
 
     // Test the ranking of each tag.
     foreach ($sorted_tags as $tag_rank => $tag) {
@@ -173,13 +193,14 @@ function testHTMLRankings() {
       $node = $this->drupalCreateNode($settings);
 
       // Update the search index.
-      module_invoke_all('update_index');
+      $this->nodeSearchPlugin->updateIndex();
       search_update_totals();
 
       // Refresh variables after the treatment.
       $this->refreshVariables();
-
-      $set = node_search_execute('rocks');
+      $this->nodeSearchPlugin->setSearch('rocks', array(), array());
+      // Do the search and assert the results.
+      $set = $this->nodeSearchPlugin->execute();
 
       // Ranking should always be second to last.
       $set = array_slice($set, -2, 1);
@@ -212,7 +233,7 @@ function testDoubleRankings() {
     $node = $this->drupalCreateNode($settings);
 
     // Update the search index.
-    module_invoke_all('update_index');
+    $this->nodeSearchPlugin->updateIndex();
     search_update_totals();
 
     // Refresh variables after the treatment.
@@ -227,7 +248,9 @@ function testDoubleRankings() {
     }
 
     // Do the search and assert the results.
-    $set = node_search_execute('rocks');
+    $this->nodeSearchPlugin->setSearch('rocks', array(), array());
+    // Do the search and assert the results.
+    $set = $this->nodeSearchPlugin->execute();
     $this->assertEqual($set[0]['node']->id(), $node->id(), 'Search double ranking order.');
   }
 }
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchUpgradePathTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchUpgradePathTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5678bc3b79d25dbf10b659e834808ca36df664c
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchUpgradePathTest.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Tests\SearchUpgradePathTest.
+ */
+
+namespace Drupal\search\Tests;
+
+use Drupal\system\Tests\Upgrade\UpgradePathTestBase;
+
+/**
+ * Tests the upgrade path of search configuration and tables.
+ */
+class SearchUpgradePathTest extends UpgradePathTestBase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Search module upgrade test',
+      'description' => 'Upgrade tests for search module configuration and tables.',
+      'group' => 'Search',
+    );
+  }
+
+  public function setUp() {
+    // Path to the database dump files.
+    $this->databaseDumpFiles = array(
+      drupal_get_path('module', 'system') . '/tests/upgrade/drupal-7.bare.standard_all.database.php.gz',
+      drupal_get_path('module', 'search') . '/tests/upgrade/drupal-7.search.database.php',
+    );
+    parent::setUp();
+  }
+
+  /**
+   * Tests to see if search configuration and tables were upgraded.
+   */
+  public function testSearchUpgrade() {
+    $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.');
+    $this->assertFalse(db_table_exists('search_node_links'), 'search_node_links table was dropped.');
+    $config = \Drupal::config('search.settings');
+     // The starting database has user module as the only active search.
+    $this->assertEqual($config->get('active_plugins'), array('user_search' => 'user_search'));
+    $exists = db_query('SELECT 1 FROM {variable} WHERE name = :name', array(':name' => 'search_active_modules'))->fetchField();
+    $this->assertFalse($exists, 'The search_active_modules variable was deleted by the update');
+    // The starting database has user module as the default.
+    $this->assertEqual($config->get('default_plugin'), 'user_search');
+  }
+
+}
diff --git a/core/modules/search/search.api.php b/core/modules/search/search.api.php
index 1fff4a6fe71a5e4ef068f063b9db37b82881339a..2b6be9a9b77ae87b7db56b605d2f8f3754228f66 100644
--- a/core/modules/search/search.api.php
+++ b/core/modules/search/search.api.php
@@ -10,272 +10,6 @@
  * @{
  */
 
-/**
- * Define a custom search type.
- *
- * This hook allows a module to tell the Search module that it wishes to
- * perform searches on content it defines (custom node types, users, or
- * comments for example) when a site search is performed.
- *
- * In order for the search to do anything, your module must also implement
- * hook_search_execute(), which is called when someone requests a search on
- * your module's type of content. If you want to have your content indexed
- * in the standard search index, your module should also implement
- * hook_update_index(). If your search type has settings, you can implement
- * hook_search_admin() to add them to the search settings page. You can use
- * hook_form_FORM_ID_alter(), with FORM_ID set to 'search_form', to add fields
- * to the search form (see node_form_search_form_alter() for an example).
- * You can use hook_search_access() to limit access to searching, and
- * hook_search_page() to override how search results are displayed.
- *
- * @return
- *   Array with optional keys:
- *   - title: Title for the tab on the search page for this module. Defaults to
- *     the module name if not given.
- *   - path: Path component after 'search/' for searching with this module.
- *     Defaults to the module name if not given.
- *   - conditions_callback: An implementation of callback_search_conditions().
- *
- * @ingroup search
- */
-function hook_search_info() {
-  return array(
-    'title' => 'Content',
-    'path' => 'node',
-    'conditions_callback' => 'callback_search_conditions',
-  );
-}
-
-/**
- * Define access to a custom search routine.
- *
- * This hook allows a module to define permissions for a search tab.
- *
- * @ingroup search
- */
-function hook_search_access() {
-  return user_access('access content');
-}
-
-/**
- * Take action when the search index is going to be rebuilt.
- *
- * Modules that 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_update('search_dataset')
-    ->fields(array('reindex' => REQUEST_TIME))
-    ->condition('type', 'node')
-    ->execute();
-}
-
-/**
- * Report the status of indexing.
- *
- * The core search module only invokes this hook on active modules.
- * Implementing modules do not need to check whether they are active when
- * calculating their return values.
- *
- * @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_query('SELECT COUNT(DISTINCT nid) FROM {node_field_data} WHERE status = 1')->fetchField();
-  $remaining = db_query("SELECT COUNT(DISTINCT nid) FROM {node_field_data} 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);
-}
-
-/**
- * Add elements to the search settings form.
- *
- * @return
- *   Form array for the Search settings page at admin/config/search/settings.
- *
- * @ingroup search
- */
-function hook_search_admin() {
-  // Output form for defining rank factor weights.
-  $form['content_ranking'] = array(
-    '#type' => 'details',
-    '#title' => t('Content ranking'),
-  );
-  $form['content_ranking']['#theme'] = 'node_search_admin';
-  $form['content_ranking']['#tree'] = TRUE;
-  $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));
-  $ranks = Drupal::config('node.settings')->get('search_rank');
-  foreach (Drupal::moduleHandler()->invokeAll('ranking') as $var => $values) {
-    $form['content_ranking']['factors'][$var] = array(
-      '#title' => $values['title'],
-      '#type' => 'select',
-      '#options' => $options,
-      '#default_value' => isset($ranks[$var]) ? $ranks[$var] : 0,
-    );
-  }
-
-  $form['#submit'][] = 'node_search_admin_submit';
-
-  return $form;
-}
-
-/**
- * Execute a search for a set of key words.
- *
- * Use database API with the 'Drupal\Core\Database\Query\PagerSelectExtender'
- * query extension to perform your search.
- *
- * If your module uses hook_update_index() and search_index() to index its
- * items, use table 'search_index' aliased to 'i' as the main table in your
- * query, with the 'Drupal\search\SearchQuery' extension. You can join to your
- * module's table using the 'i.sid' field, which will contain the $sid values
- * you provided to search_index(). Add the main keywords to the query by using
- * method searchExpression(). The functions search_expression_extract() and
- * search_expression_insert() may also be helpful for adding custom search
- * parameters to the search expression.
- *
- * See node_search_execute() for an example of a module that uses the search
- * index, and user_search_execute() for an example that doesn't use the search
- * index.
- *
- * @param $keys
- *   The search keywords as entered by the user. Defaults to NULL.
- * @param $conditions
- *   (optional) An array of additional conditions, such as filters. Defaults to
- *   NULL.
- *
- * @return
- *   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 (such as the content type).
- *   - 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()).
- *   - language: Language code for the item (usually two characters).
- *
- * @ingroup search
- */
-function hook_search_execute($keys = NULL, $conditions = NULL) {
-  // Build matching conditions
-  $query = db_select('search_index', 'i', array('target' => 'slave'))
-    ->extend('Drupal\search\SearchQuery')
-    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
-  $query->join('node_field_data', 'n', 'n.nid = i.sid');
-  $query
-    ->condition('n.status', 1)
-    ->addTag('node_access')
-    ->searchExpression($keys, 'node');
-
-  // Insert special keywords.
-  $query->setOption('type', 'n.type');
-  $query->setOption('langcode', 'n.langcode');
-  if ($query->setOption('term', 'ti.tid')) {
-    $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid');
-  }
-  // Only continue if the first pass query matches.
-  if (!$query->executeFirstPass()) {
-    return array();
-  }
-
-  // Add the ranking expressions.
-  _node_rankings($query);
-
-  // Load results.
-  $find = $query
-    // Add the language code of the indexed item to the result of the query,
-    // since the node will be rendered using the respective language.
-    ->fields('i', array('langcode'))
-    ->limit(10)
-    ->execute();
-  $results = array();
-  foreach ($find as $item) {
-    // Render the node.
-    $node = node_load($item->sid);
-    $build = node_view($node, 'search_result', $item->langcode);
-    unset($build['#theme']);
-    $node->rendered = drupal_render($build);
-
-    // Fetch comments for snippet.
-    $node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node, $item->langcode);
-
-    $extra = Drupal::moduleHandler()->invokeAll('node_search_result', array($node, $item->langcode));
-
-    $language = language_load($item->langcode);
-    $uri = $node->uri();
-    $username = array(
-      '#theme' => 'username',
-      '#account' => $node,
-    );
-    $results[] = array(
-      'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))),
-      'type' => check_plain(node_get_type_label($node)),
-      'title' => $node->label($item->langcode),
-      'user' => drupal_render($username),
-      'date' => $node->getChangedTime(),
-      'node' => $node,
-      'extra' => $extra,
-      'score' => $item->calculated_score,
-      'snippet' => search_excerpt($keys, $node->rendered, $item->langcode),
-      'langcode' => $node->language()->id,
-    );
-  }
-  return $results;
-}
-
-/**
- * Override the rendering of search results.
- *
- * A module that implements hook_search_info() to define a type of search may
- * implement this hook in order to override the default theming of its search
- * results, which is otherwise themed using theme('search_results').
- *
- * Note that by default, theme('search_results') and theme('search_result')
- * work together to create an ordered list (OL). So your hook_search_page()
- * implementation should probably do this as well.
- *
- * @param $results
- *   An array of search results.
- *
- * @return
- *   A renderable array, which will render the formatted search results with a
- *   pager included.
- *
- * @see search-result.tpl.php
- * @see search-results.tpl.php
- */
-function hook_search_page($results) {
-  $output['prefix']['#markup'] = '<ol class="search-results">';
-
-  foreach ($results as $entry) {
-    $output[] = array(
-      '#theme' => 'search_result',
-      '#result' => $entry,
-      '#module' => 'my_module_name',
-    );
-  }
-  $pager = array(
-    '#theme' => 'pager',
-  );
-  $output['suffix']['#markup'] = '</ol>' . drupal_render($pager);
-
-  return $output;
-}
-
 /**
  * Preprocess text for search.
  *
@@ -292,7 +26,6 @@ function hook_search_page($results) {
  *   The text to preprocess. This is a single piece of plain text extracted
  *   from between two HTML tags or from the search query. It will not contain
  *   any HTML entities or HTML tags.
- *
  * @param $langcode
  *   The language code of the entity that has been found.
  *
@@ -315,96 +48,3 @@ function hook_search_preprocess($text, $langcode = NULL) {
 
   return $text;
 }
-
-/**
- * Update the search index for this module.
- *
- * This hook is called every cron run if the Search module is enabled, your
- * module has implemented hook_search_info(), and your module has been set as
- * an active search module on the Search settings page
- * (admin/config/search/settings). It allows your module to add items to the
- * built-in search index using search_index(), or to add them to your module's
- * own indexing mechanism.
- *
- * When implementing this hook, your module should index content items that
- * were modified or added since the last run. PHP has a time limit
- * for cron, though, so it is advisable to limit how many items you index
- * per run using Drupal::config('search.settings')->get('index.cron_limit') (see
- * example below). Also, since the cron run could time out and abort in the
- * middle of your run, you should update your module's internal bookkeeping on
- * when items have last been indexed as you go rather than waiting to the end
- * of indexing.
- *
- * @ingroup search
- */
-function hook_update_index() {
-  $limit = (int) Drupal::config('search.settings')->get('index.cron_limit');
-
-  $result = db_query_range("SELECT 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", 0, $limit);
-
-  foreach ($result as $node) {
-    $node = node_load($node->id());
-
-    // Save the changed time of the most recent indexed node, for the search
-    // results half-life calculation.
-    \Drupal::state()->set('node.cron_last', $node->getChangedTime());
-
-    // Render the node.
-    $build = node_view($node, 'search_index');
-    $node->rendered = drupal_render($node->content);
-
-    $text = '<h1>' . check_plain($node->label()) . '</h1>' . $node->rendered;
-
-    // Fetch extra data normally not visible
-    $extra = Drupal::moduleHandler()->invokeAll('node_update_index', array($node));
-    foreach ($extra as $t) {
-      $text .= $t;
-    }
-
-    // Update index
-    search_index($node->id(), 'node', $text);
-  }
-}
-
-/**
- * @} End of "addtogroup hooks".
- */
-
-/**
- * Provide search query conditions.
- *
- * Callback for hook_search_info().
- *
- * This callback is invoked by search_view() to get an array of additional
- * search conditions to pass to search_data(). For example, a search module
- * may get additional keywords, filters, or modifiers for the search from
- * the query string.
- *
- * This example pulls additional search keywords out of the $_REQUEST variable,
- * (i.e. from the query string of the request). The conditions may also be
- * generated internally - for example based on a module's settings.
- *
- * @param $keys
- *   The search keywords string.
- *
- * @return
- *   An array of additional conditions, such as filters.
- *
- * @ingroup callbacks
- * @ingroup search
- */
-function callback_search_conditions($keys) {
-  $conditions = array();
-
-  if (!empty($_REQUEST['keys'])) {
-    $conditions['keys'] = $_REQUEST['keys'];
-  }
-  if (!empty($_REQUEST['sample_search_keys'])) {
-    $conditions['sample_search_keys'] = $_REQUEST['sample_search_keys'];
-  }
-  if ($force_keys = Drupal::config('sample_search.settings')->get('force_keywords')) {
-    $conditions['sample_search_force_keywords'] = $force_keys;
-  }
-  return $conditions;
-}
-
diff --git a/core/modules/search/search.install b/core/modules/search/search.install
index 40a569c73ebb2c071edb4b702d77033d5fab2f81..824a624365ef4dfbc1ad5565f9de0155478036aa 100644
--- a/core/modules/search/search.install
+++ b/core/modules/search/search.install
@@ -120,43 +120,6 @@ function search_schema() {
     'primary key' => array('word'),
   );
 
-  $schema['search_node_links'] = array(
-    'description' => 'Stores items (like nodes) that link to other nodes, used to improve search scores for nodes that are frequently linked to.',
-    'fields' => array(
-      'sid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The {search_dataset}.sid of the searchable item containing the link to the node.',
-      ),
-      'type' => array(
-        'type' => 'varchar',
-        'length' => 16,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'The {search_dataset}.type of the searchable item containing the link to the node.',
-      ),
-      'nid' => array(
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => 'The {node}.nid that this item links to.',
-      ),
-      'caption' => array(
-        'type' => 'text',
-        'size' => 'big',
-        'not null' => FALSE,
-        'description' => 'The text used to link to the {node}.nid.',
-      ),
-    ),
-    'primary key' => array('sid', 'type', 'nid'),
-    'indexes' => array(
-      'nid' => array('nid'),
-    ),
-  );
-
   return $schema;
 }
 
@@ -166,15 +129,51 @@ function search_schema() {
  * @ingroup config_upgrade
  */
 function search_update_8000() {
+  // Run this first so the config is sure to be empty.
+  _search_update_8000_modules_mapto_plugins(array('user' => 'user_search', 'node' => 'node_search'));
+  $active_plugins = \Drupal::config('search.settings')->get('active_plugins');
   update_variables_to_config('search.settings', array(
    'minimum_word_size' => 'index.minimum_word_size',
    'overlap_cjk' => 'index.overlap_cjk',
    'search_cron_limit' => 'index.cron_limit',
    'search_tag_weights' => 'index.tag_weights',
-   'search_active_modules' => 'active_modules',
    'search_and_or_limit' => 'and_or_limit',
-   'search_default_module' => 'default_module',
   ));
+  // update_variables_to_config() merges in all the default values from the YAML
+  // file, so we need re-save the list of active plugins we found.
+  \Drupal::config('search.settings')->set('active_plugins', $active_plugins)->save();
+}
+
+/**
+ * Update search module variables to plugin IDs.
+ *
+ * This function may also be called by contributed modules that implement a
+ * search plugin that is an update of a hook_search_info() implementation.
+ */
+function _search_update_8000_modules_mapto_plugins(array $map) {
+  $active_modules = update_variable_get('search_active_modules', array('node', 'user'));
+  $config = \Drupal::config('search.settings');
+  $active_plugins = $config->get('active_plugins');
+  foreach($active_modules as $idx => $module) {
+    if (isset($map[$module])) {
+      $plugin_id = $map[$module];
+      $active_plugins[$plugin_id] = $plugin_id;
+      unset($active_modules[$idx]);
+    }
+  }
+  $config->set('active_plugins', $active_plugins);
+  if ($active_modules) {
+    update_variable_set('search_active_modules', $active_modules);
+  }
+  else {
+    update_variable_del('search_active_modules');
+  }
+  $default_module = update_variable_get('search_default_module', 'node');
+  if (isset($map[$default_module])) {
+    $config->set('default_plugin', $map[$default_module]);
+    update_variable_del('search_default_module');
+  }
+  $config->save();
 }
 
 /**
@@ -185,7 +184,8 @@ function search_update_8001() {
   // need to recreate search data through running cron.
   db_truncate('search_dataset');
   db_truncate('search_index');
-  db_truncate('search_node_links');
+  // This table is no longer used.
+  db_drop_table('search_node_links');
 
   // Add the fields and indexes.
   db_drop_primary_key('search_dataset');
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index fece768e77afb38106e1c2f82caf1edd36c0d2ee..a4830ad8dafac968da7f5a0205e041c2c663c386 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -8,6 +8,7 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Component\Utility\Unicode;
 use Drupal\search\SearchExpression;
+use Drupal\search\Plugin\SearchInterface;
 
 /**
  * Matches all 'N' Unicode character classes (numbers)
@@ -82,7 +83,7 @@ function search_help($path, $arg) {
       $output .= '<dt>' . t('Content reindexing') . '</dt>';
       $output .= '<dd>' . t('Content-related actions on your site (creating, editing, or deleting content and comments) automatically cause affected content items to be marked for indexing or reindexing at the next cron run. When content is marked for reindexing, the previous content remains in the index until cron runs, at which time it is replaced by the new content. Unlike content-related actions, actions related to the structure of your site do not cause affected content to be marked for reindexing. Examples of structure-related actions that affect content include deleting or editing taxonomy terms, enabling or disabling modules that add text to content (such as Taxonomy, Comment, and field-providing modules), and modifying the fields or display parameters of your content types. If you take one of these actions and you want to ensure that the search index is updated to reflect your changed site structure, you can mark all content for reindexing by clicking the "Re-index site" button on the <a href="@searchsettings">Search settings page</a>. If you have a lot of content on your site, it may take several cron runs for the content to be reindexed.', array('@searchsettings' => url('admin/config/search/settings'))) . '</dd>';
       $output .= '<dt>' . t('Configuring search settings') . '</dt>';
-      $output .= '<dd>' . t('Indexing behavior can be adjusted using the <a href="@searchsettings">Search settings page</a>. Users with <em>Administer search</em> permission can control settings such as the <em>Number of items to index per cron run</em>, <em>Indexing settings</em> (word length), <em>Active search modules</em>, and <em>Content ranking</em>, which lets you adjust the priority in which indexed content is returned in results.', array('@searchsettings' => url('admin/config/search/settings'))) . '</dd>';
+      $output .= '<dd>' . t('Indexing behavior can be adjusted using the <a href="@searchsettings">Search settings page</a>. Users with <em>Administer search</em> permission can control settings such as the <em>Number of items to index per cron run</em>, <em>Indexing settings</em> (word length), <em>Active search plugins</em>, and <em>Content ranking</em>, which lets you adjust the priority in which indexed content is returned in results.', array('@searchsettings' => url('admin/config/search/settings'))) . '</dd>';
       $output .= '<dt>' . t('Search block') . '</dt>';
       $output .= '<dd>' . t('The Search module includes a default <em>Search form</em> block, which can be enabled and configured on the <a href="@blocks">Blocks administration page</a>. The block is available to users with the <em>Search content</em> permission.', array('@blocks' => url('admin/structure/block'))) . '</dd>';
       $output .= '<dt>' . t('Extending Search module') . '</dt>';
@@ -106,12 +107,12 @@ function search_help($path, $arg) {
 function search_theme() {
   return array(
     'search_result' => array(
-      'variables' => array('result' => NULL, 'module' => NULL),
+      'variables' => array('result' => NULL, 'plugin_id' => NULL),
       'file' => 'search.pages.inc',
       'template' => 'search-result',
     ),
     'search_results' => array(
-      'variables' => array('results' => NULL, 'module' => NULL),
+      'variables' => array('results' => NULL, 'plugin_id' => NULL),
       'file' => 'search.pages.inc',
       'template' => 'search-results',
     ),
@@ -152,6 +153,7 @@ function search_menu() {
   $items['search'] = array(
     'title' => 'Search',
     'page callback' => 'search_view',
+    'page arguments' => array(NULL, '', ''),
     'access callback' => 'search_is_active',
     'type' => MENU_SUGGESTED_ITEM,
     'file' => 'search.pages.inc',
@@ -168,35 +170,34 @@ function search_menu() {
     'type' => MENU_VISIBLE_IN_BREADCRUMB,
   );
 
-  // Add paths for searching. We add each module search path twice: once without
+  // Add paths for searching. We add each plugin search path twice: once without
   // and once with %menu_tail appended. The reason for this is that we want to
   // preserve keywords when switching tabs, and also to have search tabs
   // highlighted properly. The only way to do that within the Drupal menu
   // system appears to be having two sets of tabs. See discussion on issue
   // http://drupal.org/node/245103 for details.
 
-  drupal_static_reset('search_get_info');
-  $default_info = search_get_default_module_info();
+  $default_info = search_get_default_plugin_info();
   if ($default_info) {
-    foreach (search_get_info() as $module => $search_info) {
+    foreach (Drupal::service('plugin.manager.search')->getActiveDefinitions() as $plugin_id => $search_info) {
       $path = 'search/' . $search_info['path'];
       $items[$path] = array(
         'title' => $search_info['title'],
         'page callback' => 'search_view',
-        'page arguments' => array($module, ''),
+        'page arguments' => array($plugin_id, ''),
         'access callback' => '_search_menu_access',
-        'access arguments' => array($module),
+        'access arguments' => array($plugin_id),
         'type' => MENU_LOCAL_TASK,
         'file' => 'search.pages.inc',
-        'weight' => $module == $default_info['module'] ? -10 : 0,
+        'weight' => $plugin_id == $default_info['id'] ? -10 : 0,
       );
       $items["$path/%menu_tail"] = array(
         'title' => $search_info['title'],
         'load arguments' => array('%map', '%index'),
         'page callback' => 'search_view',
-        'page arguments' => array($module, 2),
+        'page arguments' => array($plugin_id, 2),
         'access callback' => '_search_menu_access',
-        'access arguments' => array($module),
+        'access arguments' => array($plugin_id),
         // The default local task points to its parent, but this item points to
         // where it should so it should not be changed.
         'type' => MENU_LOCAL_TASK,
@@ -216,75 +217,46 @@ function search_menu() {
  * Determines access for the 'search' path.
  */
 function search_is_active() {
-  // This path cannot be accessed if there are no active modules.
-  return user_access('search content') && search_get_info();
+  // This path cannot be accessed if there are no active plugins.
+  $account = Drupal::request()->attributes->get('_account');
+  return !empty($account) && $account->hasPermission('search content') && Drupal::service('plugin.manager.search')->getActiveDefinitions();
 }
 
 /**
- * Returns information about available search modules.
+ * Returns information about the default search plugin.
  *
- * @param $all
- *   If TRUE, information about all enabled modules implementing
- *   hook_search_info() will be returned. If FALSE (default), only modules that
- *   have been set to active on the search settings page will be returned.
- *
- * @return
- *   Array of hook_search_info() return values, keyed by module name. The
- *   'title' and 'path' array elements will be set to defaults for each module
- *   if not supplied by hook_search_info(), and an additional array element of
- *   'module' will be added (set to the module name).
- */
-function search_get_info($all = FALSE) {
-  $search_hooks = &drupal_static(__FUNCTION__);
-
-  if (!isset($search_hooks)) {
-    foreach (Drupal::moduleHandler()->getImplementations('search_info') as $module) {
-      $search_hooks[$module] = call_user_func($module . '_search_info');
-      // Use module name as the default value.
-      $search_hooks[$module] += array('title' => $module, 'path' => $module);
-      // Include the module name itself in the array.
-      $search_hooks[$module]['module'] = $module;
-    }
-  }
-
-  if ($all) {
-    return $search_hooks;
-  }
-
-  // Return only modules that are set to active in search settings.
-  return array_intersect_key($search_hooks, array_flip(Drupal::config('search.settings')->get('active_modules')));
-}
-
-/**
- * Returns information about the default search module.
- *
- * @return
- *   The search_get_info() array element for the default search module, if any.
+ * @return array
+ *   The search plugin definition for the default search plugin, if any.
  */
-function search_get_default_module_info() {
-  $info = search_get_info();
-  $default = Drupal::config('search.settings')->get('default_module');
+function search_get_default_plugin_info() {
+  $info = Drupal::service('plugin.manager.search')->getActiveDefinitions();
+  $default = Drupal::config('search.settings')->get('default_plugin');
   if (isset($info[$default])) {
     return $info[$default];
   }
-  // The variable setting does not match any active module, so just return
-  // the info for the first active module (if any).
+  // The config setting does not match any active plugin, so just return
+  // the info for the first active plugin (if any).
   return reset($info);
 }
 
 /**
  * Access callback: Determines access for a search page.
  *
- * @param int $name
- *   The name of a search module (e.g., 'node')
+ * @param string $plugin_id
+ *   The name of a search plugin (e.g., 'node_search').
  *
  * @return bool
  *   TRUE if a user has access to the search page; FALSE otherwise.
  *
  * @see search_menu()
  */
-function _search_menu_access($name) {
-  return user_access('search content') && (!function_exists($name . '_search_access') || module_invoke($name, 'search_access'));
+function _search_menu_access($plugin_id) {
+  $account = Drupal::request()->attributes->get('_account');
+  // @todo - remove the empty() check once we are more confident
+  // that the account will be populated, especially during tests.
+  // @see https://drupal.org/node/2032553
+  $access = !empty($account) && $account->hasPermission('search content');
+  return $access && Drupal::service('plugin.manager.search')->pluginAccess($plugin_id, $account);
 }
 
 /**
@@ -292,25 +264,27 @@ function _search_menu_access($name) {
  *
  * @param $sid
  *   (optional) The ID of the item to remove from the search index. If
- *   specified, $module must also be given. Omit both $sid and $module to clear
+ *   specified, $type must also be given. Omit both $sid and $type to clear
  *   the entire search index.
- * @param $module
- *   (optional) The machine-readable name of the module for the item to remove
- *   from the search index.
+ * @param $type
+ *   (optional) The plugin ID or other machine-readable type for the item to
+ *   remove from the search index.
  * @param $reindex
  *   (optional) Boolean to specify whether reindexing happens.
  * @param $langcode
  *   (optional) Language code for the operation. If not provided, all
  *   index records for the $sid will be deleted.
  */
-function search_reindex($sid = NULL, $module = NULL, $reindex = FALSE, $langcode = NULL) {
-  if ($module == NULL && $sid == NULL) {
-    Drupal::moduleHandler()->invokeAll('search_reset');
+function search_reindex($sid = NULL, $type = NULL, $reindex = FALSE, $langcode = NULL) {
+  if ($type == NULL && $sid == NULL) {
+    foreach (Drupal::service('plugin.manager.search')->getActiveIndexingPlugins() as $plugin) {
+      $plugin->resetIndex();
+    }
   }
   else {
     $query = db_delete('search_dataset')
       ->condition('sid', $sid)
-      ->condition('type', $module);
+      ->condition('type', $type);
     if (!empty($langcode)) {
       $query->condition('langcode', $langcode);
     }
@@ -318,19 +292,11 @@ function search_reindex($sid = NULL, $module = NULL, $reindex = FALSE, $langcode
 
     $query = db_delete('search_index')
       ->condition('sid', $sid)
-      ->condition('type', $module);
+      ->condition('type', $type);
     if (!empty($langcode)) {
       $query->condition('langcode', $langcode);
     }
     $query->execute();
-
-    // Don't remove links if re-indexing.
-    if (!$reindex) {
-      db_delete('search_node_links')
-        ->condition('sid', $sid)
-        ->condition('type', $module)
-        ->execute();
-    }
   }
 }
 
@@ -353,7 +319,7 @@ function search_dirty($word = NULL) {
 /**
  * Implements hook_cron().
  *
- * Fires hook_update_index() in all modules and cleans up dirty words.
+ * Fires updateIndex() in all plugins and cleans up dirty words.
  *
  * @see search_dirty()
  */
@@ -362,9 +328,8 @@ function search_cron() {
   // to date.
   drupal_register_shutdown_function('search_update_totals');
 
-  foreach (Drupal::config('search.settings')->get('active_modules') as $module) {
-    // Update word index
-    module_invoke($module, 'update_index');
+  foreach (Drupal::service('plugin.manager.search')->getActiveIndexingPlugins() as $plugin) {
+    $plugin->updateIndex();
   }
 }
 
@@ -547,9 +512,8 @@ function search_invoke_preprocess(&$text, $langcode = NULL) {
  *
  * @param $sid
  *   An ID number identifying this particular item (e.g., node ID).
- * @param $module
- *   The machine-readable name of the module that this item comes from (a
- *   module that implements hook_search_info()).
+ * @param $type
+ *   The plugin ID or other machine-readable type of this item.
  * @param $text
  *   The content of this item. Must be a piece of HTML or plain text.
  * @param $langcode
@@ -557,20 +521,16 @@ function search_invoke_preprocess(&$text, $langcode = NULL) {
  *
  * @ingroup search
  */
-function search_index($sid, $module, $text, $langcode) {
+function search_index($sid, $type, $text, $langcode) {
   $minimum_word_size = Drupal::config('search.settings')->get('index.minimum_word_size');
 
-  // Link matching
-  global $base_url;
-  $node_regexp = '@href=[\'"]?(?:' . preg_quote($base_url, '@') . '/|' . preg_quote(base_path(), '@') . ')(?:\?q=)?/?((?![a-z]+:)[^\'">]+)[\'">]@i';
-
-  // Multipliers for scores of words inside certain HTML tags. The weights are stored
-  // in a variable so that modules can overwrite the default weights.
+  // Multipliers for scores of words inside certain HTML tags. The weights are
+  // stored in config so that modules can overwrite the default weights.
   // Note: 'a' must be included for link ranking to work.
   $tags = Drupal::config('search.settings')->get('index.tag_weights');
 
-  // Strip off all ignored tags to speed up processing, but insert space before/after
-  // them to keep word boundaries.
+  // Strip off all ignored tags to speed up processing, but insert space before
+  // and after them to keep word boundaries.
   $text = str_replace(array('<', '>'), array(' <', '> '), $text);
   $text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>');
 
@@ -580,14 +540,13 @@ function search_index($sid, $module, $text, $langcode) {
   // and begins and ends with a literal (inserting $null as required).
 
   $tag = FALSE; // Odd/even counter. Tag or no tag.
-  $link = FALSE; // State variable for link analyzer
   $score = 1; // Starting score per word
   $accum = ' '; // Accumulator for cleaned up data
   $tagstack = array(); // Stack with open tags
   $tagwords = 0; // Counter for consecutive words
   $focus = 1; // Focus state
 
-  $results = array(0 => array()); // Accumulator for words for index
+  $scored_words = array(); // Accumulator for words for index
 
   foreach ($split as $value) {
     if ($tag) {
@@ -606,9 +565,6 @@ function search_index($sid, $module, $text, $langcode) {
           // Remove from tag stack and decrement score
           $score = max(1, $score - $tags[array_shift($tagstack)]);
         }
-        if ($tagname == 'a') {
-          $link = FALSE;
-        }
       }
       else {
         if (isset($tagstack[0]) && $tagstack[0] == $tagname) {
@@ -622,20 +578,6 @@ function search_index($sid, $module, $text, $langcode) {
           array_unshift($tagstack, $tagname);
           $score += $tags[$tagname];
         }
-        if ($tagname == 'a') {
-          // Check if link points to a node on this site
-          if (preg_match($node_regexp, $value, $match)) {
-            $path = Drupal::service('path.alias_manager')->getSystemPath($match[1]);
-            if (preg_match('!(?:node|book)/(?:view/)?([0-9]+)!i', $path, $match)) {
-              $linknid = $match[1];
-              if ($linknid > 0) {
-                $link = TRUE;
-                $node = node_load($linknid);
-                $linktitle = $node->label();
-              }
-            }
-          }
-        }
       }
       // A tag change occurred, reset counter.
       $tagwords = 0;
@@ -643,36 +585,19 @@ function search_index($sid, $module, $text, $langcode) {
     else {
       // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values
       if ($value != '') {
-        if ($link) {
-          // Check to see if the node link text is its URL. If so, we use the target node title instead.
-          if (preg_match('!^https?://!i', $value)) {
-            $value = $linktitle;
-          }
-        }
         $words = search_index_split($value, $langcode);
         foreach ($words as $word) {
           // Add word to accumulator
           $accum .= $word . ' ';
           // Check wordlength
           if (is_numeric($word) || drupal_strlen($word) >= $minimum_word_size) {
-            // Links score mainly for the target.
-            if ($link) {
-              if (!isset($results[$linknid])) {
-                $results[$linknid] = array();
-              }
-              $results[$linknid][] = $word;
-              // Reduce score of the link caption in the source.
-              $focus *= 0.2;
-            }
-            // Fall-through
-            if (!isset($results[0][$word])) {
-              $results[0][$word] = 0;
+            if (!isset($scored_words[$word])) {
+              $scored_words[$word] = 0;
             }
-            $results[0][$word] += $score * $focus;
-
+            $scored_words[$word] += $score * $focus;
             // Focus is a decaying value in terms of the amount of unique words up to this point.
             // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words.
-            $focus = min(1, .01 + 3.5 / (2 + count($results[0]) * .015));
+            $focus = min(1, .01 + 3.5 / (2 + count($scored_words) * .015));
           }
           $tagwords++;
           // Too many words inside a single tag probably mean a tag was accidentally left open.
@@ -686,21 +611,21 @@ function search_index($sid, $module, $text, $langcode) {
     $tag = !$tag;
   }
 
-  search_reindex($sid, $module, TRUE, $langcode);
+  search_reindex($sid, $type, TRUE, $langcode);
 
   // Insert cleaned up data into dataset
   db_insert('search_dataset')
     ->fields(array(
       'sid' => $sid,
       'langcode' => $langcode,
-      'type' => $module,
+      'type' => $type,
       'data' => $accum,
       'reindex' => 0,
     ))
     ->execute();
 
   // Insert results into search index
-  foreach ($results[0] as $word => $score) {
+  foreach ($scored_words as $word => $score) {
     // If a word already exists in the database, its score gets increased
     // appropriately. If not, we create a new record with the appropriate
     // starting score.
@@ -709,194 +634,31 @@ function search_index($sid, $module, $text, $langcode) {
         'word' => $word,
         'sid' => $sid,
         'langcode' => $langcode,
-        'type' => $module,
+        'type' => $type,
       ))
       ->fields(array('score' => $score))
       ->expression('score', 'score + :score', array(':score' => $score))
       ->execute();
     search_dirty($word);
   }
-  unset($results[0]);
-
-  // Get all previous links from this item.
-  $result = db_query("SELECT nid, caption FROM {search_node_links} WHERE sid = :sid AND type = :type", array(
-    ':sid' => $sid,
-    ':type' => $module
-  ), array('target' => 'slave'));
-  $links = array();
-  foreach ($result as $link) {
-    $links[$link->nid] = $link->caption;
-  }
-
-  // Now store links to nodes.
-  foreach ($results as $nid => $words) {
-    $caption = implode(' ', $words);
-    if (isset($links[$nid])) {
-      if ($links[$nid] != $caption) {
-        // Update the existing link and mark the node for reindexing.
-        db_update('search_node_links')
-          ->fields(array('caption' => $caption))
-          ->condition('sid', $sid)
-          ->condition('type', $module)
-          ->condition('nid', $nid)
-          ->execute();
-        search_touch_node($nid);
-      }
-      // Unset the link to mark it as processed.
-      unset($links[$nid]);
-    }
-    elseif ($sid != $nid || $module != 'node') {
-      // Insert the existing link and mark the node for reindexing, but don't
-      // reindex if this is a link in a node pointing to itself.
-      db_insert('search_node_links')
-        ->fields(array(
-          'caption' => $caption,
-          'sid' => $sid,
-          'type' => $module,
-          'nid' => $nid,
-        ))
-        ->execute();
-      search_touch_node($nid);
-    }
-  }
-  // Any left-over links in $links no longer exist. Delete them and mark the nodes for reindexing.
-  foreach ($links as $nid => $caption) {
-    db_delete('search_node_links')
-      ->condition('sid', $sid)
-      ->condition('type', $module)
-      ->condition('nid', $nid)
-      ->execute();
-    search_touch_node($nid);
-  }
 }
 
 /**
- * Changes a node's changed timestamp to 'now' to force reindexing.
+ * Changes the timestamp on an indexed item to 'now' to force reindexing.
  *
- * @param $nid
- *   The node ID of the node that needs reindexing.
+ * @param $type
+ *   The plugin ID or other machine-readable type of this item.
+ * @param $sid
+ *   An ID number identifying this particular item (e.g., node ID).
  */
-function search_touch_node($nid) {
+function search_mark_for_reindex($type, $sid) {
   db_update('search_dataset')
     ->fields(array('reindex' => REQUEST_TIME))
-    ->condition('type', 'node')
-    ->condition('sid', $nid)
+    ->condition('type', $type)
+    ->condition('sid', $sid)
     ->execute();
 }
 
-/**
- * Implements hook_node_update_index().
- */
-function search_node_update_index(EntityInterface $node) {
-  // Transplant links to a node into the target node.
-  $result = db_query("SELECT caption FROM {search_node_links} WHERE nid = :nid", array(':nid' => $node->id()), array('target' => 'slave'));
-  $output = array();
-  foreach ($result as $link) {
-    $output[] = $link->caption;
-  }
-  if (count($output)) {
-    return '<a>(' . implode(', ', $output) . ')</a>';
-  }
-}
-
-/**
- * Implements hook_node_update().
- */
-function search_node_update(EntityInterface $node) {
-  // Reindex the node when it is updated. The node is automatically indexed
-  // when it is added, simply by being added to the node table.
-  search_touch_node($node->id());
-}
-
-/**
- * Implements hook_comment_insert().
- */
-function search_comment_insert($comment) {
-  // Reindex the node when comments are added.
-  search_touch_node($comment->nid->target_id);
-}
-
-/**
- * Implements hook_comment_update().
- */
-function search_comment_update($comment) {
-  // Reindex the node when comments are changed.
-  search_touch_node($comment->nid->target_id);
-}
-
-/**
- * Implements hook_comment_delete().
- */
-function search_comment_delete($comment) {
-  // Reindex the node when comments are deleted.
-  search_touch_node($comment->nid->target_id);
-}
-
-/**
- * Implements hook_comment_publish().
- */
-function search_comment_publish($comment) {
-  // Reindex the node when comments are published.
-  search_touch_node($comment->nid->target_id);
-}
-
-/**
- * Implements hook_comment_unpublish().
- */
-function search_comment_unpublish($comment) {
-  // Reindex the node when comments are unpublished.
-  search_touch_node($comment->nid->target_id);
-}
-
-/**
- * Extracts a module-specific search option from a search expression.
- *
- * Search options are added using search_expression_insert(), and retrieved
- * using search_expression_extract(). They take the form option:value, and
- * are added to the ordinary keywords in the search expression.
- *
- * @param $expression
- *   The search expression to extract from.
- * @param $option
- *   The name of the option to retrieve from the search expression.
- *
- * @return
- *   The value previously stored in the search expression for option $option,
- *   if any. Trailing spaces in values will not be included.
- */
-function search_expression_extract($expression, $option) {
-  $expression = new SearchExpression($expression);
-  return $expression->extract($option);
-}
-
-/**
- * Adds a module-specific search option to a search expression.
- *
- * Search options are added using search_expression_insert(), and retrieved
- * using search_expression_extract(). They take the form option:value, and
- * are added to the ordinary keywords in the search expression.
- *
- * @param $expression
- *   The search expression to add to.
- * @param $option
- *   The name of the option to add to the search expression.
- * @param $value
- *   The value to add for the option. If present, it will replace any previous
- *   value added for the option. Cannot contain any spaces or | characters, as
- *   these are used as delimiters. If you want to add a blank value $option: to
- *   the search expression, pass in an empty string or a string that is
- *   composed of only spaces. To clear a previously-stored option without
- *   adding a replacement, pass in NULL for $value or omit.
- *
- * @return
- *   The search expression, with any previous value for this option removed, and
- *   a new $option:$value pair added if $value was provided.
- */
-function search_expression_insert($expression, $option, $value = NULL) {
-  $expression = new SearchExpression($expression);
-  return $expression->insert($option, $value)->getExpression();
-}
-
 /**
  * @defgroup search Search interface
  * @{
@@ -906,65 +668,54 @@ function search_expression_insert($expression, $option, $value = NULL) {
  * data. Most of the system is handled by the Search module, so this must be
  * enabled for all of the search features to work.
  *
+ * To be discovered, the plugins must implement
+ * \Drupal\search\Plugin\SearchInterface and be annotated as
+ * \Drupal\search\Annotation\SearchPlugin plugins.
+ *
  * There are three ways to interact with the search system:
  * - Specifically for searching nodes, you can implement
  *   hook_node_update_index() and hook_node_search_result(). However, note that
  *   the search system already indexes all visible output of a node; i.e.,
- *   everything displayed normally by hook_node_view(). This is
+ *   everything displayed normally during node viewing. This is
  *   usually sufficient. You should only use this mechanism if you want
  *   additional, non-visible data to be indexed.
- * - Implement hook_search_info(). This will create a search tab for your
- *   module on the /search page with a simple keyword search form. You will
- *   also need to implement hook_search_execute() to perform the search.
- * - Implement hook_update_index(). This allows your module to use Drupal's
- *   HTML indexing mechanism for searching full text efficiently.
+ * - Define a plugin implementing \Drupal\search\Plugin\SearchInterface and
+ *   annotated as \Drupal\search\Annotation\SearchPlugin. This will create a
+ *   search tab for your plugin on the /search page with a simple keyword
+ *   search form. You will also need to implement the execute() method
+ *   from the interface to perform the search. A base class is provided in
+ *   \Drupal\search\Plugin\SearchPluginBase.
  *
  * If your module needs to provide a more complicated search form, then you
- * need to implement it yourself without hook_search_info(). In that case, you
- * should define it as a local task (tab) under the /search page (e.g.
- * /search/mymodule) so that users can easily find it.
+ * need to implement it yourself. In that case, you may wish to define it as a
+ * local task (tab) under the /search page (e.g. /search/mymodule) so that users
+ * can easily find it.
  */
 
 /**
  * Form constructor for the search form.
  *
+ * @param \Drupal\search\Plugin\SearchInterface $plugin
+ *   A search plugin instance to render the form for.
  * @param $action
  *   Form action. Defaults to "search/$path", where $path is the search path
- *   associated with the module in its hook_search_info(). This will be
- *   run through url().
- * @param $keys
- *   The search string entered by the user, containing keywords for the search.
- * @param $module
- *   The search module to render the form for: a module that implements
- *   hook_search_info(). If not supplied, the default search module is used.
+ *   associated with the plugin in its definition. This will be run through
+ *   url().
  * @param $prompt
  *   Label for the keywords field. Defaults to t('Enter your keywords') if
  *   NULL. Supply '' to omit.
  *
- * @return
- *   A Form API array for the search form.
- *
  * @see search_form_validate()
  * @see search_form_submit()
+ *
+ * @ingroup forms
  */
-function search_form($form, &$form_state, $action = '', $keys = '', $module = NULL, $prompt = NULL) {
-  $module_info = FALSE;
-  if (!$module) {
-    $module_info = search_get_default_module_info();
-  }
-  else {
-    $info = search_get_info();
-    $module_info = isset($info[$module]) ? $info[$module] : FALSE;
-  }
+function search_form($form, &$form_state, SearchInterface $plugin, $action = '', $prompt = NULL) {
 
-  // Sanity check.
-  if (!$module_info) {
-    form_set_error(NULL, t('Search is currently disabled.'), 'error');
-    return $form;
-  }
+  $plugin_info = $plugin->getPluginDefinition();
 
   if (!$action) {
-    $action = 'search/' . $module_info['path'];
+    $action = 'search/' . $plugin_info['path'];
   }
   if (!isset($prompt)) {
     $prompt = t('Enter your keywords');
@@ -973,12 +724,12 @@ function search_form($form, &$form_state, $action = '', $keys = '', $module = NU
   $form['#action'] = url($action);
   // Record the $action for later use in redirecting.
   $form_state['action'] = $action;
-  $form['module'] = array('#type' => 'value', '#value' => $module);
+  $form['plugin_id'] = array('#type' => 'value', '#value' => $plugin->getPluginId());
   $form['basic'] = array('#type' => 'container', '#attributes' => array('class' => array('container-inline')));
   $form['basic']['keys'] = array(
     '#type' => 'search',
     '#title' => $prompt,
-    '#default_value' => $keys,
+    '#default_value' => $plugin->getKeywords(),
     '#size' => $prompt ? 40 : 20,
     '#maxlength' => 255,
   );
@@ -986,35 +737,69 @@ function search_form($form, &$form_state, $action = '', $keys = '', $module = NU
   // that hook into the basic search form.
   $form['basic']['processed_keys'] = array('#type' => 'value', '#value' => '');
   $form['basic']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
+  // Make sure the default validate and submit handlers are added.
+  $form['#validate'][] = 'search_form_validate';
+  $form['#submit'][] = 'search_form_submit';
+  // Allow the plugin to add to or alter the search form.
+  $plugin->searchFormAlter($form, $form_state);
 
   return $form;
 }
 
 /**
- * Performs a search by calling hook_search_execute().
+ * Form constructor for the search block's search box.
  *
- * @param $keys
- *   Keyword query to search on.
- * @param $module
- *   Search module to search.
- * @param $conditions
- *   Optional array of additional search conditions.
+ * @param $form_id
+ *   The unique string identifying the desired form.
  *
- * @return
- *   Renderable array of search results. No return value if $keys are not
- *   supplied or if the given search module is not active.
+ * @see search_box_form_submit()
+ *
+ * @ingroup forms
  */
-function search_data($keys, $module, $conditions = NULL) {
-  $results = module_invoke($module, 'search_execute', $keys, $conditions);
-  if (Drupal::moduleHandler()->implementsHook($module, 'search_page')) {
-    return module_invoke($module, 'search_page', $results);
+function search_box($form, &$form_state, $form_id) {
+  $form[$form_id] = array(
+    '#type' => 'search',
+    '#title' => t('Search'),
+    '#title_display' => 'invisible',
+    '#size' => 15,
+    '#default_value' => '',
+    '#attributes' => array('title' => t('Enter the terms you wish to search for.')),
+  );
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
+  $form['#submit'][] = 'search_box_form_submit';
+
+  return $form;
+}
+
+/**
+ * Form submission handler for search_box().
+ */
+function search_box_form_submit($form, &$form_state) {
+  // The search form relies on control of the redirect destination for its
+  // functionality, so we override any static destination set in the request.
+  // See http://drupal.org/node/292565.
+  if (isset($_GET['destination'])) {
+    unset($_GET['destination']);
+  }
+
+  // Check to see if the form was submitted empty.
+  // If it is empty, display an error message.
+  // (This method is used instead of setting #required to TRUE for this field
+  // because that results in a confusing error message.  It would say a plain
+  // "field is required" because the search keywords field has no title.
+  // The error message would also complain about a missing #title field.)
+  if ($form_state['values']['search_block_form'] == '') {
+    form_set_error('keys', t('Please enter some keywords.'));
+  }
+
+  $form_id = $form['form_id']['#value'];
+  $info = search_get_default_plugin_info();
+  if ($info) {
+    $form_state['redirect'] = 'search/' . $info['path'] . '/' . trim($form_state['values'][$form_id]);
   }
   else {
-    return array(
-      '#theme' => 'search_results',
-      '#results' => $results,
-      '#module' => $module,
-    );
+    form_set_error(NULL, t('Search is currently disabled.'), 'error');
   }
 }
 
diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc
index a39a8153ec94e321dbfc4c942fb0ab6da43171e1..804e4be34a9b74325fb3a094dbe49a98be270d94 100644
--- a/core/modules/search/search.pages.inc
+++ b/core/modules/search/search.pages.inc
@@ -11,12 +11,12 @@
 /**
  * Page callback: Presents the search form and/or search results.
  *
- * @param $module
- *   Search module to use for the search.
+ * @param $plugin_id
+ *   Search plugin_id to use for the search.
  * @param $keys
  *   Keywords to use for the search.
  */
-function search_view($module = NULL, $keys = '') {
+function search_view($plugin_id = NULL, $keys = '') {
   $info = FALSE;
   $keys = trim($keys);
   // Also try to pull search keywords out of the $_REQUEST variable to
@@ -25,18 +25,19 @@ function search_view($module = NULL, $keys = '') {
     $keys = trim($_REQUEST['keys']);
   }
 
-  if (!empty($module)) {
-    $active_module_info = search_get_info();
-    if (isset($active_module_info[$module])) {
-      $info = $active_module_info[$module];
+  $manager = Drupal::service('plugin.manager.search');
+  if (!empty($plugin_id)) {
+    $active_plugin_info = $manager->getActiveDefinitions();
+    if (isset($active_plugin_info[$plugin_id])) {
+      $info = $active_plugin_info[$plugin_id];
     }
   }
 
-  if (empty($info)) {
-    // No path or invalid path: find the default module. Note that if there
-    // are no enabled search modules, this function should never be called,
+  if (empty($plugin_id) || empty($info)) {
+    // No path or invalid path: find the default plugin. Note that if there
+    // are no enabled search plugins, this function should never be called,
     // since hook_menu() would not have defined any search paths.
-    $info = search_get_default_module_info();
+    $info = search_get_default_plugin_info();
     // Redirect from bare /search or an invalid path to the default search path.
     $path = 'search/' . $info['path'];
     if ($keys) {
@@ -44,31 +45,28 @@ function search_view($module = NULL, $keys = '') {
     }
     return new RedirectResponse(url($path, array('absolute' => TRUE)));
   }
-
+  $plugin = $manager->createInstance($plugin_id);
+  $request = Drupal::request();
+  $plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
   // Default results output is an empty string.
   $results = array('#markup' => '');
   // Process the search form. Note that if there is $_POST data,
-  // search_form_submit() will cause a redirect to search/[module path]/[keys],
+  // search_form_submit() will cause a redirect to search/[path]/[keys],
   // which will get us back to this page callback. In other words, the search
   // form submits with POST but redirects to GET. This way we can keep
   // the search query URL clean as a whistle.
   if (empty($_POST['form_id']) || $_POST['form_id'] != 'search_form') {
-    $conditions =  NULL;
-    if (isset($info['conditions_callback'])) {
-      // Build an optional array of more search conditions.
-      $conditions = call_user_func($info['conditions_callback'], $keys);
-    }
     // Only search if there are keywords or non-empty conditions.
-    if ($keys || !empty($conditions)) {
+    if ($plugin->isSearchExecutable()) {
       // Log the search keys.
       watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys));
 
       // Collect the search results.
-      $results = search_data($keys, $info['module'], $conditions);
+      $results = $plugin->buildResults();
     }
   }
   // The form may be altered based on whether the search was run.
-  $build['search_form'] = drupal_get_form('search_form', NULL, $keys, $info['module']);
+  $build['search_form'] = drupal_get_form('search_form', $plugin);
   $build['search_results'] = $results;
 
   return $build;
@@ -82,26 +80,25 @@ function search_view($module = NULL, $keys = '') {
  * @param array $variables
  *   An array with the following elements:
  *   - results: Search results array.
- *   - module: Module the search results came from (module implementing
- *     hook_search_info()).
+ *   - plugin_id: Plugin the search results came from.
  */
 function template_preprocess_search_results(&$variables) {
   $variables['search_results'] = '';
-  if (!empty($variables['module'])) {
-    $variables['module'] = check_plain($variables['module']);
+  if (!empty($variables['plugin_id'])) {
+    $variables['plugin_id'] = check_plain($variables['plugin_id']);
   }
   foreach ($variables['results'] as $result) {
     $variables['search_results'][] = array(
       '#theme' => 'search_result',
       '#result' => $result,
-      '#module' => $variables['module'],
+      '#plugin_id' => $variables['plugin_id'],
     );
   }
   $variables['pager'] = array('#theme' => 'pager');
   // @todo Revisit where this help text is added, see also
   //   http://drupal.org/node/1918856.
   $variables['help'] = search_help('search#noresults', drupal_help_arg());
-  $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module'];
+  $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['plugin_id'];
 }
 
 /**
@@ -112,8 +109,7 @@ function template_preprocess_search_results(&$variables) {
  * @param array $variables
  *   An array with the following elements:
  *   - result: Individual search result.
- *   - module: Module the search results came from (module implementing
- *     hook_search_info()).
+ *   - plugin_id: Plugin the search results came from.
  *   - title_prefix: Additional output populated by modules, intended to be
  *     displayed in front of the main title tag that appears in the template.
  *   - title_suffix: Additional output populated by modules, intended to be
@@ -133,8 +129,8 @@ function template_preprocess_search_result(&$variables) {
   }
 
   $info = array();
-  if (!empty($result['module'])) {
-    $info['module'] = check_plain($result['module']);
+  if (!empty($result['plugin_id'])) {
+    $info['plugin_id'] = check_plain($result['plugin_id']);
   }
   if (!empty($result['user'])) {
     $info['user'] = $result['user'];
@@ -150,7 +146,7 @@ function template_preprocess_search_result(&$variables) {
   // Provide separated and grouped meta information..
   $variables['info_split'] = $info;
   $variables['info'] = implode(' - ', $info);
-  $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module'];
+  $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['plugin_id'];
 }
 
 /**
diff --git a/core/modules/search/search.services.yml b/core/modules/search/search.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..22dc7f2e1ec65ba07791f87298c4917c486cb020
--- /dev/null
+++ b/core/modules/search/search.services.yml
@@ -0,0 +1,4 @@
+services:
+  plugin.manager.search:
+    class: Drupal\search\SearchPluginManager
+    arguments: ['@container.namespaces', '@config.factory']
diff --git a/core/modules/search/templates/search-result.html.twig b/core/modules/search/templates/search-result.html.twig
index 42cd6d4a063fc294ad9d69a157a95150087983d9..281f8b121494d5e2b19591f1d06cc34151f0611d 100644
--- a/core/modules/search/templates/search-result.html.twig
+++ b/core/modules/search/templates/search-result.html.twig
@@ -13,8 +13,8 @@
  * - snippet: A small preview of the result. Does not apply to user searches.
  * - info: String of all the meta information ready for print. Does not apply
  *   to user searches.
- * - module: The machine-readable name of the module (tab) being searched, such
- *   as "node" or "user".
+ * - plugin_id: The machine-readable name of the plugin being executed,such
+ *   as "node_search" or "user_search".
  * - title_prefix: Additional output populated by modules, intended to be
  *   displayed in front of the main title tag that appears in the template.
  * - title_suffix: Additional output populated by modules, intended to be
diff --git a/core/modules/search/templates/search-results.html.twig b/core/modules/search/templates/search-results.html.twig
index a43d1620717b02b3da52f0d8ed2ec8b4cb18717a..4c14be7832d26481a4587fe5753b29b374c0d699 100644
--- a/core/modules/search/templates/search-results.html.twig
+++ b/core/modules/search/templates/search-results.html.twig
@@ -13,8 +13,8 @@
  * Available variables:
  * - search_results: All results as it is rendered through
  *   search-result.html.twig.
- * - module: The machine-readable name of the module (tab) being searched, such
- *   as 'node' or 'user'.
+ * - plugin_id: The machine-readable name of the plugin being executed, such
+ *   as 'node_search' or 'user_search'.
  * - pager: The pager next/prev links to display, if any.
  * - help: HTML for help text to display when no results are found.
  *
@@ -25,7 +25,7 @@
 #}
 {% if search_results %}
   <h2>{{ 'Search results'|t }}</h2>
-  <ol class="search-results {{ module }}-results">
+  <ol class="search-results {{ plugin_id }}-results">
     {{ search_results }}
   </ol>
   {{ pager }}
diff --git a/core/modules/search/tests/Drupal/search/Tests/SearchExpressionTest.php b/core/modules/search/tests/Drupal/search/Tests/SearchExpressionTest.php
deleted file mode 100644
index 27f59ed200795e956cb7e49217bab98ec89d0a97..0000000000000000000000000000000000000000
--- a/core/modules/search/tests/Drupal/search/Tests/SearchExpressionTest.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\search\Tests\SearchExpressionTest.
- */
-
-namespace Drupal\search\Tests;
-
-use Drupal\search\SearchExpression;
-use Drupal\Tests\UnitTestCase;
-
-/**
- * Tests the search expression class.
- *
- * @see \Drupal\search\SearchExpression
- */
-class SearchExpressionTest extends UnitTestCase {
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Search expression insert/extract',
-      'description' => 'Tests the search expression class.',
-      'group' => 'Search',
-    );
-  }
-
-
-  /**
-   * Provides data for the search expression tests.
-   *
-   * @return array
-   *   An array of values passed to the test methods.
-   */
-  public function dataProvider() {
-    $cases = array(
-      // Normal case.
-      array('foo', 'bar', 'foo:bar', 'bar'),
-      // Empty value: shouldn't insert.
-      array('foo', NULL, '', NULL),
-      // Space as value: should insert but retrieve empty string.
-      array('foo', ' ', 'foo:', ''),
-      // Empty string as value: should insert but retrieve empty string.
-      array('foo', '', 'foo:', ''),
-      // String zero as value: should insert.
-      array('foo', '0', 'foo:0', '0'),
-      // Numeric zero as value: should insert.
-      array('foo', 0, 'foo:0', '0'),
-    );
-    return $cases;
-  }
-
-  /**
-   * Tests the search expression methods.
-   *
-   * @dataProvider dataProvider
-   */
-  public function testInsertExtract($case_0, $case_1, $case_2, $case_3) {
-    $base_expression = 'mykeyword';
-    // Build an array of option, value, what should be in the expression, what
-    // should be retrieved from expression.
-
-    $after_insert = new SearchExpression($base_expression);
-    $after_insert->insert($case_0, $case_1);
-
-    if (empty($case_2)) {
-      $this->assertEquals($base_expression, $after_insert->getExpression(), 'Empty insert does change expression.');
-    }
-    else {
-      $this->assertEquals($base_expression . ' ' . $case_2, $after_insert->getExpression(), 'Insert added incorrect expression.');
-    }
-
-    $retrieved = $after_insert->extract($case_0);
-
-    if (!isset($case_3)) {
-      $this->assertFalse(isset($retrieved), 'Empty retrieval results in unset value.');
-    }
-    else {
-      $this->assertEquals($case_3, $retrieved, 'Value is retrieved.');
-    }
-
-    $after_clear = $after_insert->insert($case_0);
-    $this->assertEquals($base_expression, $after_clear->getExpression(), 'After clearing, base expression is not restored.');
-
-    $cleared = $after_clear->extract($case_0);
-    $this->assertFalse(isset($cleared), 'After clearing, value could be retrieved.');
-  }
-
-}
diff --git a/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php b/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b40e45c262f53211b44aff1995a87e15741c09d
--- /dev/null
+++ b/core/modules/search/tests/modules/search_extra_type/lib/Drupal/search_extra_type/Plugin/Search/SearchExtraTypeSearch.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch.
+ */
+
+namespace Drupal\search_extra_type\Plugin\Search;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Config\Config;
+use Drupal\search\Plugin\SearchPluginBase;
+use Drupal\search\Annotation\SearchPlugin;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Executes a keyword search against the search index.
+ *
+ * @SearchPlugin(
+ *   id = "search_extra_type_search",
+ *   title = @Translation("Dummy search type"),
+ *   path = "dummy_path"
+ * )
+ */
+class SearchExtraTypeSearch extends SearchPluginBase {
+
+  /**
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $configSettings;
+
+  /**
+   * {@inheritdoc}
+   */
+  static public function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static(
+      $container->get('config.factory')->get('search_extra_type.settings'),
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  /**
+   * Creates a SearchExtraTypeSearch object.
+   *
+   * @param Config $config_settings
+   *   The extra config settings.
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param array $plugin_definition
+   *   The plugin implementation definition.
+   */
+  public function __construct(Config $config_settings, array $configuration, $plugin_id, array $plugin_definition) {
+    $this->configSettings = $config_settings;
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSearch($keywords, array $parameters, array $attributes) {
+    if (empty($parameters['search_conditions'])) {
+      $parameters['search_conditions'] = '';
+    }
+    parent::setSearch($keywords, $parameters, $attributes);
+  }
+
+  /**
+   * Verifies if the given parameters are valid enough to execute a search for.
+   *
+   * @return bool
+   *   A true or false depending on the implementation.
+   */
+  public function isSearchExecutable() {
+    return (bool) ($this->keywords || !empty($this->searchParameters['search_conditions']));
+  }
+
+  /**
+   * Execute the search.
+   *
+   * This is a dummy search, so when search "executes", we just return a dummy
+   * result containing the keywords and a list of conditions.
+   *
+   * @return array
+   *   A structured list of search results
+   */
+  public function execute() {
+    $results = array();
+    if (!$this->isSearchExecutable()) {
+      return $results;
+    }
+    return array(
+      array(
+        'link' => url('node'),
+        'type' => 'Dummy result type',
+        'title' => 'Dummy title',
+        'snippet' => "Dummy search snippet to display. Keywords: {$this->keywords}\n\nConditions: " . print_r($this->searchParameters, TRUE),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildResults() {
+    $results = $this->execute();
+    $output['prefix']['#markup'] = '<h2>Test page text is here</h2> <ol class="search-results">';
+
+    foreach ($results as $entry) {
+      $output[] = array(
+        '#theme' => 'search_result',
+        '#result' => $entry,
+        '#plugin_id' => 'search_extra_type_search',
+      );
+    }
+    $output['suffix']['#markup'] = '</ol>' . theme('pager');
+
+    return $output;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addToAdminForm(array &$form, array &$form_state) {
+    // Output form for defining rank factor weights.
+    $form['extra_type_settings'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Extra type settings'),
+      '#tree' => TRUE,
+    );
+
+    $form['extra_type_settings']['boost'] = array(
+      '#type' => 'select',
+      '#title' => t('Boost method'),
+      '#options' => array(
+        'bi' => t('Bistromathic'),
+        'ii' => t('Infinite Improbability'),
+      ),
+      '#default_value' => $this->configSettings->get('boost'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitAdminForm(array &$form, array &$form_state) {
+    $this->configSettings
+      ->set('boost', $form_state['values']['extra_type_settings']['boost'])
+      ->save();
+  }
+
+}
diff --git a/core/modules/search/tests/modules/search_extra_type/search_extra_type.module b/core/modules/search/tests/modules/search_extra_type/search_extra_type.module
index b405e9a114a495073307c2cf6d1c8ebce57ad890..46007088df4bd4ac46221e1a74d4b7d5034a2277 100644
--- a/core/modules/search/tests/modules/search_extra_type/search_extra_type.module
+++ b/core/modules/search/tests/modules/search_extra_type/search_extra_type.module
@@ -5,105 +5,3 @@
  * Dummy module implementing a search type for search module testing.
  */
 
-/**
- * Implements hook_search_info().
- */
-function search_extra_type_search_info() {
-  return array(
-    'title' => 'Dummy search type',
-    'path' => 'dummy_path',
-    'conditions_callback' => 'search_extra_type_conditions',
-  );
-}
-
-/**
- * Implements callback_search_conditions().
- *
- * Tests the conditions callback for hook_search_info().
- */
-function search_extra_type_conditions() {
-  $conditions = array();
-
-  if (!empty($_REQUEST['search_conditions'])) {
-    $conditions['search_conditions'] = $_REQUEST['search_conditions'];
-  }
-  return $conditions;
-}
-
-/**
- * Implements hook_search_execute().
- *
- * This is a dummy search, so when search "executes", we just return a dummy
- * result containing the keywords and a list of conditions.
- */
-function search_extra_type_search_execute($keys = NULL, $conditions = NULL) {
-  if (!$keys) {
-    $keys = '';
-  }
-  return array(
-    array(
-      'link' => url('node'),
-      'type' => 'Dummy result type',
-      'title' => 'Dummy title',
-      'snippet' => "Dummy search snippet to display. Keywords: {$keys}\n\nConditions: " . print_r($conditions, TRUE),
-    ),
-  );
-}
-
-/**
- * Implements hook_search_page().
- *
- * Adds some text to the search page so we can verify that it runs.
- */
-function search_extra_type_search_page($results) {
-  $output['prefix']['#markup'] = '<h2>Test page text is here</h2> <ol class="search-results">';
-
-  foreach ($results as $entry) {
-    $output[] = array(
-      '#theme' => 'search_result',
-      '#result' => $entry,
-      '#module' => 'search_extra_type',
-    );
-  }
-  $pager = array(
-    '#theme' => 'pager',
-  );
-  $output['suffix']['#markup'] = '</ol>' . drupal_render($pager);
-
-  return $output;
-}
-
-/**
- * Implements hook_search_admin().
- */
-function search_extra_type_search_admin() {
-  // Output form for defining rank factor weights.
-  $form['extra_type_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Extra type settings'),
-    '#tree' => TRUE,
-  );
-
-  $form['extra_type_settings']['boost'] = array(
-    '#type' => 'select',
-    '#title' => t('Boost method'),
-    '#options' => array(
-      'bi' => t('Bistromathic'),
-      'ii' => t('Infinite Improbability'),
-    ),
-    '#default_value' => Drupal::config('search_extra_type.settings')->get('boost'),
-  );
-
-  $form['#submit'][] = 'search_extra_type_admin_submit';
-
-  return $form;
-}
-
-/**
- * Form API callback: Save admin settings
- */
-function search_extra_type_admin_submit($form, &$form_state) {
-  Drupal::config('search_extra_type.settings')
-    ->set('boost', $form_state['values']['extra_type_settings']['boost'])
-    ->save();
-}
diff --git a/core/modules/search/tests/upgrade/drupal-7.search.database.php b/core/modules/search/tests/upgrade/drupal-7.search.database.php
new file mode 100644
index 0000000000000000000000000000000000000000..5dade9d4341e82b243843420166a17e9478491f5
--- /dev/null
+++ b/core/modules/search/tests/upgrade/drupal-7.search.database.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Database additions for \Drupal\search\Tests\SearchUpgradePathTest.
+ *
+ * This dump only contains data and schema components relevant for search
+ * functionality. The bare.standard_all.database.php file is imported before
+ * this dump, so the two form the database structure expected in tests.
+ */
+
+// Set user as the only active and default search.
+db_insert('variable')
+  ->fields(array(
+    'name',
+    'value',
+  ))
+  ->values(array(
+    'name' => 'search_active_modules',
+    'value'=> 'a:1:{s:4:"user";s:4:"user";}',
+  ))
+  ->values(array(
+    'name' => 'search_default_module',
+    'value'=> 's:4:"user";',
+  ))
+  ->execute();
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php
new file mode 100644
index 0000000000000000000000000000000000000000..52ed50b07a3986ca5559806060a7851c98cc94b3
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php
@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Plugin\Search\UserSearch.
+ */
+
+namespace Drupal\user\Plugin\Search;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TypedData\AccessibleInterface;
+use Drupal\search\Annotation\SearchPlugin;
+use Drupal\search\Plugin\SearchPluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Executes a keyword search for users against the {users} database table.
+ *
+ * @SearchPlugin(
+ *   id = "user_search",
+ *   title = @Translation("Users"),
+ *   path = "user"
+ * )
+ */
+class UserSearch extends SearchPluginBase implements AccessibleInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * {@inheritdoc}
+   */
+  static public function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static(
+      $container->get('database'),
+      $container->get('plugin.manager.entity'),
+      $container->get('module_handler'),
+      $container->get('request'),
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  /**
+   * Creates a UserSearch object.
+   *
+   * @param Connection $database
+   *   The database connection.
+   * @param EntityManager $entity_manager
+   *   The entity manager.
+   * @param ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param array $plugin_definition
+   *   The plugin implementation definition.
+   */
+  public function __construct(Connection $database, EntityManager $entity_manager, ModuleHandlerInterface $module_handler, Request $request, array $configuration, $plugin_id, array $plugin_definition) {
+    $this->database = $database;
+    $this->entityManager = $entity_manager;
+    $this->moduleHandler = $module_handler;
+    $this->request = $request;
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($operation = 'view', AccountInterface $account = NULL) {
+    return !empty($account) && $account->hasPermission('access user profiles');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $results = array();
+    if (!$this->isSearchExecutable()) {
+      return $results;
+    }
+    $keys = $this->keywords;
+    // Replace wildcards with MySQL/PostgreSQL wildcards.
+    $keys = preg_replace('!\*+!', '%', $keys);
+    $query = $this->database
+      ->select('users')
+      ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
+    $query->fields('users', array('uid'));
+    $user_account = $this->request->attributes->get('_account');
+    if ($user_account->hasPermission('administer users')) {
+      // Administrators can also search in the otherwise private email field, and
+      // they don't need to be restricted to only active users.
+      $query->fields('users', array('mail'));
+      $query->condition($query->orConditionGroup()
+        ->condition('name', '%' . $this->database->escapeLike($keys) . '%', 'LIKE')
+        ->condition('mail', '%' . $this->database->escapeLike($keys) . '%', 'LIKE')
+      );
+    }
+    else {
+      // Regular users can only search via usernames, and we do not show them
+      // blocked accounts.
+      $query->condition('name', '%' . $this->database->escapeLike($keys) . '%', 'LIKE')
+        ->condition('status', 1);
+    }
+    $uids = $query
+      ->limit(15)
+      ->execute()
+      ->fetchCol();
+    $accounts = $this->entityManager->getStorageController('user')->loadMultiple($uids);
+
+    foreach ($accounts as $account) {
+      $result = array(
+        'title' => $account->getUsername(),
+        'link' => url('user/' . $account->id(), array('absolute' => TRUE)),
+      );
+      if ($user_account->hasPermission('administer users')) {
+        $result['title'] .= ' (' . $account->getEmail() . ')';
+      }
+      $results[] = $result;
+    }
+
+    return $results;
+  }
+
+}
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 53eaf99aaabedb2d75390f5edb06f76b48769c7f..1f50de03e24f1764a3dfbdc54888d02f1a3b3b5b 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1,7 +1,6 @@
 <?php
 
 use Drupal\Component\Utility\Crypt;
-use Drupal\Core\Database\Query\SelectInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\entity\Entity\EntityDisplay;
@@ -492,66 +491,6 @@ function user_permission() {
   );
 }
 
-/**
- * Implements hook_search_info().
- */
-function user_search_info() {
-  return array(
-    'title' => 'Users',
-  );
-}
-
-/**
- * Implements hook_search_access().
- */
-function user_search_access() {
-  return user_access('access user profiles');
-}
-
-/**
- * Implements hook_search_execute().
- */
-function user_search_execute($keys = NULL, $conditions = NULL) {
-  // Replace wildcards with MySQL/PostgreSQL wildcards.
-  $keys = preg_replace('!\*+!', '%', $keys);
-  $query = db_select('users')
-    ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
-  $query->fields('users', array('uid'));
-  if (user_access('administer users')) {
-    // Administrators can also search in the otherwise private email field, and
-    // they don't need to be restricted to only active users.
-    $query->fields('users', array('mail'));
-    $query->condition(db_or()->
-      condition('name', '%' . db_like($keys) . '%', 'LIKE')->
-      condition('mail', '%' . db_like($keys) . '%', 'LIKE'));
-  }
-  else {
-    // Regular users can only search via usernames, and we do not show them
-    // blocked accounts.
-    $query->condition('name', '%' . db_like($keys) . '%', 'LIKE')
-      ->condition('status', 1);
-  }
-  $uids = $query
-    ->limit(15)
-    ->execute()
-    ->fetchCol();
-  $accounts = user_load_multiple($uids);
-
-  $results = array();
-  foreach ($accounts as $account) {
-    $result = array(
-      'title' => user_format_name($account),
-      'link' => url('user/' . $account->id(), array('absolute' => TRUE)),
-    );
-    if (user_access('administer users')) {
-      $result['title'] .= ' (' . $account->getEmail() . ')';
-    }
-    $results[] = $result;
-  }
-
-  return $results;
-}
-
 /**
  * Implements hook_user_view().
  */
diff --git a/core/modules/views/config/views.view.backlinks.yml b/core/modules/views/config/views.view.backlinks.yml
deleted file mode 100644
index ec0bb3dc465677e6d1343759ec287965db49b905..0000000000000000000000000000000000000000
--- a/core/modules/views/config/views.view.backlinks.yml
+++ /dev/null
@@ -1,225 +0,0 @@
-base_field: nid
-base_table: node
-core: '8'
-description: 'A list of other content items which have a link to the content item.'
-status: '0'
-display:
-  default:
-    id: default
-    display_title: Master
-    display_plugin: default
-    position: '1'
-    display_options:
-      query:
-        type: views_query
-        options:
-          query_comment: '0'
-          disable_sql_rewrite: '0'
-          distinct: '0'
-          slave: '0'
-          query_tags: {  }
-      access:
-        type: none
-        options: {  }
-      cache:
-        type: none
-        options: {  }
-      exposed_form:
-        type: basic
-        options:
-          submit_button: Apply
-          reset_button: '0'
-          reset_button_label: Reset
-          exposed_sorts_label: 'Sort by'
-          expose_sort_order: '1'
-          sort_asc_label: Asc
-          sort_desc_label: Desc
-      pager:
-        type: mini
-        options:
-          items_per_page: '30'
-          offset: '0'
-          id: '0'
-          total_pages: ''
-          expose:
-            items_per_page: '0'
-            items_per_page_label: 'Items per page'
-            items_per_page_options: '5, 10, 20, 40, 60'
-            items_per_page_options_all: '0'
-            items_per_page_options_all_label: '- All -'
-            offset: '0'
-            offset_label: Offset
-          tags:
-            previous: ‹‹
-            next: ››
-      empty:
-        text:
-          id: area
-          table: views
-          field: area
-          empty: '0'
-          content: 'No backlinks found.'
-          format: '1'
-          plugin_id: text
-          relationship: none
-          group_type: group
-          admin_label: ''
-          tokenize: '0'
-          provider: views
-      fields:
-        title:
-          id: title
-          table: node_field_data
-          field: title
-          label: ''
-          link_to_node: '1'
-          plugin_id: node
-          relationship: none
-          group_type: group
-          admin_label: ''
-          exclude: '0'
-          alter:
-            alter_text: '0'
-            text: ''
-            make_link: '0'
-            path: ''
-            absolute: '0'
-            external: '0'
-            replace_spaces: '0'
-            path_case: none
-            trim_whitespace: '0'
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: '0'
-            max_length: ''
-            word_boundary: '1'
-            ellipsis: '1'
-            more_link: '0'
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: '0'
-            trim: '0'
-            preserve_tags: ''
-            html: '0'
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: '1'
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: '1'
-          empty: ''
-          hide_empty: '0'
-          empty_zero: '0'
-          hide_alter_empty: '1'
-          provider: node
-      arguments:
-        nid:
-          id: nid
-          table: search_node_links_to
-          field: nid
-          default_action: 'not found'
-          title_enable: '1'
-          title: 'Pages that link to %1'
-          default_argument_type: fixed
-          summary:
-            format: default_summary
-          specify_validation: '1'
-          validate:
-            type: node
-          plugin_id: node_nid
-          provider: node
-      filters:
-        status:
-          id: status
-          table: node_field_data
-          field: status
-          value: '1'
-          group: '0'
-          expose:
-            operator: '0'
-          plugin_id: boolean
-          provider: views
-      style:
-        type: html_list
-        options:
-          type: ol
-          grouping: {  }
-          row_class: ''
-          default_row_class: '1'
-          row_class_special: '1'
-          uses_fields: '0'
-          class: ''
-          wrapper_class: item-list
-      row:
-        type: fields
-        options:
-          inline: {  }
-          separator: ''
-          hide_empty: '0'
-          default_field_elements: '1'
-      header: {  }
-      footer: {  }
-      relationships: {  }
-      sorts: {  }
-  page_1:
-    id: page_1
-    display_title: Page
-    display_plugin: page
-    position: '2'
-    display_options:
-      query:
-        type: views_query
-        options: {  }
-      path: node/%/backlinks
-      menu:
-        type: tab
-        title: 'What links here'
-        weight: '0'
-  block_1:
-    id: block
-    display_title: 'What links here'
-    display_plugin: block
-    position: '3'
-    display_options:
-      query:
-        type: views_query
-        options: {  }
-        defaults:
-          use_more: '0'
-          style_plugin: '0'
-          style_options: '0'
-          row_plugin: '0'
-          row_options: '0'
-          arguments: '0'
-      use_more: '1'
-      arguments:
-        nid:
-          id: nid
-          table: search_node_links_to
-          field: nid
-          default_action: default
-          title_enable: '1'
-          title: 'What links here'
-          default_argument_type: node
-          summary:
-            format: default_summary
-          specify_validation: '1'
-          validate:
-            type: node
-          plugin_type: node_nid
-          provider: node
-      style:
-        type: html_list
-      row:
-        type: fields
-label: Backlinks
-module: search
-id: backlinks
-tag: default
-langcode: en