diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 856c424a1305b57acbfd560729959e6d41103fa2..6043d112b7f492f6042a94ade705cb5f34373954 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -480,6 +480,7 @@ function menu_get_item($path = NULL, $router_item = NULL) {
     if (\Drupal::state()->get('menu_rebuild_needed') || !\Drupal::state()->get('menu.masks')) {
       menu_router_rebuild();
       \Drupal::service('router.builder')->rebuild();
+      \Drupal::cache()->deleteTags(array('local_task' => 1));
     }
     $original_map = arg(NULL, $path);
 
diff --git a/core/lib/Drupal/Core/Config/Entity/DraggableListController.php b/core/lib/Drupal/Core/Config/Entity/DraggableListController.php
index 8e61eb4a2e7b7636bf05edfb69f5da51e970f74b..91a1dcee2ebe31eedea2b6d2362922731791578c 100644
--- a/core/lib/Drupal/Core/Config/Entity/DraggableListController.php
+++ b/core/lib/Drupal/Core/Config/Entity/DraggableListController.php
@@ -39,6 +39,13 @@ abstract class DraggableListController extends ConfigEntityListController implem
    */
   protected $weightKey = FALSE;
 
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
   /**
    * {@inheritdoc}
    */
@@ -88,7 +95,7 @@ public function buildRow(EntityInterface $entity) {
    */
   public function render() {
     if (!empty($this->weightKey)) {
-      return drupal_get_form($this);
+      return $this->formBuilder()->getForm($this);
     }
     return parent::render();
   }
@@ -149,4 +156,17 @@ public function submitForm(array &$form, array &$form_state) {
     }
   }
 
+  /**
+   * Returns the form builder.
+   *
+   * @return \Drupal\Core\Form\FormBuilderInterface
+   *   The form builder.
+   */
+  protected function formBuilder() {
+    if (!$this->formBuilder) {
+      $this->formBuilder = \Drupal::formBuilder();
+    }
+    return $this->formBuilder;
+  }
+
 }
diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml
index f2a82960522b0f64fd46561ae53b8eafc0dd865d..506049be24a4db8b7f42bcd1919caa587f7f1f36 100644
--- a/core/modules/node/config/schema/node.schema.yml
+++ b/core/modules/node/config/schema/node.schema.yml
@@ -75,3 +75,15 @@ node.settings.node:
     submitted:
       type: boolean
       label: 'Display setting for author and date Submitted by post information'
+
+# Plugin \Drupal\node\Plugin\Search\NodeSearch
+search.plugin.node_search:
+  type: mapping
+  label: 'Content search'
+  mapping:
+    rankings:
+      type: sequence
+      label: 'Content ranking'
+      sequence:
+        - type: integer
+          label: 'Influence'
diff --git a/core/modules/node/config/search.page.node_search.yml b/core/modules/node/config/search.page.node_search.yml
new file mode 100644
index 0000000000000000000000000000000000000000..597f4e3d8e1581495fb885150047d278e12b9d3f
--- /dev/null
+++ b/core/modules/node/config/search.page.node_search.yml
@@ -0,0 +1,10 @@
+id: node_search
+label: Content
+uuid: 25687eeb-4bb5-469c-ad05-5eb24cd7012c
+status: true
+langcode: en
+path: node
+weight: -10
+plugin: node_search
+configuration:
+  rankings: {  }
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php
index 573447d8a747c60170392e586c7264d42e6d4932..fb0538a861d94c3a752a5732d2e99a800ca174f4 100644
--- a/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php
+++ b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php
@@ -15,13 +15,11 @@
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\KeyValueStore\StateInterface;
 use Drupal\Core\Language\Language;
-use Drupal\Core\Plugin\PluginFormInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Access\AccessibleInterface;
 use Drupal\Core\Database\Query\Condition;
-use Drupal\search\Plugin\SearchPluginBase;
+use Drupal\search\Plugin\ConfigurableSearchPluginBase;
 use Drupal\search\Plugin\SearchIndexingInterface;
-
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -29,11 +27,10 @@
  *
  * @SearchPlugin(
  *   id = "node_search",
- *   title = @Translation("Content"),
- *   path = "node"
+ *   title = @Translation("Content")
  * )
  */
-class NodeSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface, PluginFormInterface {
+class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInterface, SearchIndexingInterface {
 
   /**
    * A database connection object.
@@ -77,6 +74,13 @@ class NodeSearch extends SearchPluginBase implements AccessibleInterface, Search
    */
   protected $account;
 
+  /**
+   * An array of additional rankings from hook_ranking().
+   *
+   * @var array
+   */
+  protected $rankings;
+
   /**
    * The list of options and info for advanced search filters.
    *
@@ -261,17 +265,17 @@ public function execute() {
   }
 
   /**
-   * Gathers the rankings from the the hook_ranking() implementations.
+   * Adds the configured rankings to the search query.
    *
    * @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')) {
+    if ($ranking = $this->getRankings()) {
       $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 (isset($this->configuration['rankings'][$rank]) && !empty($this->configuration['rankings'][$rank])) {
+          $node_rank = $this->configuration['rankings'][$rank];
           // 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']);
@@ -404,7 +408,6 @@ public function searchFormAlter(array &$form, array &$form_state) {
     );
 
     // 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',
@@ -504,13 +507,41 @@ public function searchFormSubmit(array &$form, array &$form_state) {
     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);
+    $form_state['redirect_route'] = array(
+      'route_name' => 'search.view_' . $form_state['search_page_id'],
+      'route_parameters' => array(
+        'keys' => $keys,
+      ),
+      'options' => $options,
+    );
+  }
+
+  /**
+   * Gathers ranking definitions from hook_ranking().
+   *
+   * @return array
+   *   An array of ranking definitions.
+   */
+  protected function getRankings() {
+    if (!$this->rankings) {
+      $this->rankings = $this->moduleHandler->invokeAll('ranking');
+    }
+    return $this->rankings;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    $configuration = array(
+      'rankings' => array(),
+    );
+    return $configuration;
   }
 
   /**
@@ -524,37 +555,34 @@ public function buildConfigurationForm(array $form, array &$form_state) {
     );
     $form['content_ranking']['#theme'] = 'node_search_admin';
     $form['content_ranking']['info'] = array(
-      '#value' => '<em>' . t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em>'
+      '#value' => '<em>' . $this->t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor 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(
+    foreach ($this->getRankings() as $var => $values) {
+      $form['content_ranking']['factors']["rankings_$var"] = array(
         '#title' => $values['title'],
         '#type' => 'select',
         '#options' => $options,
-        '#default_value' => variable_get('node_rank_' . $var, 0),
+        '#default_value' => isset($this->configuration['rankings'][$var]) ? $this->configuration['rankings'][$var] : 0,
       );
     }
     return $form;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function validateConfigurationForm(array &$form, array &$form_state) {
-  }
-
   /**
    * {@inheritdoc}
    */
   public function submitConfigurationForm(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]);
+    foreach ($this->getRankings() as $var => $values) {
+      if (!empty($form_state['values']["rankings_$var"])) {
+        $this->configuration['rankings'][$var] = $form_state['values']["rankings_$var"];
+      }
+      else {
+        unset($this->configuration['rankings'][$var]);
       }
     }
   }
+
 }
diff --git a/core/modules/node/node.install b/core/modules/node/node.install
index 8c104dd71180ee98c1610b0eb5b436f27c098a87..91589a02029011b0f8340580a9bb63ba39f76c28 100644
--- a/core/modules/node/node.install
+++ b/core/modules/node/node.install
@@ -450,12 +450,6 @@ function node_uninstall() {
     \Drupal::config('language.settings')->clear('node. ' . $type . '.language.default_configuration')->save();
   }
 
-  // Delete node search ranking variables.
-  variable_del('node_rank_relevance');
-  variable_del('node_rank_sticky');
-  variable_del('node_rank_promote');
-  variable_del('node_rank_recent');
-
   // Delete remaining general module variables.
   \Drupal::state()->delete('node.node_access_needs_rebuild');
 
diff --git a/core/modules/search/config/schema/search.schema.yml b/core/modules/search/config/schema/search.schema.yml
index 9fdd63f829317bdd2df024b986430165424724b2..68e6877aeb0d50f4e5d0499eac0f320b18ac7358 100644
--- a/core/modules/search/config/schema/search.schema.yml
+++ b/core/modules/search/config/schema/search.schema.yml
@@ -4,18 +4,12 @@ search.settings:
   type: mapping
   label: 'Search settings'
   mapping:
-    active_plugins:
-      type: sequence
-      label: 'Active search plugins'
-      sequence:
-        - type: string
-          label: 'Plugin'
     and_or_limit:
       type: integer
       label: 'AND/OR combination limit'
-    default_plugin:
+    default_page:
       type: string
-      label: 'Default search plugin'
+      label: 'Default search page'
     index:
       type: mapping
       label: 'Indexing settings'
@@ -69,3 +63,34 @@ search.settings:
             a:
               type: integer
               label: 'Tag a weight'
+
+search.page.*:
+  type: mapping
+  label: 'Search page'
+  mapping:
+    id:
+      type: string
+      label: 'Machine-readable name'
+    label:
+      type: label
+      label: 'Label'
+    uuid:
+      type: string
+      label: 'UUID'
+    status:
+      type: boolean
+      label: 'Enabled status of the configuration entity'
+    langcode:
+      type: string
+      label: 'Default language'
+    path:
+      type: string
+      label: 'Search page path'
+    weight:
+      type: integer
+      label: 'Weight'
+    plugin:
+      type: string
+      label: 'Plugin'
+    configuration:
+      type: search.plugin.[%parent.plugin]
diff --git a/core/modules/search/config/search.settings.yml b/core/modules/search/config/search.settings.yml
index 78e0767988e03e6a7da26427e943b9bbc6ef61a4..09c09cb06a6b50b5ee030032b0696fe83b0ba58a 100644
--- a/core/modules/search/config/search.settings.yml
+++ b/core/modules/search/config/search.settings.yml
@@ -1,8 +1,5 @@
-active_plugins:
-  node_search: node_search
-  user_search: user_search
 and_or_limit: 7
-default_plugin: node_search
+default_page: node_search
 index:
   cron_limit: 100
   overlap_cjk: true
diff --git a/core/modules/search/css/search.admin.css b/core/modules/search/css/search.admin.css
new file mode 100644
index 0000000000000000000000000000000000000000..c049ebdad80223e543a3f5160e204137c6b5ab7e
--- /dev/null
+++ b/core/modules/search/css/search.admin.css
@@ -0,0 +1,14 @@
+/**
+ * @file
+ * Styles for administration pages.
+ */
+
+/**
+ * Add search page select/submit.
+ */
+#search-admin-settings #edit-add-page {
+  margin-bottom: 1em;
+}
+#search-admin-settings #edit-add-page label {
+  display: block;
+}
diff --git a/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php b/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php
deleted file mode 100644
index 2ad2400699ab97abc88f82ff81d24a667da495fc..0000000000000000000000000000000000000000
--- a/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\search\Access\SearchAccessCheck
- */
-
-namespace Drupal\search\Access;
-
-use Drupal\Core\Routing\Access\AccessInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\search\SearchPluginManager;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Routing\Route;
-
-/**
- * Checks access for viewing search.
- */
-class SearchAccessCheck implements AccessInterface {
-
-  /**
-   * The search plugin manager.
-   *
-   * @var \Drupal\search\SearchPluginManager
-   */
-  protected $searchManager;
-
-  /**
-   * Contructs a new search access check.
-   *
-   * @param SearchPluginManager $search_plugin_manager
-   *   The search plugin manager.
-   */
-  public function __construct(SearchPluginManager $search_plugin_manager) {
-    $this->searchManager = $search_plugin_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access(Route $route, Request $request, AccountInterface $account) {
-    return $this->searchManager->getActiveDefinitions() ? static::ALLOW : static::DENY;
-  }
-
-}
diff --git a/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php b/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php
deleted file mode 100644
index 86e81663918392f37b1d889bcd3e1dc88917da29..0000000000000000000000000000000000000000
--- a/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\search\Access\SearchPluginAccessCheck
- */
-
-namespace Drupal\search\Access;
-
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Routing\Route;
-
-/**
- * Route access check for search plugins.
- */
-class SearchPluginAccessCheck extends SearchAccessCheck {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access(Route $route, Request $request, AccountInterface $account) {
-    $plugin_id = $route->getRequirement('_search_plugin_view_access');
-    return $this->searchManager->pluginAccess($plugin_id, $account) ? static::ALLOW : static::DENY;
-  }
-
-}
diff --git a/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php
index 720f2b29c613b352dbca5e2afbc63c8dcf6c12c2..b31ffe4879eb9c08b3679148efe544cc3bf62528 100644
--- a/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php
+++ b/core/modules/search/lib/Drupal/search/Annotation/SearchPlugin.php
@@ -13,8 +13,7 @@
  * 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/".
+ * search type can be used to create search pages from the Search settings page.
  *
  * @see SearchPluginBase
  *
@@ -29,13 +28,6 @@ class SearchPlugin extends Plugin {
    */
   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.
    *
@@ -47,4 +39,5 @@ class SearchPlugin extends Plugin {
    * @var \Drupal\Core\Annotation\Translation
    */
   public $title;
+
 }
diff --git a/core/modules/search/lib/Drupal/search/Controller/SearchController.php b/core/modules/search/lib/Drupal/search/Controller/SearchController.php
index 98dae2b1968f3e62940ee0b967bf8c75f0b0ab0f..83ca3b54d5232ae23f21ac04bb156ef21fc7608a 100644
--- a/core/modules/search/lib/Drupal/search/Controller/SearchController.php
+++ b/core/modules/search/lib/Drupal/search/Controller/SearchController.php
@@ -9,8 +9,8 @@
 
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Form\FormBuilderInterface;
-use Drupal\search\SearchPluginManager;
+use Drupal\search\SearchPageInterface;
+use Drupal\search\SearchPageRepositoryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -20,30 +20,20 @@
 class SearchController extends ControllerBase implements ContainerInjectionInterface {
 
   /**
-   * The search plugin manager.
+   * The search page repository.
    *
-   * @var \Drupal\search\SearchPluginManager
+   * @var \Drupal\search\SearchPageRepositoryInterface
    */
-  protected $searchManager;
-
-  /**
-   * The form builder.
-   *
-   * @var \Drupal\Core\Form\FormBuilderInterface
-   */
-  protected $formBuilder;
+  protected $searchPageRepository;
 
   /**
    * Constructs a new search controller.
    *
-   * @param \Drupal\search\SearchPluginManager $search_plugin_manager
-   *   The search plugin manager.
-   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
-   *   The form builder.
+   * @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
+   *   The search page repository.
    */
-  public function __construct(SearchPluginManager $search_plugin_manager, FormBuilderInterface $form_builder) {
-    $this->searchManager = $search_plugin_manager;
-    $this->formBuilder = $form_builder;
+  public function __construct(SearchPageRepositoryInterface $search_page_repository) {
+    $this->searchPageRepository = $search_page_repository;
   }
 
   /**
@@ -51,8 +41,7 @@ public function __construct(SearchPluginManager $search_plugin_manager, FormBuil
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('plugin.manager.search'),
-      $container->get('form_builder')
+      $container->get('search.search_page_repository')
     );
   }
 
@@ -61,46 +50,24 @@ public static function create(ContainerInterface $container) {
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request object.
-   * @param string $plugin_id
-   *   The ID of a search plugin.
+   * @param \Drupal\search\SearchPageInterface $entity
+   *   The search page entity.
    * @param string $keys
-   *   Search keywords.
+   *   (optional) Search keywords, defaults to an empty string.
    *
    * @return array|\Symfony\Component\HttpFoundation\RedirectResponse
    *   The search form and search results or redirect response.
    */
-  public function view(Request $request, $plugin_id = NULL, $keys = NULL) {
-    $info = FALSE;
-    $keys = trim($keys);
+  public function view(Request $request, SearchPageInterface $entity, $keys = '') {
     // Also try to pull search keywords from the request to support old GET
     // format of searches for existing links.
     if (!$keys && $request->query->has('keys')) {
-      $keys = trim($request->query->get('keys'));
+      $keys = $request->query->get('keys');
     }
+    $keys = trim($keys);
     $build['#title'] = $this->t('Search');
 
-    if (!empty($plugin_id)) {
-      $active_plugin_info = $this->searchManager->getActiveDefinitions();
-      if (isset($active_plugin_info[$plugin_id])) {
-        $info = $active_plugin_info[$plugin_id];
-      }
-    }
-
-    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_plugin_info();
-      // Redirect from bare /search or an invalid path to the default search
-      // path.
-      $path = 'search/' . $info['path'];
-      if ($keys) {
-        $path .= '/' . $keys;
-      }
-
-      return $this->redirect('search.view_' . $info['id']);
-    }
-    $plugin = $this->searchManager->createInstance($plugin_id);
+    $plugin = $entity->getPlugin();
     $plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
     // Default results output is an empty string.
     $results = array('#markup' => '');
@@ -114,16 +81,85 @@ public function view(Request $request, $plugin_id = NULL, $keys = NULL) {
       // Only search if there are keywords or non-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));
+        watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $entity->label()), WATCHDOG_NOTICE, $this->l(t('results'), 'search.view_' . $entity->id(), array('keys' => $keys)));
 
         // Collect the search results.
         $results = $plugin->buildResults();
       }
     }
     // The form may be altered based on whether the search was run.
-    $build['search_form'] = $this->formBuilder->getForm('\Drupal\search\Form\SearchForm', $plugin);
+    $build['search_form'] = $this->entityManager()->getForm($entity, 'search');
     $build['search_results'] = $results;
     return $build;
   }
 
+  /**
+   * Redirects to a search page.
+   *
+   * This is used to redirect from /search to the default search page.
+   *
+   * @param \Drupal\search\SearchPageInterface $entity
+   *   The search page entity.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect to the search page.
+   */
+  public function redirectSearchPage(SearchPageInterface $entity) {
+    return $this->redirect('search.view_' . $entity->id());
+  }
+
+  /**
+   * Route title callback.
+   *
+   * @param \Drupal\search\SearchPageInterface $search_page
+   *   The search page entity.
+   *
+   * @return string
+   *   The title for the search page edit form.
+   */
+  public function editTitle(SearchPageInterface $search_page) {
+    return $this->t('Edit %label search page', array('%label' => $search_page->label()));
+  }
+
+  /**
+   * Performs an operation on the search page entity.
+   *
+   * @param \Drupal\search\SearchPageInterface $search_page
+   *   The search page entity.
+   * @param string $op
+   *   The operation to perform, usually 'enable' or 'disable'.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect back to the search settings page.
+   */
+  public function performOperation(SearchPageInterface $search_page, $op) {
+    $search_page->$op()->save();
+
+    if ($op == 'enable') {
+      drupal_set_message($this->t('The %label search page has been enabled.', array('%label' => $search_page->label())));
+    }
+    elseif ($op == 'disable') {
+      drupal_set_message($this->t('The %label search page has been disabled.', array('%label' => $search_page->label())));
+    }
+
+    return $this->redirect('search.settings');
+  }
+
+  /**
+   * Sets the search page as the default.
+   *
+   * @param \Drupal\search\SearchPageInterface $search_page
+   *   The search page entity.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect to the search settings page.
+   */
+  public function setAsDefault(SearchPageInterface $search_page) {
+    // Set the default page to this search page.
+    $this->searchPageRepository->setDefaultSearchPage($search_page);
+
+    drupal_set_message($this->t('The default search page is now %label. Be sure to check the ordering of your search pages.', array('%label' => $search_page->label())));
+    return $this->redirect('search.settings');
+  }
+
 }
diff --git a/core/modules/search/lib/Drupal/search/Entity/SearchPage.php b/core/modules/search/lib/Drupal/search/Entity/SearchPage.php
new file mode 100644
index 0000000000000000000000000000000000000000..d0419d46c2ea8cb2c1bb01c6d6a6109684a81f6b
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Entity/SearchPage.php
@@ -0,0 +1,281 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Entity\SearchPage.
+ */
+
+namespace Drupal\search\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\search\Plugin\SearchIndexingInterface;
+use Drupal\search\Plugin\SearchPluginBag;
+use Drupal\search\SearchPageInterface;
+
+/**
+ * Defines a configured search page.
+ *
+ * @EntityType(
+ *   id = "search_page",
+ *   label = @Translation("Search page"),
+ *   controllers = {
+ *     "access" = "Drupal\search\SearchPageAccessController",
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController",
+ *     "list" = "Drupal\search\SearchPageListController",
+ *     "form" = {
+ *       "add" = "Drupal\search\Form\SearchPageAddForm",
+ *       "edit" = "Drupal\search\Form\SearchPageEditForm",
+ *       "search" = "Drupal\search\Form\SearchPageForm",
+ *       "delete" = "Drupal\search\Form\SearchPageDeleteForm"
+ *     }
+ *   },
+ *   admin_permission = "administer search",
+ *   links = {
+ *     "edit-form" = "search.edit"
+ *   },
+ *   config_prefix = "search.page",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *     "uuid" = "uuid",
+ *     "weight" = "weight",
+ *     "status" = "status"
+ *   }
+ * )
+ */
+class SearchPage extends ConfigEntityBase implements SearchPageInterface {
+
+  /**
+   * The name (plugin ID) of the search page entity.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The label of the search page entity.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * The UUID of the search page entity.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The configuration of the search page entity.
+   *
+   * @var array
+   */
+  protected $configuration = array();
+
+  /**
+   * The search plugin ID.
+   *
+   * @var string
+   */
+  protected $plugin;
+
+  /**
+   * The path this search page will appear upon.
+   *
+   * This value is appended to 'search/' when building the path.
+   *
+   * @var string
+   */
+  protected $path;
+
+  /**
+   * The weight of the search page.
+   *
+   * @var int
+   */
+  protected $weight;
+
+  /**
+   * The plugin bag that stores search plugins.
+   *
+   * @var \Drupal\search\Plugin\SearchPluginBag
+   */
+  protected $pluginBag;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $values, $entity_type) {
+    parent::__construct($values, $entity_type);
+
+    $this->pluginBag = new SearchPluginBag($this->searchPluginManager(), array($this->plugin), $this->configuration, $this->id());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPlugin() {
+    return $this->pluginBag->get($this->plugin);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPlugin($plugin_id) {
+    $this->plugin = $plugin_id;
+    $this->pluginBag->addInstanceID($plugin_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isIndexable() {
+    return $this->status() && $this->getPlugin() instanceof SearchIndexingInterface;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isDefaultSearch() {
+    return $this->searchPageRepository()->getDefaultSearchPage() == $this->id();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPath() {
+    return $this->path;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWeight() {
+    return $this->weight;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExportProperties() {
+    $properties = parent::getExportProperties();
+    $names = array(
+      'path',
+      'weight',
+      'plugin',
+      'configuration',
+    );
+    foreach ($names as $name) {
+      $properties[$name] = $this->get($name);
+    }
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageControllerInterface $storage_controller) {
+    parent::postCreate($storage_controller);
+
+    // @todo Use self::applyDefaultValue() once https://drupal.org/node/2004756
+    //   is in.
+    if (!isset($this->weight)) {
+      $this->weight = $this->isDefaultSearch() ? -10 : 0;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    parent::preSave($storage_controller);
+
+    $plugin = $this->getPlugin();
+    // If this plugin has any configuration, ensure that it is set.
+    if ($plugin instanceof ConfigurablePluginInterface) {
+      $this->set('configuration', $plugin->getConfiguration());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    parent::postSave($storage_controller, $update);
+
+    $this->state()->set('menu_rebuild_needed', TRUE);
+    // @todo The above call should be sufficient, but it is not until
+    //   https://drupal.org/node/2167323 is fixed.
+    \Drupal::service('router.builder')->rebuild();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    parent::postDelete($storage_controller, $entities);
+
+    $search_page_repository = \Drupal::service('search.search_page_repository');
+    if (!$search_page_repository->isSearchActive()) {
+      $search_page_repository->clearDefaultSearchPage();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function sort($a, $b) {
+    /** @var $a \Drupal\search\SearchPageInterface */
+    /** @var $b \Drupal\search\SearchPageInterface */
+    $a_status = (int) $a->status();
+    $b_status = (int) $b->status();
+    if ($a_status != $b_status) {
+      return ($a_status > $b_status) ? -1 : 1;
+    }
+    return parent::sort($a, $b);
+  }
+
+  /**
+   * Wraps the state storage.
+   *
+   * @return \Drupal\Core\KeyValueStore\StateInterface
+   *   An object for state storage.
+   */
+  protected function state() {
+    return \Drupal::state();
+  }
+
+  /**
+   * Wraps the config factory.
+   *
+   * @return \Drupal\Core\Config\ConfigFactory
+   *   A config factory object.
+   */
+  protected function configFactory() {
+    return \Drupal::service('config.factory');
+  }
+
+  /**
+   * Wraps the search page repository.
+   *
+   * @return \Drupal\search\SearchPageRepositoryInterface
+   *   A search page repository object.
+   */
+  protected function searchPageRepository() {
+    return \Drupal::service('search.search_page_repository');
+  }
+
+  /**
+   * Wraps the search plugin manager.
+   *
+   * @return \Drupal\Component\Plugin\PluginManagerInterface
+   *   A search plugin manager object.
+   */
+  protected function searchPluginManager() {
+    return \Drupal::service('plugin.manager.search');
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
index c943c9338dae79c7d6d38b73e89ed5976c38383b..ad967359d6fddb21717af2679429bfa4d0cad11e 100644
--- a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
+++ b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
@@ -8,12 +8,40 @@
 namespace Drupal\search\Form;
 
 use Drupal\Core\Form\FormBase;
+use Drupal\search\SearchPageRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Builds the search form for the search block.
  */
 class SearchBlockForm extends FormBase {
 
+  /**
+   * The search page repository.
+   *
+   * @var \Drupal\search\SearchPageRepositoryInterface
+   */
+  protected $searchPageRepository;
+
+  /**
+   * Constructs a new SearchBlockForm.
+   *
+   * @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
+   *   The search page repository.
+   */
+  public function __construct(SearchPageRepositoryInterface $search_page_repository) {
+    $this->searchPageRepository = $search_page_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('search.search_page_repository')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -62,17 +90,17 @@ public function submitForm(array &$form, array &$form_state) {
     }
 
     $form_id = $form['form_id']['#value'];
-    $info = search_get_default_plugin_info();
-    if ($info) {
+    if ($entity_id = $this->searchPageRepository->getDefaultSearchPage()) {
       $form_state['redirect_route'] = array(
-        'route_name' => 'search.view_' . $info['id'],
+        'route_name' => 'search.view_' . $entity_id,
         'route_parameters' => array(
           'keys' => trim($form_state['values'][$form_id]),
         ),
       );
     }
     else {
-      $this->setFormError('', $form_state, $this->t('Search is currently disabled.'), 'error');
+      $this->setFormError('', $form_state, $this->t('Search is currently disabled.'));
     }
   }
+
 }
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageAddForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageAddForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..79ff6093310cc7a7f7e8f4d6fea3991ea1ec643b
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Form/SearchPageAddForm.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Form\SearchPageAddForm.
+ */
+
+namespace Drupal\search\Form;
+
+/**
+ * Provides a form controller for adding a search page.
+ */
+class SearchPageAddForm extends SearchPageFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, $search_plugin_id = NULL) {
+    $this->entity->setPlugin($search_plugin_id);
+    $definition = $this->entity->getPlugin()->getPluginDefinition();
+    $this->entity->set('label', $definition['title']);
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, array &$form_state) {
+    $actions = parent::actions($form, $form_state);
+    $actions['submit']['#value'] = $this->t('Add search page');
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    // If there is no default search page, make the added search the default.
+    if (!$this->searchPageRepository->getDefaultSearchPage()) {
+      $this->searchPageRepository->setDefaultSearchPage($this->entity);
+    }
+
+    parent::save($form, $form_state);
+
+    drupal_set_message($this->t('The %label search page has been added.', array('%label' => $this->entity->label())));
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageDeleteForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageDeleteForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..c9a575255a7cd9708e54749f7ca162440c4ad0e7
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Form/SearchPageDeleteForm.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search\Form\SearchPageDeleteForm.
+ */
+
+namespace Drupal\search\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+
+/**
+ * Provides a deletion confirm form for search.
+ */
+class SearchPageDeleteForm extends EntityConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete the %label search page?', array('%label' => $this->entity->label()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelRoute() {
+    return array(
+      'route_name' => 'search.settings',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    $this->entity->delete();
+    $form_state['redirect_route']['route_name'] = 'search.settings';
+    drupal_set_message($this->t('The %label search page has been deleted.', array('%label' => $this->entity->label())));
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageEditForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageEditForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..de9b1e448eee2761e92e150d26bd3d2679b3a7ee
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Form/SearchPageEditForm.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Form\SearchPageEditForm.
+ */
+
+namespace Drupal\search\Form;
+
+/**
+ * Provides a form controller for editing a search page.
+ */
+class SearchPageEditForm extends SearchPageFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, array &$form_state) {
+    $actions = parent::actions($form, $form_state);
+    $actions['submit']['#value'] = $this->t('Save search page');
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    parent::save($form, $form_state);
+
+    drupal_set_message($this->t('The %label search page has been updated.', array('%label' => $this->entity->label())));
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageForm.php b/core/modules/search/lib/Drupal/search/Form/SearchPageForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..d817f7c9b3c5350c816957ccaba6d050f8d931ae
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Form/SearchPageForm.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Form\SearchPageForm.
+ */
+
+namespace Drupal\search\Form;
+
+use Drupal\Core\Entity\EntityFormController;
+
+/**
+ * Provides a search form for site wide search.
+ */
+class SearchPageForm extends EntityFormController {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @var \Drupal\search\SearchPageInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'search_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $plugin = $this->entity->getPlugin();
+
+    $form_state['search_page_id'] = $this->entity->id();
+    $form['basic'] = array(
+      '#type' => 'container',
+      '#attributes' => array(
+        'class' => array('container-inline'),
+      ),
+    );
+    $form['basic']['keys'] = array(
+      '#type' => 'search',
+      '#title' => $this->t('Enter your keywords'),
+      '#default_value' => $plugin->getKeywords(),
+      '#size' => 30,
+      '#maxlength' => 255,
+    );
+    // processed_keys is used to coordinate keyword passing between other forms
+    // that hook into the basic search form.
+    $form['basic']['processed_keys'] = array(
+      '#type' => 'value',
+      '#value' => '',
+    );
+    $form['basic']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Search'),
+    );
+    // Allow the plugin to add to or alter the search form.
+    $plugin->searchFormAlter($form, $form_state);
+
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, array &$form_state) {
+    // The submit button is added in the form directly.
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+    form_set_value($form['basic']['processed_keys'], trim($form_state['values']['keys']), $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $keys = $form_state['values']['processed_keys'];
+    if ($keys == '') {
+      $this->setFormError('keys', $form_state, $this->t('Please enter some keywords.'));
+      // Fall through to the form redirect.
+    }
+
+    $form_state['redirect_route'] = array(
+      'route_name' => 'search.view_' . $this->entity->id(),
+      'route_parameters' => array(
+        'keys' => $keys,
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchPageFormBase.php b/core/modules/search/lib/Drupal/search/Form/SearchPageFormBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..1e574c38529e8e0d796220a85ffe18ac3b38455b
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Form/SearchPageFormBase.php
@@ -0,0 +1,207 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Form\SearchPageFormBase.
+ */
+
+namespace Drupal\search\Form;
+
+use Drupal\Core\Entity\EntityFormController;
+use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\search\SearchPageRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base form controller for search pages.
+ */
+abstract class SearchPageFormBase extends EntityFormController {
+
+  /**
+   * The entity being used by this form.
+   *
+   * @var \Drupal\search\SearchPageInterface
+   */
+  protected $entity;
+
+  /**
+   * The search plugin being configured.
+   *
+   * @var \Drupal\search\Plugin\SearchInterface
+   */
+  protected $plugin;
+
+  /**
+   * The entity query factory.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryFactory
+   */
+  protected $entityQuery;
+
+  /**
+   * The search page repository.
+   *
+   * @var \Drupal\search\SearchPageRepositoryInterface
+   */
+  protected $searchPageRepository;
+
+  /**
+   * Constructs a new search form.
+   *
+   * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query
+   *   The entity query.
+   * @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
+   *   The search page repository.
+   */
+  public function __construct(QueryFactory $entity_query, SearchPageRepositoryInterface $search_page_repository) {
+    $this->entityQuery = $entity_query;
+    $this->searchPageRepository = $search_page_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.query'),
+      $container->get('search.search_page_repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBaseFormID() {
+    return 'search_entity_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $this->plugin = $this->entity->getPlugin();
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state) {
+    $form['label'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Label'),
+      '#description' => $this->t('The label for this search page.'),
+      '#default_value' => $this->entity->label(),
+      '#maxlength' => '255',
+    );
+
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#default_value' => $this->entity->id(),
+      '#disabled' => !$this->entity->isNew(),
+      '#maxlength' => 64,
+      '#machine_name' => array(
+        'exists' => array($this, 'exists'),
+      ),
+    );
+    $form['path'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Path'),
+      '#field_prefix' => 'search/',
+      '#default_value' => $this->entity->getPath(),
+      '#maxlength' => '255',
+    );
+    $form['plugin'] = array(
+      '#type' => 'value',
+      '#value' => $this->entity->get('plugin'),
+    );
+
+    if ($this->plugin instanceof PluginFormInterface) {
+      $form += $this->plugin->buildConfigurationForm($form, $form_state);
+    }
+
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * Determines if the search page entity already exists.
+   *
+   * @param string $id
+   *   The search configuration ID.
+   *
+   * @return bool
+   *   TRUE if the search configuration exists, FALSE otherwise.
+   */
+  public function exists($id) {
+    $entity = $this->entityQuery->get('search_page')
+      ->condition('id', $id)
+      ->execute();
+    return (bool) $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+
+    // Ensure each path is unique.
+    $path = $this->entityQuery->get('search_page')
+      ->condition('path', $form_state['values']['path'])
+      ->condition('id', $form_state['values']['id'], '<>')
+      ->execute();
+    if ($path) {
+      $this->setFormError('path', $form_state, $this->t('The search page path must be unique.'));
+    }
+
+    if ($this->plugin instanceof PluginFormInterface) {
+      $this->plugin->validateConfigurationForm($form, $form_state);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    parent::submit($form, $form_state);
+
+    if ($this->plugin instanceof PluginFormInterface) {
+      $this->plugin->submitConfigurationForm($form, $form_state);
+    }
+    return $this->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    $this->entity->save();
+
+    $form_state['redirect_route']['route_name'] = 'search.settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(array $form, array &$form_state) {
+    $form_state['redirect_route'] = array(
+      'route_name' => 'search.delete',
+      'route_parameters' => array(
+        'search_page' => $this->entity->id(),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, array &$form_state) {
+    $actions = parent::actions($form, $form_state);
+    if ($this->entity->isDefaultSearch()) {
+      unset($actions['delete']);
+    }
+    return $actions;
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php b/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php
deleted file mode 100644
index 0d3e1155e005db9ff710e40dc8188581230a5801..0000000000000000000000000000000000000000
--- a/core/modules/search/lib/Drupal/search/Form/SearchSettingsForm.php
+++ /dev/null
@@ -1,281 +0,0 @@
-<?php
-/**
- * @file
- * Contains \Drupal\search\Form\SearchSettingsForm.
- */
-
-namespace Drupal\search\Form;
-
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Config\ConfigFactory;
-use Drupal\Core\Config\Context\ContextInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\KeyValueStore\StateInterface;
-use Drupal\Core\Plugin\PluginFormInterface;
-use Drupal\search\SearchPluginManager;
-use Drupal\Core\Form\ConfigFormBase;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Configure search settings for this site.
- */
-class SearchSettingsForm extends ConfigFormBase {
-
-  /**
-   * 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.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
-   * The Drupal state storage service.
-   *
-   * @var \Drupal\Core\KeyValueStore\StateInterface
-   */
-  protected $state;
-
-  /**
-   * Constructs a \Drupal\search\Form\SearchSettingsForm object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactory $config_factory
-   *   The configuration factory object that manages search settings.
-   * @param \Drupal\Core\Config\Context\ContextInterface $context
-   *   The context interface
-   * @param \Drupal\search\SearchPluginManager $manager
-   *   The manager for search plugins.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler
-   * @param \Drupal\Core\KeyValueStore\StateInterface $state
-   *   The state key/value store interface, gives access to state based config settings.
-   */
-  public function __construct(ConfigFactory $config_factory, ContextInterface $context, SearchPluginManager $manager, ModuleHandlerInterface $module_handler, StateInterface $state) {
-    parent::__construct($config_factory, $context);
-    $this->searchSettings = $config_factory->get('search.settings');
-    $this->searchPluginManager = $manager;
-    $this->moduleHandler = $module_handler;
-    $this->state = $state;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  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')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'search_admin_settings';
-  }
-
-  /**
-   * Returns names of available search plugins.
-   *
-   * @return array
-   *   An array of the names of available search plugins.
-   */
-  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) {
-
-    // Collect some stats.
-    $remaining = 0;
-    $total = 0;
-
-    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))) . '%';
-    $status = '<p><strong>' . $this->t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '</strong></p>';
-    $form['status'] = array(
-      '#type' => 'details',
-      '#title' => $this->t('Indexing status'),
-    );
-    $form['status']['status'] = array('#markup' => $status);
-    $form['status']['wipe'] = array(
-      '#type' => 'submit',
-      '#value' => $this->t('Re-index site'),
-      '#submit' => array(array($this, 'searchAdminReindexSubmit')),
-    );
-
-    $items = drupal_map_assoc(array(10, 20, 50, 100, 200, 500));
-
-    // Indexing throttle:
-    $form['indexing_throttle'] = array(
-      '#type' => 'details',
-      '#title' => $this->t('Indexing throttle')
-    );
-    $form['indexing_throttle']['cron_limit'] = array(
-      '#type' => 'select',
-      '#title' => $this->t('Number of items to index per cron run'),
-      '#default_value' => $this->searchSettings->get('index.cron_limit'),
-      '#options' => $items,
-      '#description' => $this->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')))
-    );
-    // Indexing settings:
-    $form['indexing_settings'] = array(
-      '#type' => 'details',
-      '#title' => $this->t('Indexing settings')
-    );
-    $form['indexing_settings']['info'] = array(
-      '#markup' => $this->t('<p><em>Changing the settings below will cause the site index to be rebuilt. The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed.</em></p><p><em>The default settings should be appropriate for the majority of sites.</em></p>')
-    );
-    $form['indexing_settings']['minimum_word_size'] = array(
-      '#type' => 'number',
-      '#title' => $this->t('Minimum word length to index'),
-      '#default_value' => $this->searchSettings->get('index.minimum_word_size'),
-      '#min' => 1,
-      '#max' => 1000,
-      '#description' => $this->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).')
-    );
-    $form['indexing_settings']['overlap_cjk'] = array(
-      '#type' => 'checkbox',
-      '#title' => $this->t('Simple CJK handling'),
-      '#default_value' => $this->searchSettings->get('index.overlap_cjk'),
-      '#description' => $this->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' => $this->t('Active search plugins')
-    );
-    $options = $this->getOptions();
-    $form['active']['active_plugins'] = array(
-      '#type' => 'checkboxes',
-      '#title' => $this->t('Active plugins'),
-      '#title_display' => 'invisible',
-      '#default_value' => $this->searchSettings->get('active_plugins'),
-      '#options' => $options,
-      '#description' => $this->t('Choose which search plugins are active from the available plugins.')
-    );
-    $form['active']['default_plugin'] = array(
-      '#title' => $this->t('Default search plugin'),
-      '#type' => 'radios',
-      '#default_value' => $this->searchSettings->get('default_plugin'),
-      '#options' => $options,
-      '#description' => $this->t('Choose which search plugin is the default.')
-    );
-
-    // Per plugin settings.
-    foreach ($active_plugins as $plugin) {
-      if ($plugin instanceof PluginFormInterface) {
-        $form = $plugin->buildConfigurationForm($form, $form_state);
-      }
-    }
-    // Set #submit so we are sure it's invoked even if one of
-    // the active search plugins added its own #submit.
-    $form['#submit'][] = array($this, 'submitForm');
-
-    return parent::buildForm($form, $form_state);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, array &$form_state) {
-    parent::validateForm($form, $form_state);
-
-    // Check whether we selected a valid default.
-    if ($form_state['triggering_element']['#value'] != $this->t('Reset to defaults')) {
-      $new_plugins = array_filter($form_state['values']['active_plugins']);
-      $default = $form_state['values']['default_plugin'];
-      if (!in_array($default, $new_plugins, TRUE)) {
-        $this->setFormError('default_plugin', $form_state, $this->t('Your default search plugin is not selected as an active plugin.'));
-      }
-    }
-    // Handle per-plugin validation logic.
-    foreach ($this->searchPluginManager->getActivePlugins() as $plugin) {
-      if ($plugin instanceof PluginFormInterface) {
-        $plugin->validateConfigurationForm($form, $form_state);
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, array &$form_state) {
-    parent::submitForm($form, $form_state);
-
-    // If these settings change, the index needs to be rebuilt.
-    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($this->t('The index will be rebuilt.'));
-      search_reindex();
-    }
-    $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) {
-      if ($plugin instanceof PluginFormInterface) {
-        $plugin->submitConfigurationForm($form, $form_state);
-      }
-    }
-
-    // Check whether we are resetting the values.
-    if ($form_state['triggering_element']['#value'] == $this->t('Reset to defaults')) {
-      $new_plugins = array('node_search', 'user_search');
-    }
-    else {
-      $new_plugins = array_filter($form_state['values']['active_plugins']);
-    }
-    if ($this->searchSettings->get('active_plugins') != $new_plugins) {
-      $this->searchSettings->set('active_plugins', $new_plugins);
-      drupal_set_message($this->t('The active search plugins have been changed.'));
-      $this->state->set('menu_rebuild_needed', TRUE);
-      Cache::deleteTags(array('local_task' => TRUE));
-    }
-    $this->searchSettings->save();
-  }
-
-  /**
-   * Form submission handler for the reindex button on the search admin settings
-   * form.
-   */
-  public function searchAdminReindexSubmit(array $form, array &$form_state) {
-    // send the user to the confirmation page
-    $form_state['redirect_route']['route_name'] = 'search.reindex_confirm';
-  }
-
-}
diff --git a/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..0328915555a84bc108192e3dfac03f6455e715ea
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginBase.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Plugin\ConfigurableSearchPluginBase.
+ */
+
+namespace Drupal\search\Plugin;
+
+use Drupal\Component\Utility\NestedArray;
+
+/**
+ * Provides a base implementation for a configurable Search plugin.
+ */
+abstract class ConfigurableSearchPluginBase extends SearchPluginBase implements ConfigurableSearchPluginInterface {
+
+  /**
+   * The unique ID for the search page using this plugin.
+   *
+   * @var string
+   */
+  protected $searchPageId;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $this->configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSearchPageId($search_page_id) {
+    $this->searchPageId = $search_page_id;
+    return $this;
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginInterface.php b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..62899c6c8bd79c8730045ca35f99411c4fe9e9ec
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Plugin/ConfigurableSearchPluginInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Plugin\ConfigurableSearchPluginInterface.
+ */
+
+namespace Drupal\search\Plugin;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+
+/**
+ * Provides an interface for a configurable Search plugin.
+ */
+interface ConfigurableSearchPluginInterface extends ConfigurablePluginInterface, PluginFormInterface, SearchInterface {
+
+  /**
+   * Sets the ID for the search page using this plugin.
+   *
+   * @param string $search_page_id
+   *   The search page ID.
+   *
+   * @return static
+   */
+  public function setSearchPageId($search_page_id);
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Plugin/Derivative/SearchLocalTask.php b/core/modules/search/lib/Drupal/search/Plugin/Derivative/SearchLocalTask.php
index 5c418bc0a94151c6b2e1f8ef4353a55e0034c846..20244784064659fd88c7b2ccd1d5360ed5188eee 100644
--- a/core/modules/search/lib/Drupal/search/Plugin/Derivative/SearchLocalTask.php
+++ b/core/modules/search/lib/Drupal/search/Plugin/Derivative/SearchLocalTask.php
@@ -8,11 +8,40 @@
 namespace Drupal\search\Plugin\Derivative;
 
 use Drupal\Component\Plugin\Derivative\DerivativeBase;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
+use Drupal\search\SearchPageRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * Provides local tasks for each search plugin.
+ * Provides local tasks for each search page.
  */
-class SearchLocalTask extends DerivativeBase {
+class SearchLocalTask extends DerivativeBase implements ContainerDerivativeInterface {
+
+  /**
+   * The search page repository.
+   *
+   * @var \Drupal\search\SearchPageRepositoryInterface
+   */
+  protected $searchPageRepository;
+
+  /**
+   * Constructs a new SearchLocalTask.
+   *
+   * @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
+   *   The search page repository.
+   */
+  public function __construct(SearchPageRepositoryInterface $search_page_repository) {
+    $this->searchPageRepository = $search_page_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('search.search_page_repository')
+    );
+  }
 
   /**
    * {@inheritdoc}
@@ -20,20 +49,15 @@ class SearchLocalTask extends DerivativeBase {
   public function getDerivativeDefinitions(array $base_plugin_definition) {
     $this->derivatives = array();
 
-    $default_info = search_get_default_plugin_info();
-    if ($default_info) {
-      foreach (\Drupal::service('plugin.manager.search')->getActiveDefinitions() as $plugin_id => $search_info) {
-        $this->derivatives[$plugin_id] = array(
-          'title' => $search_info['title'],
-          'route_name' => 'search.view_' . $plugin_id,
-          'base_route' => 'search.view_' . $default_info['id'],
+    if ($default = $this->searchPageRepository->getDefaultSearchPage()) {
+      $active_search_pages = $this->searchPageRepository->getActiveSearchPages();
+      foreach ($this->searchPageRepository->sortSearchPages($active_search_pages) as $entity_id => $entity) {
+        $this->derivatives[$entity_id] = array(
+          'title' => $entity->label(),
+          'route_name' => 'search.view_' . $entity_id,
+          'base_route' => 'search.plugins:' . $default,
+          'weight' => $entity->getWeight(),
         );
-        if ($plugin_id == $default_info['id']) {
-          $this->derivatives[$plugin_id]['weight'] = -10;
-        }
-        else {
-          $this->derivatives[$plugin_id]['weight'] = 0;
-        }
       }
     }
     return $this->derivatives;
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php
index a2ca97378adc04707ab432d8f3550a60e11e609b..26edef5556699f66efe9a9ac59993f9838758950 100644
--- a/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchIndexingInterface.php
@@ -14,6 +14,11 @@
  * 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.
+ *
+ * Multiple search pages can be created for each search plugin, so you will need
+ * to choose whether these search pages should share an index (in which case
+ * they must not use any search page-specific configuration while indexing) or
+ * they will have separate indexes (which will use additional server resources).
  */
 interface SearchIndexingInterface {
 
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBag.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBag.php
new file mode 100644
index 0000000000000000000000000000000000000000..0f8596eea2ff291096b966735f7cf9b3813e41d0
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBag.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Plugin\SearchPluginBag.
+ */
+
+namespace Drupal\search\Plugin;
+
+use Drupal\Component\Plugin\DefaultSinglePluginBag;
+use Drupal\Component\Plugin\PluginManagerInterface;
+
+/**
+ * Provides a container for lazily loading search plugins.
+ */
+class SearchPluginBag extends DefaultSinglePluginBag {
+
+  /**
+   * The unique ID for the search page using this plugin bag.
+   *
+   * @var string
+   */
+  protected $searchPageId;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(PluginManagerInterface $manager, array $instance_ids, array $configuration, $search_page_id) {
+    parent::__construct($manager, $instance_ids, $configuration);
+
+    $this->searchPageId = $search_page_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\search\Plugin\SearchInterface
+   */
+  public function &get($instance_id) {
+    return parent::get($instance_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializePlugin($instance_id) {
+    parent::initializePlugin($instance_id);
+
+    $plugin_instance = $this->pluginInstances[$instance_id];
+    if ($plugin_instance instanceof ConfigurableSearchPluginInterface) {
+      $plugin_instance->setSearchPageId($this->searchPageId);
+    }
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
index 7f534d5ffc586c1539ce30f6cd1461ed2e6ee4a0..ec808ad75491c8d2d071eda607a57d101d31b40e 100644
--- a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a base class for plugins wishing to support search.
@@ -36,6 +37,13 @@ abstract class SearchPluginBase extends PluginBase implements ContainerFactoryPl
    */
   protected $searchAttributes;
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/search/lib/Drupal/search/Routing/SearchPageRoutes.php b/core/modules/search/lib/Drupal/search/Routing/SearchPageRoutes.php
new file mode 100644
index 0000000000000000000000000000000000000000..9a5f505333edd3ce0bd92d6cb0931c86a0a73db6
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Routing/SearchPageRoutes.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Routing\SearchPageRoutes.
+ */
+
+namespace Drupal\search\Routing;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\search\SearchPageRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides dynamic routes for search.
+ */
+class SearchPageRoutes implements ContainerInjectionInterface {
+
+  /**
+   * The search page repository.
+   *
+   * @var \Drupal\search\SearchPageRepositoryInterface
+   */
+  protected $searchPageRepository;
+
+  /**
+   * Constructs a new search route subscriber.
+   *
+   * @param \Drupal\search\SearchPageRepositoryInterface $search_page_repository
+   *   The search page repository.
+   */
+  public function __construct(SearchPageRepositoryInterface $search_page_repository) {
+    $this->searchPageRepository = $search_page_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('search.search_page_repository')
+    );
+  }
+
+  /**
+   * Returns an array of route objects.
+   *
+   * @return \Symfony\Component\Routing\Route[]
+   *   An array of route objects.
+   */
+  public function routes() {
+    $routes = array();
+    // @todo Decide if /search should continue to redirect to /search/$default,
+    //   or just perform the appropriate search.
+    if ($default_page = $this->searchPageRepository->getDefaultSearchPage()) {
+      $routes['search.view'] = new Route(
+        '/search',
+        array(
+          '_content' => 'Drupal\search\Controller\SearchController::redirectSearchPage',
+          '_title' => 'Search',
+          'entity' => $default_page,
+        ),
+        array(
+          '_entity_access' => 'entity.view',
+          '_permission' => 'search content',
+        ),
+        array(
+          'parameters' => array(
+            'entity' => array(
+              'type' => 'entity:search_page',
+            ),
+          ),
+        )
+      );
+    }
+    $active_pages = $this->searchPageRepository->getActiveSearchPages();
+    foreach ($active_pages as $entity_id => $entity) {
+      $routes["search.view_$entity_id"] = new Route(
+        '/search/' . $entity->getPath() . '/{keys}',
+        array(
+          '_content' => 'Drupal\search\Controller\SearchController::view',
+          '_title' => $entity->label(),
+          'entity' => $entity_id,
+          'keys' => '',
+        ),
+        array(
+          'keys' => '.+',
+          '_entity_access' => 'entity.view',
+          '_permission' => 'search content',
+        ),
+        array(
+          'parameters' => array(
+            'entity' => array(
+              'type' => 'entity:search_page',
+            ),
+          ),
+        )
+      );
+    }
+    return $routes;
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Routing/SearchPluginRoutes.php b/core/modules/search/lib/Drupal/search/Routing/SearchPluginRoutes.php
deleted file mode 100644
index bfc37eaa9c382011855711169b9292177eb41533..0000000000000000000000000000000000000000
--- a/core/modules/search/lib/Drupal/search/Routing/SearchPluginRoutes.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\search\Routing\SearchPluginRoutes.
- */
-
-namespace Drupal\search\Routing;
-
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\search\SearchPluginManager;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\Routing\Route;
-
-/**
- * Provides dynamic routes for search.
- */
-class SearchPluginRoutes implements ContainerInjectionInterface {
-
-  /**
-   * The search plugin manager.
-   *
-   * @var \Drupal\search\SearchPluginManager
-   */
-  protected $searchManager;
-
-  /**
-   * Constructs a new search route subscriber.
-   *
-   * @param \Drupal\search\SearchPluginManager $search_plugin_manager
-   *   The search plugin manager.
-   */
-  public function __construct(SearchPluginManager $search_plugin_manager) {
-    $this->searchManager = $search_plugin_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('plugin.manager.search')
-    );
-  }
-
-  /**
-   * Returns an array of route objects.
-   *
-   * @return \Symfony\Component\Routing\Route[]
-   *   An array of route objects.
-   */
-  public function routes() {
-    $routes = array();
-    foreach ($this->searchManager->getActiveDefinitions() as $plugin_id => $search_info) {
-      $routes["search.view_$plugin_id"] = new Route(
-        'search/' . $search_info['path'] . '/{keys}',
-        array(
-          '_content' => 'Drupal\search\Controller\SearchController::view',
-          '_title' => $search_info['title'],
-          'plugin_id' => $plugin_id,
-          'keys' => '',
-        ),
-        array(
-          'keys' => '.+',
-          '_search_plugin_view_access' => $plugin_id,
-          '_permission' => 'search content',
-        )
-      );
-    }
-    return $routes;
-  }
-
-}
diff --git a/core/modules/search/lib/Drupal/search/SearchPageAccessController.php b/core/modules/search/lib/Drupal/search/SearchPageAccessController.php
new file mode 100644
index 0000000000000000000000000000000000000000..cd5127828a7bf4bc6b95921630b089c4e6d4e242
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/SearchPageAccessController.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\SearchPageAccessController.
+ */
+
+namespace Drupal\search;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access controller for the search page entity type.
+ */
+class SearchPageAccessController extends EntityAccessController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    /** @var $entity \Drupal\search\SearchPageInterface */
+    if (in_array($operation, array('delete', 'disable')) && $entity->isDefaultSearch()) {
+      return FALSE;
+    }
+    if ($operation == 'view') {
+      if (!$entity->status()) {
+        return FALSE;
+      }
+      $plugin = $entity->getPlugin();
+      if ($plugin instanceof AccessibleInterface) {
+        return $plugin->access($operation, $account);
+      }
+      return TRUE;
+    }
+    return parent::checkAccess($entity, $operation, $langcode, $account);
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/SearchPageInterface.php b/core/modules/search/lib/Drupal/search/SearchPageInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..dae91f97caa7e9c142b7f6dd90eb26c93bdbf74d
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/SearchPageInterface.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\SearchPageInterface.
+ */
+
+namespace Drupal\search;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface defining a search page entity.
+ */
+interface SearchPageInterface extends ConfigEntityInterface {
+
+  /**
+   * Returns the search plugin.
+   *
+   * @return \Drupal\search\Plugin\SearchInterface
+   *   The search plugin used by this search page entity.
+   */
+  public function getPlugin();
+
+  /**
+   * Sets the search plugin.
+   *
+   * @param string $plugin_id
+   *   The search plugin ID.
+   */
+  public function setPlugin($plugin_id);
+
+  /**
+   * Determines if this search page entity is currently the default search.
+   *
+   * @return bool
+   *   TRUE if this search page entity is the default search, FALSE otherwise.
+   */
+  public function isDefaultSearch();
+
+  /**
+   * Determines if this search page entity is indexable.
+   *
+   * @return bool
+   *   TRUE if this search page entity is indexable, FALSE otherwise.
+   */
+  public function isIndexable();
+
+  /**
+   * Returns the path for the search.
+   *
+   * @return string
+   *  The part of the path for this search page that comes after 'search'.
+   */
+  public function getPath();
+
+  /**
+   * Returns the weight for the page.
+   *
+   * @return int
+   *   The page weight.
+   */
+  public function getWeight();
+
+}
diff --git a/core/modules/search/lib/Drupal/search/SearchPageListController.php b/core/modules/search/lib/Drupal/search/SearchPageListController.php
new file mode 100644
index 0000000000000000000000000000000000000000..61b17d95af1e2b5bfc3b495ffa69f6f44390ebcf
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/SearchPageListController.php
@@ -0,0 +1,346 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\SearchPageListController.
+ */
+
+namespace Drupal\search;
+
+use Drupal\Component\Utility\MapArray;
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Config\Context\ContextInterface;
+use Drupal\Core\Config\Entity\DraggableListController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\FormInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a listing of search page entities.
+ */
+class SearchPageListController extends DraggableListController implements FormInterface {
+
+  /**
+   * The entities being listed.
+   *
+   * @var \Drupal\search\SearchPageInterface[]
+   */
+  protected $entities = array();
+
+  /**
+   * Stores the configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * The search manager.
+   *
+   * @var \Drupal\search\SearchPluginManager
+   */
+  protected $searchManager;
+
+  /**
+   * Constructs a new SearchPageListController object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_info
+   *   The entity info for the entity type.
+   * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage
+   *   The entity storage controller class.
+   * @param \Drupal\search\SearchPluginManager $search_manager
+   *   The search plugin manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke hooks on.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\Context\ContextInterface $context
+   *   The configuration context to use.
+   */
+  public function __construct(EntityTypeInterface $entity_info, EntityStorageControllerInterface $storage, SearchPluginManager $search_manager, ModuleHandlerInterface $module_handler, ConfigFactory $config_factory, ContextInterface $context) {
+    parent::__construct($entity_info, $storage, $module_handler);
+
+    $this->configFactory = $config_factory;
+    $this->configFactory->enterContext($context);
+
+    $this->searchManager = $search_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_info) {
+    return new static(
+      $entity_info,
+      $container->get('entity.manager')->getStorageController($entity_info->id()),
+      $container->get('plugin.manager.search'),
+      $container->get('module_handler'),
+      $container->get('config.factory'),
+      $container->get('config.context.free')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'search_admin_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['label'] = array(
+      'data' => $this->t('Label'),
+    );
+    $header['url'] = array(
+      'data' => $this->t('URL'),
+      'class' => array(RESPONSIVE_PRIORITY_LOW),
+    );
+    $header['plugin'] = array(
+      'data' => $this->t('Type'),
+      'class' => array(RESPONSIVE_PRIORITY_LOW),
+    );
+    $header['status'] = array(
+      'data' => $this->t('Status'),
+      'class' => array(RESPONSIVE_PRIORITY_LOW),
+    );
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var $entity \Drupal\search\SearchPageInterface */
+    $row['label'] = $this->getLabel($entity);
+    $row['url']['#markup'] = 'search/' . $entity->getPath();
+    // If the search page is active, link to it.
+    if ($entity->status()) {
+      $row['url'] = array(
+        '#type' => 'link',
+        '#title' => $row['url'],
+        '#route_name' => 'search.view_' . $entity->id(),
+      );
+    }
+
+    $definition = $entity->getPlugin()->getPluginDefinition();
+    $row['plugin']['#markup'] = $definition['title'];
+
+    if ($entity->isDefaultSearch()) {
+      $status = $this->t('Default');
+    }
+    elseif ($entity->status()) {
+      $status = $this->t('Enabled');
+    }
+    else {
+      $status = $this->t('Disabled');
+    }
+    $row['status']['#markup'] = $status;
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $form = parent::buildForm($form, $form_state);
+    $search_settings = $this->configFactory->get('search.settings');
+    // Collect some stats.
+    $remaining = 0;
+    $total = 0;
+    foreach ($this->entities as $entity) {
+      if ($entity->isIndexable() && $status = $entity->getPlugin()->indexStatus()) {
+        $remaining += $status['remaining'];
+        $total += $status['total'];
+      }
+    }
+
+    $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))) . '%';
+    $status = '<p><strong>' . $this->t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '</strong></p>';
+    $form['status'] = array(
+      '#type' => 'details',
+      '#title' => $this->t('Indexing status'),
+    );
+    $form['status']['status'] = array('#markup' => $status);
+    $form['status']['wipe'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Re-index site'),
+      '#submit' => array(array($this, 'searchAdminReindexSubmit')),
+    );
+
+    $items = MapArray::copyValuesToKeys(array(10, 20, 50, 100, 200, 500));
+
+    // Indexing throttle:
+    $form['indexing_throttle'] = array(
+      '#type' => 'details',
+      '#title' => $this->t('Indexing throttle')
+    );
+    $form['indexing_throttle']['cron_limit'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Number of items to index per cron run'),
+      '#default_value' => $search_settings->get('index.cron_limit'),
+      '#options' => $items,
+      '#description' => $this->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' => url('admin/reports/status'))),
+    );
+    // Indexing settings:
+    $form['indexing_settings'] = array(
+      '#type' => 'details',
+      '#title' => $this->t('Indexing settings')
+    );
+    $form['indexing_settings']['info'] = array(
+      '#markup' => $this->t('<p><em>Changing the settings below will cause the site index to be rebuilt. The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed.</em></p><p><em>The default settings should be appropriate for the majority of sites.</em></p>')
+    );
+    $form['indexing_settings']['minimum_word_size'] = array(
+      '#type' => 'number',
+      '#title' => $this->t('Minimum word length to index'),
+      '#default_value' => $search_settings->get('index.minimum_word_size'),
+      '#min' => 1,
+      '#max' => 1000,
+      '#description' => $this->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).')
+    );
+    $form['indexing_settings']['overlap_cjk'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Simple CJK handling'),
+      '#default_value' => $search_settings->get('index.overlap_cjk'),
+      '#description' => $this->t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.')
+    );
+
+    $form['search_pages'] = array(
+      '#type' => 'details',
+      '#title' => $this->t('Search pages'),
+    );
+    $form['search_pages']['add_page'] = array(
+      '#type' => 'container',
+      '#attributes' => array(
+        'class' => array('container-inline'),
+      ),
+      '#attached' => array(
+        'css' => array(
+          drupal_get_path('module', 'search') . '/css/search.admin.css',
+        ),
+      ),
+    );
+    // In order to prevent validation errors for the parent form, this cannot be
+    // required, see self::validateAddSearchPage().
+    $form['search_pages']['add_page']['search_type'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Search page type'),
+      '#empty_option' => $this->t('- Choose page type -'),
+      '#options' => array_map(function ($definition) {
+        return $definition['title'];
+      }, $this->searchManager->getDefinitions()),
+    );
+    $form['search_pages']['add_page']['add_search_submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Add new page'),
+      '#validate' => array(array($this, 'validateAddSearchPage')),
+      '#submit' => array(array($this, 'submitAddSearchPage')),
+      '#limit_validation_errors' => array(array('search_type')),
+    );
+
+    // Move the listing into the search_pages element.
+    $form['search_pages'][$this->entitiesKey] = $form[$this->entitiesKey];
+    $form['search_pages'][$this->entitiesKey]['#empty'] = $this->t('No search pages have been configured.');
+    unset($form[$this->entitiesKey]);
+
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Save configuration'),
+      '#button_type' => 'primary',
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperations(EntityInterface $entity) {
+    /** @var $entity \Drupal\search\SearchPageInterface */
+    $operations = parent::getOperations($entity);
+
+    // Prevent the default search from being disabled or deleted.
+    if ($entity->isDefaultSearch()) {
+      unset($operations['disable'], $operations['delete']);
+    }
+    else {
+      $operations['default'] = array(
+        'title' => $this->t('Set as default'),
+        'route_name' => 'search.set_default',
+        'route_parameters' => array(
+          'search_page' => $entity->id(),
+        ),
+        'weight' => 50,
+      );
+    }
+
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    parent::submitForm($form, $form_state);
+
+    $search_settings = $this->configFactory->get('search.settings');
+    // If these settings change, the index needs to be rebuilt.
+    if (($search_settings->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($search_settings->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) {
+      $search_settings->set('index.minimum_word_size', $form_state['values']['minimum_word_size']);
+      $search_settings->set('index.overlap_cjk', $form_state['values']['overlap_cjk']);
+      drupal_set_message($this->t('The index will be rebuilt.'));
+      search_reindex();
+    }
+
+    $search_settings
+      ->set('index.cron_limit', $form_state['values']['cron_limit'])
+      ->save();
+
+    drupal_set_message($this->t('The configuration options have been saved.'));
+  }
+
+  /**
+   * Form submission handler for the reindex button on the search admin settings
+   * form.
+   */
+  public function searchAdminReindexSubmit(array &$form, array &$form_state) {
+    // Send the user to the confirmation page.
+    $form_state['redirect_route']['route_name'] = 'search.reindex_confirm';
+  }
+
+  /**
+   * Form validation handler for adding a new search page.
+   */
+  public function validateAddSearchPage(array &$form, array &$form_state) {
+    if (empty($form_state['values']['search_type'])) {
+      $this->formBuilder()->setErrorByName('search_type', $form_state, $this->t('You must select the new search page type.'));
+    }
+  }
+
+  /**
+   * Form submission handler for adding a new search page.
+   */
+  public function submitAddSearchPage(array &$form, array &$form_state) {
+    $form_state['redirect_route'] = array(
+      'route_name' => 'search.add_type',
+      'route_parameters' => array(
+        'search_plugin_id' => $form_state['values']['search_type'],
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/SearchPageRepository.php b/core/modules/search/lib/Drupal/search/SearchPageRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2af5ffd52ef7b583d19c0a1ca81048c56edd11f
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/SearchPageRepository.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\SearchPageRepository.
+ */
+
+namespace Drupal\search;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Entity\EntityManagerInterface;
+
+/**
+ * Provides a repository for Search Page config entities.
+ */
+class SearchPageRepository implements SearchPageRepositoryInterface {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * The search page storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageControllerInterface
+   */
+  protected $storage;
+
+  /**
+   * Constructs a new SearchPageRepository.
+   *
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   */
+  public function __construct(ConfigFactory $config_factory, EntityManagerInterface $entity_manager) {
+    $this->configFactory = $config_factory;
+    $this->storage = $entity_manager->getStorageController('search_page');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActiveSearchPages() {
+    $ids = $this->getQuery()
+      ->condition('status', TRUE)
+      ->execute();
+    return $this->storage->loadMultiple($ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSearchActive() {
+    return (bool) $this->getQuery()
+      ->condition('status', TRUE)
+      ->range(0, 1)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIndexableSearchPages() {
+    return array_filter($this->getActiveSearchPages(), function (SearchPageInterface $search) {
+      return $search->isIndexable();
+    });
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultSearchPage() {
+    // Find all active search pages (without loading them).
+    $search_pages = $this->getQuery()
+      ->condition('status', TRUE)
+      ->execute();
+
+    // If the default page is active, return it.
+    $default = $this->configFactory->get('search.settings')->get('default_page');
+    if (isset($search_pages[$default])) {
+      return $default;
+    }
+
+    // Otherwise, use the first active search page.
+    return reset($search_pages);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearDefaultSearchPage() {
+    $this->configFactory->get('search.settings')->clear('default_page')->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDefaultSearchPage(SearchPageInterface $search_page) {
+    $this->configFactory->get('search.settings')->set('default_page', $search_page->id())->save();
+    $search_page->enable()->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sortSearchPages($search_pages) {
+    $entity_info = $this->storage->entityInfo();
+    uasort($search_pages, array($entity_info->getClass(), 'sort'));
+    return $search_pages;
+  }
+
+  /**
+   * Returns an entity query instance.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface
+   *   The query instance.
+   */
+  protected function getQuery() {
+    return $this->storage->getQuery();
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/SearchPageRepositoryInterface.php b/core/modules/search/lib/Drupal/search/SearchPageRepositoryInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..b75202f9bff33e82dced5454ca6b6aeadf01e4c7
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/SearchPageRepositoryInterface.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\SearchPageRepositoryInterface;
+ */
+
+namespace Drupal\search;
+
+/**
+ * Provides the interface for a repository Search Page entities.
+ */
+interface SearchPageRepositoryInterface {
+
+  /**
+   * Returns all active search page entities.
+   *
+   * @return \Drupal\search\SearchPageInterface[]
+   *   An array of active search page entities.
+   */
+  public function getActiveSearchPages();
+
+  /**
+   * Returns whether search is active.
+   *
+   * @return bool
+   *   TRUE if at least one search is active, FALSE otherwise.
+   */
+  public function isSearchActive();
+
+  /**
+   * Returns all indexable search page entities.
+   *
+   * @return \Drupal\search\SearchPageInterface[]
+   *   An array of indexable search page entities.
+   */
+  public function getIndexableSearchPages();
+
+  /**
+   * Returns the default search page.
+   *
+   * @return \Drupal\search\SearchPageInterface|bool
+   *   The search page entity, or FALSE if no pages are active.
+   */
+  public function getDefaultSearchPage();
+
+  /**
+   * Sets a given search page as the default.
+   *
+   * @param \Drupal\search\SearchPageInterface $search_page
+   *   The search page entity.
+   *
+   * @return static
+   */
+  public function setDefaultSearchPage(SearchPageInterface $search_page);
+
+  /**
+   * Clears the default search page.
+   */
+  public function clearDefaultSearchPage();
+
+  /**
+   * Sorts a list of search pages.
+   *
+   * @param \Drupal\search\SearchPageInterface[] $search_pages
+   *   The unsorted list of search pages.
+   *
+   * @return \Drupal\search\SearchPageInterface[]
+   *   The sorted list of search pages.
+   */
+  public function sortSearchPages($search_pages);
+
+}
diff --git a/core/modules/search/lib/Drupal/search/SearchPluginManager.php b/core/modules/search/lib/Drupal/search/SearchPluginManager.php
index dac919b3f277c875e26a261ecdc97bba52a1d92e..56075d352a6960fb4b9bd7d476bd2e13b3d7087a 100644
--- a/core/modules/search/lib/Drupal/search/SearchPluginManager.php
+++ b/core/modules/search/lib/Drupal/search/SearchPluginManager.php
@@ -7,10 +7,8 @@
 
 namespace Drupal\search;
 
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Language\LanguageManager;
 
@@ -20,103 +18,23 @@
 class SearchPluginManager extends DefaultPluginManager {
 
   /**
-   * The config factory.
+   * Constructs SearchPluginManager
    *
-   * @var \Drupal\Core\Config\ConfigFactory
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Language\LanguageManager $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
    */
-  protected $configFactory;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(\Traversable $namespaces, ConfigFactory $config_factory, CacheBackendInterface $cache_backend, LanguageManager $language_manager) {
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) {
     parent::__construct('Plugin/Search', $namespaces, 'Drupal\search\Annotation\SearchPlugin');
 
-    $this->configFactory = $config_factory;
     $this->setCacheBackend($cache_backend, $language_manager, 'search_plugins');
+    // @todo Set an alter hook.
   }
 
-  /**
-   * {@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\Access\AccessibleInterface')) {
-      return $this->createInstance($plugin_id)->access('view', $account);
-    }
-    return TRUE;
-  }
 }
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php
index 1a528772cf9abc1e136c66e380f07bba23c106db..2fa66a74fdc9811dc9e831a3901183598ccca2c1 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchBlockTest.php
@@ -52,7 +52,8 @@ protected function testSearchFormBlock() {
 
     // Test a normal search via the block form, from the front page.
     $terms = array('search_block_form' => 'test');
-    $this->drupalPostForm('node', $terms, t('Search'));
+    $this->drupalPostForm('', $terms, t('Search'));
+    $this->assertResponse(200);
     $this->assertText('Your search yielded no results');
 
     // Test a search from the block on a 404 page.
@@ -66,7 +67,8 @@ protected function testSearchFormBlock() {
     $visibility['path']['pages'] = 'search';
     $block->set('visibility', $visibility);
 
-    $this->drupalPostForm('node', $terms, t('Search'));
+    $this->drupalPostForm('', $terms, t('Search'));
+    $this->assertResponse(200);
     $this->assertText('Your search yielded no results');
 
     // Confirm that the user is redirected to the search page.
@@ -78,7 +80,8 @@ protected function testSearchFormBlock() {
 
     // Test an empty search via the block form, from the front page.
     $terms = array('search_block_form' => '');
-    $this->drupalPostForm('node', $terms, t('Search'));
+    $this->drupalPostForm('', $terms, t('Search'));
+    $this->assertResponse(200);
     $this->assertText('Please enter some keywords');
 
     // Confirm that the user is redirected to the search page, when form is
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php
index 205071df06fe9bab3a39e9863fce60cf26596f0d..0cff49db4d6745cc4e4ea18054b74813112f0378 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php
@@ -19,7 +19,18 @@ class SearchConfigSettingsFormTest extends SearchTestBase {
    */
   public static $modules = array('block', 'search_extra_type');
 
+  /**
+   * User who can search and administer search.
+   *
+   * @var \Drupal\user\UserInterface
+   */
   public $search_user;
+
+  /**
+   * Node indexed for searching.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
   public $search_node;
 
   public static function getInfo() {
@@ -86,28 +97,8 @@ function testSearchSettingsPage() {
    * 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 plugin is not present on
-    // the page
-    $this->assertNoText(t('Extra type settings'));
-    $this->assertNoText(t('Boost method'));
-
-    // 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 plugin
-    $edit['active_plugins[search_extra_type_search]'] = 'search_extra_type_search';
-    $edit['default_plugin'] = 'search_extra_type_search';
-    $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration'));
-
-    // Ensure that the settings fieldset is visible after enabling search for
-    // the test plugin
-    $this->assertText(t('Extra type settings'));
-    $this->assertText(t('Boost method'));
+    $this->clickLink(t('Edit'), 1);
 
     // Ensure that the default setting was picked up from the default config
     $this->assertTrue($this->xpath('//select[@id="edit-extra-type-settings-boost"]//option[@value="bi" and @selected="selected"]'), 'Module specific settings are picked up from the default config');
@@ -115,66 +106,60 @@ function testSearchModuleSettingsPage() {
     // Change extra type setting and also modify a common search setting.
     $edit = array(
       'extra_type_settings[boost]' => 'ii',
-      'minimum_word_size' => 5,
     );
-    $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration'));
+    $this->drupalPostForm(NULL, $edit, t('Save search page'));
 
     // Ensure that the modifications took effect.
-    $this->assertText(t('The configuration options have been saved.'));
+    $this->assertRaw(t('The %label search page has been updated.', array('%label' => 'Dummy search type')));
+    $this->drupalGet('admin/config/search/settings/manage/dummy_search_type');
     $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 plugin-specific form is active');
   }
 
   /**
    * Verifies that you can disable individual search plugins.
    */
   function testSearchModuleDisabling() {
-    // 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.
+    // Array of search plugins to test: 'keys' are the keywords to search for,
+    // and 'text' is the text to assert is on the results page.
     $plugin_info = array(
       'node_search' => array(
-        'path' => 'node',
-        'title' => 'Content',
         'keys' => 'pizza',
         'text' => $this->search_node->label(),
       ),
       'user_search' => array(
-        'path' => 'user',
-        'title' => 'User',
         'keys' => $this->search_user->getUsername(),
         'text' => $this->search_user->getEmail(),
       ),
-      'search_extra_type_search' => array(
-        'path' => 'dummy_path',
-        'title' => 'Dummy search type',
+      'dummy_search_type' => array(
         'keys' => 'foo',
         'text' => 'Dummy search snippet to display',
       ),
     );
     $plugins = array_keys($plugin_info);
+    /** @var $entities \Drupal\search\SearchPageInterface[] */
+    $entities = entity_load_multiple('search_page');
+    // Disable all of the search pages.
+    foreach ($entities as $entity) {
+      $entity->disable()->save();
+    }
 
     // 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 ($plugins as $other) {
-        $edit['active_plugins[' . $other . ']'] = (($other == $plugin) ? $plugin : FALSE);
-      }
-      $edit['default_plugin'] = $plugin;
-      $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration'));
+    foreach ($entities as $entity_id => $entity) {
+      // Set this as default.
+      $this->drupalGet("admin/config/search/settings/manage/$entity_id/set-default");
 
       // Run a search from the correct search URL.
-      $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']);
-      $this->assertNoText('no results', $info['title'] . ' search found results');
+      $info = $plugin_info[$entity_id];
+      $this->drupalGet('search/' . $entity->getPath() . '/' . $info['keys']);
+      $this->assertResponse(200);
+      $this->assertNoText('no results', $entity->label() . ' search found results');
       $this->assertText($info['text'], 'Correct search text found');
 
-      // Verify that other plugin search tab titles are not visible.
+      // Verify that other plugin search tab labels are not visible.
       foreach ($plugins as $other) {
-        if ($other != $plugin) {
-          $title = $plugin_info[$other]['title'];
-          $this->assertNoText($title, $title . ' search tab is not shown');
+        if ($other != $entity_id) {
+          $label = $entities[$other]->label();
+          $this->assertNoText($label, $label . ' search tab is not shown');
         }
       }
 
@@ -184,33 +169,194 @@ function testSearchModuleDisabling() {
       $this->drupalPostForm('node', $terms, t('Search'));
       $this->assertEqual(
         $this->getURL(),
-        url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)),
+        \Drupal::url('search.view_' . $entity->id(), array('keys' => $info['keys']), array('absolute' => TRUE)),
         'Block redirected to right search page');
 
-      // Try an invalid search path. Should redirect to our active plugin.
+      // Try an invalid search path, which should 404.
       $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');
+      $this->assertResponse(404);
+
+      $entity->disable()->save();
     }
 
     // Test with all search plugins enabled. When you go to the search
     // page or run search, all plugins should be shown.
-    $edit = array();
-    foreach ($plugins as $plugin) {
-      $edit['active_plugins[' . $plugin . ']'] = $plugin;
+    foreach ($entities as $entity) {
+      $entity->enable()->save();
     }
-    $edit['default_plugin'] = 'node_search';
-
-    $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration'));
+    // Set the node search as default.
+    $this->drupalGet('admin/config/search/settings/manage/node_search/set-default');
 
     foreach (array('search/node/pizza', 'search/node') as $path) {
       $this->drupalGet($path);
-      foreach ($plugins as $plugin) {
-        $title = $plugin_info[$plugin]['title'];
-        $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title)));
+      foreach ($plugins as $entity_id) {
+        $label = $entities[$entity_id]->label();
+        $this->assertText($label, format_string('%label search tab is shown', array('%label' => $label)));
       }
     }
   }
+
+  /**
+   * Tests the ordering of search pages on a clean install.
+   */
+  public function testDefaultSearchPageOrdering() {
+    $this->drupalGet('search');
+    $elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary'));
+    $this->assertIdentical((string) $elements[0]['href'], url('search/node'));
+    $this->assertIdentical((string) $elements[1]['href'], url('search/user'));
+  }
+
+  /**
+   * Tests multiple search pages of the same type.
+   */
+  public function testMultipleSearchPages() {
+    $this->assertDefaultSearch('node_search', 'The default page is set to the installer default.');
+    $search_storage = \Drupal::entityManager()->getStorageController('search_page');
+    $entities = $search_storage->loadMultiple();
+    $search_storage->delete($entities);
+    $this->assertDefaultSearch(FALSE);
+
+    // Ensure that no search pages are configured.
+    $this->drupalGet('admin/config/search/settings');
+    $this->assertText(t('No search pages have been configured.'));
+
+    // Add a search page.
+    $edit = array();
+    $edit['search_type'] = 'search_extra_type_search';
+    $this->drupalPostForm(NULL, $edit, t('Add new page'));
+    $this->assertTitle('Add new search page | Drupal');
+
+    $first = array();
+    $first['label'] = $this->randomString();
+    $first_id = $first['id'] = strtolower($this->randomName(8));
+    $first['path'] = strtolower($this->randomName(8));
+    $this->drupalPostForm(NULL, $first, t('Add search page'));
+    $this->assertDefaultSearch($first_id, 'The default page matches the only search page.');
+    $this->assertRaw(t('The %label search page has been added.', array('%label' => $first['label'])));
+
+    // Attempt to add a search page with an existing path.
+    $edit = array();
+    $edit['search_type'] = 'search_extra_type_search';
+    $this->drupalPostForm(NULL, $edit, t('Add new page'));
+    $edit = array();
+    $edit['label'] = $this->randomString();
+    $edit['id'] = strtolower($this->randomName(8));
+    $edit['path'] = $first['path'];
+    $this->drupalPostForm(NULL, $edit, t('Add search page'));
+    $this->assertText(t('The search page path must be unique.'));
+
+    // Add a second search page.
+    $second = array();
+    $second['label'] = $this->randomString();
+    $second_id = $second['id'] = strtolower($this->randomName(8));
+    $second['path'] = strtolower($this->randomName(8));
+    $this->drupalPostForm(NULL, $second, t('Add search page'));
+    $this->assertDefaultSearch($first_id, 'The default page matches the only search page.');
+
+    // Ensure both search pages have their tabs displayed.
+    $this->drupalGet('search');
+    $elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary'));
+    $this->assertIdentical((string) $elements[0]['href'], url('search/' . $first['path']));
+    $this->assertIdentical((string) $elements[1]['href'], url('search/' . $second['path']));
+
+    // Switch the weight of the search pages and check the order of the tabs.
+    $edit = array(
+      'entities[' . $first_id . '][weight]' => 10,
+      'entities[' . $second_id . '][weight]' => -10,
+    );
+    $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration'));
+    $this->drupalGet('search');
+    $elements = $this->xpath('//*[contains(@class, :class)]//a', array(':class' => 'tabs primary'));
+    $this->assertIdentical((string) $elements[0]['href'], url('search/' . $second['path']));
+    $this->assertIdentical((string) $elements[1]['href'], url('search/' . $first['path']));
+
+    // Check the initial state of the search pages.
+    $this->drupalGet('admin/config/search/settings');
+    $this->verifySearchPageOperations($first_id, TRUE, FALSE, FALSE, FALSE);
+    $this->verifySearchPageOperations($second_id, TRUE, TRUE, TRUE, FALSE);
+
+    // Change the default search page.
+    $this->clickLink(t('Set as default'));
+    $this->assertRaw(t('The default search page is now %label. Be sure to check the ordering of your search pages.', array('%label' => $second['label'])));
+    $this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE);
+    $this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
+
+    // Disable the first search page.
+    $this->clickLink(t('Disable'));
+    $this->assertResponse(200);
+    $this->assertNoLink(t('Disable'));
+    $this->verifySearchPageOperations($first_id, TRUE, TRUE, FALSE, TRUE);
+    $this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
+
+    // Enable the first search page.
+    $this->clickLink(t('Enable'));
+    $this->assertResponse(200);
+    $this->verifySearchPageOperations($first_id, TRUE, TRUE, TRUE, FALSE);
+    $this->verifySearchPageOperations($second_id, TRUE, FALSE, FALSE, FALSE);
+
+    // Test deleting.
+    $this->clickLink(t('Delete'));
+    $this->assertRaw(t('Are you sure you want to delete the %label search page?', array('%label' => $first['label'])));
+    $this->drupalPostForm(NULL, array(), t('Delete'));
+    $this->assertRaw(t('The %label search page has been deleted.', array('%label' => $first['label'])));
+    $this->verifySearchPageOperations($first_id, FALSE, FALSE, FALSE, FALSE);
+  }
+
+  /**
+   * Checks that the search page operations match expectations.
+   *
+   * @param string $id
+   *   The search page ID to check.
+   * @param bool $edit
+   *   Whether the edit link is expected.
+   * @param bool $delete
+   *   Whether the delete link is expected.
+   * @param bool $disable
+   *   Whether the disable link is expected.
+   * @param bool $enable
+   *   Whether the enable link is expected.
+   */
+  protected function verifySearchPageOperations($id, $edit, $delete, $disable, $enable) {
+    if ($edit) {
+      $this->assertLinkByHref("admin/config/search/settings/manage/$id");
+    }
+    else {
+      $this->assertNoLinkByHref("admin/config/search/settings/manage/$id");
+    }
+    if ($delete) {
+      $this->assertLinkByHref("admin/config/search/settings/manage/$id/delete");
+    }
+    else {
+      $this->assertNoLinkByHref("admin/config/search/settings/manage/$id/delete");
+    }
+    if ($disable) {
+      $this->assertLinkByHref("admin/config/search/settings/manage/$id/disable");
+    }
+    else {
+      $this->assertNoLinkByHref("admin/config/search/settings/manage/$id/disable");
+    }
+    if ($enable) {
+      $this->assertLinkByHref("admin/config/search/settings/manage/$id/enable");
+    }
+    else {
+      $this->assertNoLinkByHref("admin/config/search/settings/manage/$id/enable");
+    }
+  }
+
+  /**
+   * Checks that the default search page matches expectations.
+   *
+   * @param string $expected
+   *   The expected search page.
+   * @param string $message
+   *   (optional) A message to display with the assertion.
+   * @param string $group
+   *   (optional) The group this message is in.
+   */
+  protected function assertDefaultSearch($expected, $message = '', $group = 'Other') {
+    /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
+    $search_page_repository = \Drupal::service('search.search_page_repository');
+    $this->assertIdentical($search_page_repository->getDefaultSearchPage(), $expected, $message, $group);
+  }
+
 }
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php
index 67deae356bc44bbe2575526478c65302c7d6e097..cc9da27ffda8099adbd26043ca0796d9d3ffb952 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php
@@ -10,11 +10,11 @@
 class SearchRankingTest extends SearchTestBase {
 
   /**
-   * A node search plugin instance.
+   * The node search page.
    *
-   * @var \Drupal\search\Plugin\SearchInterface
+   * @var \Drupal\search\SearchPageInterface
    */
-  protected $nodeSearchPlugin;
+  protected $nodeSearch;
 
   /**
    * Modules to enable.
@@ -35,7 +35,7 @@ public function setUp() {
     parent::setUp();
 
     // Create a plugin instance.
-    $this->nodeSearchPlugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
+    $this->nodeSearch = entity_load('search_page', 'node_search');
   }
 
   public function testRankings() {
@@ -48,6 +48,7 @@ public function testRankings() {
     $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
 
     // Create nodes for testing.
+    $nodes = array();
     foreach ($node_ranks as $node_rank) {
       $settings = array(
         'type' => 'page',
@@ -80,12 +81,9 @@ public function testRankings() {
     }
 
     // Update the search index.
-    $this->nodeSearchPlugin->updateIndex();
+    $this->nodeSearch->getPlugin()->updateIndex();
     search_update_totals();
 
-    // Refresh variables after the treatment.
-    $this->refreshVariables();
-
     // Add a comment to one of the nodes.
     $edit = array();
     $edit['subject'] = 'my comment title';
@@ -115,35 +113,39 @@ public function testRankings() {
     array_pop($node_ranks);
 
     // Test that the settings form displays the context ranking section.
-    $this->drupalGet('admin/config/search/settings');
+    $this->drupalGet('admin/config/search/settings/manage/node_search');
     $this->assertText(t('Content ranking'));
 
     // Check that all rankings are visible and set to 0.
     foreach ($node_ranks as $node_rank) {
-      $this->assertTrue($this->xpath('//select[@id="edit-node-rank-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
+      $this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
     }
 
     // Test each of the possible rankings.
     $edit = array();
     foreach ($node_ranks as $node_rank) {
       // Enable the ranking we are testing.
-      $edit['node_rank_' . $node_rank] = 10;
-      $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration'));
-      $this->assertTrue($this->xpath('//select[@id="edit-node-rank-' . $node_rank . '"]//option[@value="10"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 10.');
+      $edit['rankings_' . $node_rank] = 10;
+      $this->drupalPostForm('admin/config/search/settings/manage/node_search', $edit, t('Save search page'));
+      $this->drupalGet('admin/config/search/settings/manage/node_search');
+      $this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '"]//option[@value="10"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 10.');
 
+      // Reload the plugin to get the up-to-date values.
+      $this->nodeSearch = entity_load('search_page', 'node_search');
       // Do the search and assert the results.
-      $this->nodeSearchPlugin->setSearch('rocks', array(), array());
-      $set = $this->nodeSearchPlugin->execute();
+      $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
+      $set = $this->nodeSearch->getPlugin()->execute();
       $this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.');
       // Clear this ranking for the next test.
-      $edit['node_rank_' . $node_rank] = 0;
+      $edit['rankings_' . $node_rank] = 0;
     }
 
     // Save the final node_rank change then check that all rankings are visible
     // and have been set back to 0.
-    $this->drupalPostForm('admin/config/search/settings', $edit, t('Save configuration'));
+    $this->drupalPostForm('admin/config/search/settings/manage/node_search', $edit, t('Save search page'));
+    $this->drupalGet('admin/config/search/settings/manage/node_search');
     foreach ($node_ranks as $node_rank) {
-      $this->assertTrue($this->xpath('//select[@id="edit-node-rank-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
+      $this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.');
     }
   }
 
@@ -170,6 +172,7 @@ public function testHTMLRankings() {
       'type' => 'page',
       'title' => 'Simple node',
     );
+    $nodes = array();
     foreach ($shuffled_tags as $tag) {
       switch ($tag) {
         case 'a':
@@ -186,20 +189,12 @@ public function testHTMLRankings() {
     }
 
     // Update the search index.
-    $this->nodeSearchPlugin->updateIndex();
+    $this->nodeSearch->getPlugin()->updateIndex();
     search_update_totals();
 
-    // Refresh variables after the treatment.
-    $this->refreshVariables();
-
-    // Disable all other rankings.
-    $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views');
-    foreach ($node_ranks as $node_rank) {
-      variable_set('node_rank_' . $node_rank, 0);
-    }
-    $this->nodeSearchPlugin->setSearch('rocks', array(), array());
+    $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
     // Do the search and assert the results.
-    $set = $this->nodeSearchPlugin->execute();
+    $set = $this->nodeSearch->getPlugin()->execute();
 
     // Test the ranking of each tag.
     foreach ($sorted_tags as $tag_rank => $tag) {
@@ -218,14 +213,12 @@ public function testHTMLRankings() {
       $node = $this->drupalCreateNode($settings);
 
       // Update the search index.
-      $this->nodeSearchPlugin->updateIndex();
+      $this->nodeSearch->getPlugin()->updateIndex();
       search_update_totals();
 
-      // Refresh variables after the treatment.
-      $this->refreshVariables();
-      $this->nodeSearchPlugin->setSearch('rocks', array(), array());
+      $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
       // Do the search and assert the results.
-      $set = $this->nodeSearchPlugin->execute();
+      $set = $this->nodeSearch->getPlugin()->execute();
 
       // Ranking should always be second to last.
       $set = array_slice($set, -2, 1);
@@ -247,35 +240,36 @@ function testDoubleRankings() {
     // Login with sufficient privileges.
     $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content')));
 
-    // See testRankings() above - build a node that will rank high for sticky.
+    // Create two nodes that will match the search, one that is sticky.
     $settings = array(
       'type' => 'page',
       'title' => 'Drupal rocks',
       'body' => array(array('value' => "Drupal's search rocks")),
-      'sticky' => 1,
     );
-
+    $this->drupalCreateNode($settings);
+    $settings['sticky'] = 1;
     $node = $this->drupalCreateNode($settings);
 
     // Update the search index.
-    $this->nodeSearchPlugin->updateIndex();
+    $this->nodeSearch->getPlugin()->updateIndex();
     search_update_totals();
 
-    // Refresh variables after the treatment.
-    $this->refreshVariables();
-
     // Set up for ranking sticky and lots of comments; make sure others are
     // disabled.
     $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
+    $configuration = $this->nodeSearch->getPlugin()->getConfiguration();
     foreach ($node_ranks as $var) {
       $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0;
-      variable_set('node_rank_' . $var, $value);
+      $configuration['rankings'][$var] = $value;
     }
+    $this->nodeSearch->getPlugin()->setConfiguration($configuration);
+    $this->nodeSearch->save();
 
     // Do the search and assert the results.
-    $this->nodeSearchPlugin->setSearch('rocks', array(), array());
+    $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array());
     // Do the search and assert the results.
-    $set = $this->nodeSearchPlugin->execute();
+    $set = $this->nodeSearch->getPlugin()->execute();
     $this->assertEqual($set[0]['node']->id(), $node->id(), 'Search double ranking order.');
   }
+
 }
diff --git a/core/modules/search/search.install b/core/modules/search/search.install
index 824a624365ef4dfbc1ad5565f9de0155478036aa..955bfed400e119a16a5d139d64541f04e0bed41e 100644
--- a/core/modules/search/search.install
+++ b/core/modules/search/search.install
@@ -170,7 +170,7 @@ function _search_update_8000_modules_mapto_plugins(array $map) {
   }
   $default_module = update_variable_get('search_default_module', 'node');
   if (isset($map[$default_module])) {
-    $config->set('default_plugin', $map[$default_module]);
+    $config->set('default_page', $map[$default_module]);
     update_variable_del('search_default_module');
   }
   $config->save();
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index ca160bfcd2448021e4dd150addcd70b58be9bc1a..551f435c90d1d66513fe1f9c873488ed1d07e59b 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -5,10 +5,7 @@
  * Enables site-wide keyword searching.
  */
 
-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)
@@ -170,23 +167,6 @@ function search_menu() {
   return $items;
 }
 
-/**
- * Returns information about the default search plugin.
- *
- * @return array
- *   The search plugin definition for the default search plugin, if any.
- */
-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 config setting does not match any active plugin, so just return
-  // the info for the first active plugin (if any).
-  return reset($info);
-}
-
 /**
  * Clears either a part of, or the entire search index.
  *
@@ -201,12 +181,14 @@ function search_get_default_plugin_info() {
  *   (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.
+ *   index records for the $sid and $type will be deleted.
  */
 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();
+    /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
+    $search_page_repository = \Drupal::service('search.search_page_repository');
+    foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
+      $entity->getPlugin()->resetIndex();
     }
   }
   else {
@@ -247,7 +229,8 @@ function search_dirty($word = NULL) {
 /**
  * Implements hook_cron().
  *
- * Fires updateIndex() in all plugins and cleans up dirty words.
+ * Fires updateIndex() in the plugins for all indexable active search pages,
+ * and cleans up dirty words.
  *
  * @see search_dirty()
  */
@@ -256,8 +239,10 @@ function search_cron() {
   // to date.
   drupal_register_shutdown_function('search_update_totals');
 
-  foreach (\Drupal::service('plugin.manager.search')->getActiveIndexingPlugins() as $plugin) {
-    $plugin->updateIndex();
+  /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */
+  $search_page_repository = \Drupal::service('search.search_page_repository');
+  foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
+    $entity->getPlugin()->updateIndex();
   }
 }
 
@@ -598,7 +583,8 @@ function search_mark_for_reindex($type, $sid) {
  *
  * To be discovered, the plugins must implement
  * \Drupal\search\Plugin\SearchInterface and be annotated as
- * \Drupal\search\Annotation\SearchPlugin plugins.
+ * \Drupal\search\Annotation\SearchPlugin plugins. Defining a plugin will allow
+ * administrators to set up one or more search pages using this plugin.
  *
  * There are three ways to interact with the search system:
  * - Specifically for searching nodes, you can implement
@@ -609,10 +595,11 @@ function search_mark_for_reindex($type, $sid) {
  *   additional, non-visible data to be indexed.
  * - 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.
+ *   search page type that users can use to set up one or more search pages.
+ *   Each of these corresponds to a tab on the /search page, which can be
+ *   used to perform searches. 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. In that case, you may wish to define it as a
@@ -851,3 +838,11 @@ function _search_find_match_with_simplify($key, $text, $boundary, $langcode = NU
   // If we get here, we couldn't find a match.
   return NULL;
 }
+
+/**
+ * Implements hook_module_preinstall().
+ */
+function search_module_preinstall() {
+  // @todo Remove in https://drupal.org/node/2155635.
+  \Drupal::service('plugin.manager.search')->clearCachedDefinitions();
+}
diff --git a/core/modules/search/search.routing.yml b/core/modules/search/search.routing.yml
index a3247bc67562ffbafc4eef9a7a4ed45cd634cfd9..8cdec79d97c82124978901f0b1f3b689c4e2e92b 100644
--- a/core/modules/search/search.routing.yml
+++ b/core/modules/search/search.routing.yml
@@ -1,7 +1,7 @@
 search.settings:
   path: '/admin/config/search/settings'
   defaults:
-    _form: '\Drupal\search\Form\SearchSettingsForm'
+    _entity_list: 'search_page'
     _title: 'Search settings'
   requirements:
     _permission: 'administer search'
@@ -14,17 +14,51 @@ search.reindex_confirm:
   requirements:
     _permission: 'administer search'
 
-search.view:
-  path: '/search/{plugin_id}'
+search.add_type:
+  path: '/admin/config/search/settings/add/{search_plugin_id}'
   defaults:
-    _title: 'Search'
-    _content: '\Drupal\search\Controller\SearchController::view'
-    plugin_id: NULL
-    keys: ''
+    _entity_form: 'search_page.add'
+    _title: 'Add new search page'
   requirements:
-    keys: '.+'
-    _permission: 'search content'
-    _search_access: 'TRUE'
+    _entity_create_access: 'search_page'
+
+search.edit:
+  path: '/admin/config/search/settings/manage/{search_page}'
+  defaults:
+    _entity_form: 'search_page.edit'
+    _title_callback: '\Drupal\search\Controller\SearchController::editTitle'
+  requirements:
+    _entity_access: 'search_page.update'
+
+search.enable:
+  path: '/admin/config/search/settings/manage/{search_page}/enable'
+  defaults:
+    _controller: '\Drupal\search\Controller\SearchController::performOperation'
+    op: 'enable'
+  requirements:
+    _entity_access: 'search_page.update'
+
+search.disable:
+  path: '/admin/config/search/settings/manage/{search_page}/disable'
+  defaults:
+    _controller: '\Drupal\search\Controller\SearchController::performOperation'
+    op: 'disable'
+  requirements:
+    _entity_access: 'search_page.disable'
+
+search.set_default:
+  path: '/admin/config/search/settings/manage/{search_page}/set-default'
+  defaults:
+    _controller: '\Drupal\search\Controller\SearchController::setAsDefault'
+  requirements:
+    _entity_access: 'search_page.update'
+
+search.delete:
+  path: '/admin/config/search/settings/manage/{search_page}/delete'
+  defaults:
+    _entity_form: 'search_page.delete'
+  requirements:
+    _entity_access: 'search_page.delete'
 
 route_callbacks:
-  - '\Drupal\search\Routing\SearchPluginRoutes::routes'
+  - '\Drupal\search\Routing\SearchPageRoutes::routes'
diff --git a/core/modules/search/search.services.yml b/core/modules/search/search.services.yml
index 543bd09c4c535f9b0d8a9cf0b79d75c3da4b6a65..a92d161cc277d2bee8488c85f8b082a37e071973 100644
--- a/core/modules/search/search.services.yml
+++ b/core/modules/search/search.services.yml
@@ -1,16 +1,8 @@
 services:
   plugin.manager.search:
     class: Drupal\search\SearchPluginManager
-    arguments: ['@container.namespaces', '@config.factory', '@cache.cache', '@language_manager']
+    parent: default_plugin_manager
 
-  access_check.search:
-    class: Drupal\search\Access\SearchAccessCheck
-    arguments: ['@plugin.manager.search']
-    tags:
-      - { name: access_check, applies_to: _search_access }
-
-  access_check.search_plugin:
-    class: Drupal\search\Access\SearchPluginAccessCheck
-    arguments: ['@plugin.manager.search']
-    tags:
-      - { name: access_check, applies_to: _search_plugin_view_access }
+  search.search_page_repository:
+    class: Drupal\search\SearchPageRepository
+    arguments: ['@config.factory', '@entity.manager']
diff --git a/core/modules/search/tests/Drupal/search/Tests/SearchPageRepositoryTest.php b/core/modules/search/tests/Drupal/search/Tests/SearchPageRepositoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b054b71562a8982287214b10f072e7390e55e922
--- /dev/null
+++ b/core/modules/search/tests/Drupal/search/Tests/SearchPageRepositoryTest.php
@@ -0,0 +1,303 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Tests\SearchPageRepositoryTest.
+ */
+
+namespace Drupal\search\Tests;
+
+use Drupal\search\Entity\SearchPage;
+use Drupal\search\SearchPageRepository;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the search page repository.
+ *
+ * @see \Drupal\search\SearchPageRepository
+ *
+ * @group Drupal
+ * @group Search
+ */
+class SearchPageRepositoryTest extends UnitTestCase {
+
+  /**
+   * The search page repository.
+   *
+   * @var \Drupal\search\SearchPageRepository
+   */
+  protected $searchPageRepository;
+
+  /**
+   * The entity query object.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $query;
+
+  /**
+   * The search page storage.
+   *
+   * @var \Drupal\Core\Config\Entity\ConfigStorageController|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $storage;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $configFactory;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Search page repository test',
+      'description' => 'Tests methods on the \Drupal\search\SearchPageRepository class',
+      'group' => 'Search',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    $this->query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
+
+    $this->storage = $this->getMockBuilder('Drupal\Core\Config\Entity\ConfigStorageController')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->storage->expects($this->any())
+      ->method('getQuery')
+      ->will($this->returnValue($this->query));
+
+    $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+    $entity_manager->expects($this->any())
+      ->method('getStorageController')
+      ->will($this->returnValue($this->storage));
+
+    $this->configFactory = $this->getMockBuilder('Drupal\Core\Config\ConfigFactory')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->searchPageRepository = new SearchPageRepository($this->configFactory, $entity_manager);
+  }
+
+  /**
+   * Tests the getActiveSearchPages() method.
+   */
+  public function testGetActiveSearchPages() {
+    $this->query->expects($this->once())
+      ->method('condition')
+      ->with('status', TRUE)
+      ->will($this->returnValue($this->query));
+    $this->query->expects($this->once())
+      ->method('execute')
+      ->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test')));
+
+    $entities = array();
+    $entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
+    $entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
+    $this->storage->expects($this->once())
+      ->method('loadMultiple')
+      ->with(array('test' => 'test', 'other_test' => 'other_test'))
+      ->will($this->returnValue($entities));
+
+    $result = $this->searchPageRepository->getActiveSearchPages();
+    $this->assertSame($entities, $result);
+  }
+
+  /**
+   * Tests the isSearchActive() method.
+   */
+  public function testIsSearchActive() {
+    $this->query->expects($this->once())
+      ->method('condition')
+      ->with('status', TRUE)
+      ->will($this->returnValue($this->query));
+    $this->query->expects($this->once())
+      ->method('range')
+      ->with(0, 1)
+      ->will($this->returnValue($this->query));
+    $this->query->expects($this->once())
+      ->method('execute')
+      ->will($this->returnValue(array('test' => 'test')));
+
+    $this->assertSame(TRUE, $this->searchPageRepository->isSearchActive());
+  }
+
+  /**
+   * Tests the getIndexableSearchPages() method.
+   */
+  public function testGetIndexableSearchPages() {
+    $this->query->expects($this->once())
+      ->method('condition')
+      ->with('status', TRUE)
+      ->will($this->returnValue($this->query));
+    $this->query->expects($this->once())
+      ->method('execute')
+      ->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test')));
+
+    $entities = array();
+    $entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
+    $entities['test']->expects($this->once())
+      ->method('isIndexable')
+      ->will($this->returnValue(TRUE));
+    $entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
+    $entities['other_test']->expects($this->once())
+      ->method('isIndexable')
+      ->will($this->returnValue(FALSE));
+    $this->storage->expects($this->once())
+      ->method('loadMultiple')
+      ->with(array('test' => 'test', 'other_test' => 'other_test'))
+      ->will($this->returnValue($entities));
+
+    $result = $this->searchPageRepository->getIndexableSearchPages();
+    $this->assertCount(1, $result);
+    $this->assertSame($entities['test'], reset($result));
+  }
+
+  /**
+   * Tests the clearDefaultSearchPage() method.
+   */
+  public function testClearDefaultSearchPage() {
+    $config = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config->expects($this->once())
+      ->method('clear')
+      ->with('default_page')
+      ->will($this->returnValue($config));
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('search.settings')
+      ->will($this->returnValue($config));
+    $this->searchPageRepository->clearDefaultSearchPage();
+  }
+
+  /**
+   * Tests the getDefaultSearchPage() method when the default is active.
+   */
+  public function testGetDefaultSearchPageWithActiveDefault() {
+    $this->query->expects($this->once())
+      ->method('condition')
+      ->with('status', TRUE)
+      ->will($this->returnValue($this->query));
+    $this->query->expects($this->once())
+      ->method('execute')
+      ->will($this->returnValue(array('test' => 'test', 'other_test' => 'other_test')));
+
+    $config = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config->expects($this->once())
+      ->method('get')
+      ->with('default_page')
+      ->will($this->returnValue('test'));
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('search.settings')
+      ->will($this->returnValue($config));
+
+    $this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
+  }
+
+  /**
+   * Tests the getDefaultSearchPage() method when the default is inactive.
+   */
+  public function testGetDefaultSearchPageWithInactiveDefault() {
+    $this->query->expects($this->once())
+      ->method('condition')
+      ->with('status', TRUE)
+      ->will($this->returnValue($this->query));
+    $this->query->expects($this->once())
+      ->method('execute')
+      ->will($this->returnValue(array('test' => 'test')));
+
+    $config = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config->expects($this->once())
+      ->method('get')
+      ->with('default_page')
+      ->will($this->returnValue('other_test'));
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('search.settings')
+      ->will($this->returnValue($config));
+
+    $this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
+  }
+
+  /**
+   * Tests the setDefaultSearchPage() method.
+   */
+  public function testSetDefaultSearchPage() {
+    $id = 'bananas';
+    $config = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config->expects($this->once())
+      ->method('set')
+      ->with('default_page', $id)
+      ->will($this->returnValue($config));
+    $config->expects($this->once())
+      ->method('save')
+      ->will($this->returnValue($config));
+    $this->configFactory->expects($this->once())
+      ->method('get')
+      ->with('search.settings')
+      ->will($this->returnValue($config));
+
+    $search_page = $this->getMock('Drupal\search\SearchPageInterface');
+    $search_page->expects($this->once())
+      ->method('id')
+      ->will($this->returnValue($id));
+    $search_page->expects($this->once())
+      ->method('enable')
+      ->will($this->returnValue($search_page));
+    $search_page->expects($this->once())
+      ->method('save')
+      ->will($this->returnValue($search_page));
+    $this->searchPageRepository->setDefaultSearchPage($search_page);
+  }
+
+  /**
+   * Tests the sortSearchPages() method.
+   */
+  public function testSortSearchPages() {
+    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+    $entity_type->expects($this->any())
+      ->method('getClass')
+      ->will($this->returnValue('Drupal\search\Tests\TestSearchPage'));
+    $this->storage->expects($this->once())
+      ->method('entityInfo')
+      ->will($this->returnValue($entity_type));
+
+    // Declare entities out of their expected order so we can be sure they were
+    // sorted. We cannot mock these because of uasort(), see
+    // https://bugs.php.net/bug.php?id=50688.
+    $unsorted_entities['test4'] = new TestSearchPage(array('weight' => 0, 'status' => FALSE, 'label' => 'Test4'));
+    $unsorted_entities['test3'] = new TestSearchPage(array('weight' => 10, 'status' => TRUE, 'label' => 'Test3'));
+    $unsorted_entities['test2'] = new TestSearchPage(array('weight' => 0, 'status' => TRUE, 'label' => 'Test2'));
+    $unsorted_entities['test1'] = new TestSearchPage(array('weight' => 0, 'status' => TRUE, 'label' => 'Test1'));
+    $expected = $unsorted_entities;
+    ksort($expected);
+
+    $sorted_entities = $this->searchPageRepository->sortSearchPages($unsorted_entities);
+    $this->assertSame($expected, $sorted_entities);
+  }
+
+}
+
+class TestSearchPage extends SearchPage {
+  public function __construct(array $values) {
+    foreach ($values as $key => $value) {
+      $this->$key = $value;
+    }
+  }
+  public function label($langcode = NULL) {
+    return $this->label;
+  }
+}
diff --git a/core/modules/search/tests/Drupal/search/Tests/SearchPluginBagTest.php b/core/modules/search/tests/Drupal/search/Tests/SearchPluginBagTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ee70f0d9dc5c01772e6a7d4cdee09c40e18fe5c
--- /dev/null
+++ b/core/modules/search/tests/Drupal/search/Tests/SearchPluginBagTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Tests\SearchPluginBagTest.
+ */
+
+namespace Drupal\search\Tests;
+
+use Drupal\search\Plugin\SearchPluginBag;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the search plugin bag.
+ *
+ * @see \Drupal\search\Plugin\SearchPluginBag
+ *
+ * @group Drupal
+ * @group Search
+ */
+class SearchPluginBagTest extends UnitTestCase {
+
+  /**
+   * The mocked plugin manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $pluginManager;
+
+  /**
+   * The tested plugin bag.
+   *
+   * @var \Drupal\search\Plugin\SearchPluginBag
+   */
+  protected $searchPluginBag;
+
+  /**
+   * Stores all setup plugin instances.
+   *
+   * @var \Drupal\search\Plugin\SearchInterface[]
+   */
+  protected $pluginInstances;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Search plugin bag test',
+      'description' => 'Tests the \Drupal\search\Plugin\SearchPluginBag class',
+      'group' => 'Search',
+    );
+  }
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->pluginManager = $this->getMock('Drupal\Component\Plugin\PluginManagerInterface');
+    $this->searchPluginBag = new SearchPluginBag($this->pluginManager, array('banana'), array('id' => 'banana', 'color' => 'yellow'), 'fruit_stand');
+  }
+
+  /**
+   * Tests the get() method.
+   */
+  public function testGet() {
+    $plugin = $this->getMock('Drupal\search\Plugin\SearchInterface');
+    $this->pluginManager->expects($this->once())
+      ->method('createInstance')
+      ->will($this->returnValue($plugin));
+    $this->assertSame($plugin, $this->searchPluginBag->get('banana'));
+  }
+
+  /**
+   * Tests the get() method with a configurable plugin.
+   */
+  public function testGetWithConfigurablePlugin() {
+    $plugin = $this->getMock('Drupal\search\Plugin\ConfigurableSearchPluginInterface');
+    $plugin->expects($this->once())
+      ->method('setSearchPageId')
+      ->with('fruit_stand')
+      ->will($this->returnValue($plugin));
+
+    $this->pluginManager->expects($this->once())
+      ->method('createInstance')
+      ->will($this->returnValue($plugin));
+
+    $this->assertSame($plugin, $this->searchPluginBag->get('banana'));
+  }
+
+}
diff --git a/core/modules/search/tests/modules/search_extra_type/config/schema/search_extra_type.schema.yml b/core/modules/search/tests/modules/search_extra_type/config/schema/search_extra_type.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9a98361a4dab339bcb63b9f845326f61251c9474
--- /dev/null
+++ b/core/modules/search/tests/modules/search_extra_type/config/schema/search_extra_type.schema.yml
@@ -0,0 +1,10 @@
+# Schema for the configuration files of the Search Extra Type module.
+
+# Plugin \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch
+search.plugin.search_extra_type_search:
+  type: mapping
+  label: 'Extra type settings'
+  mapping:
+    boost:
+      type: string
+      label: 'Boost method'
diff --git a/core/modules/search/tests/modules/search_extra_type/config/search.page.dummy_search_type.yml b/core/modules/search/tests/modules/search_extra_type/config/search.page.dummy_search_type.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e60478c2b65d9e81fe1d099cc973a2d05e2ab72b
--- /dev/null
+++ b/core/modules/search/tests/modules/search_extra_type/config/search.page.dummy_search_type.yml
@@ -0,0 +1,8 @@
+id: dummy_search_type
+label: 'Dummy search type'
+uuid: b55858d4-f428-474c-8200-ef47a4597aef
+status: true
+langcode: en
+path: dummy_path
+plugin: search_extra_type_search
+configuration: {  }
diff --git a/core/modules/search/tests/modules/search_extra_type/config/search_extra_type.settings.yml b/core/modules/search/tests/modules/search_extra_type/config/search_extra_type.settings.yml
deleted file mode 100644
index db6424665a9ae42ac52b905e3a4b6c12304e1504..0000000000000000000000000000000000000000
--- a/core/modules/search/tests/modules/search_extra_type/config/search_extra_type.settings.yml
+++ /dev/null
@@ -1 +0,0 @@
-boost: bi
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
index e30b10e31d3734359c78db8b61c19b2508f029b6..920f14bc5b539d77f91faba859e3b199b4dda71f 100644
--- 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
@@ -7,55 +7,17 @@
 
 namespace Drupal\search_extra_type\Plugin\Search;
 
-use Drupal\Core\Config\Config;
-use Drupal\Core\Plugin\PluginFormInterface;
-use Drupal\search\Plugin\SearchPluginBase;
-use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\search\Plugin\ConfigurableSearchPluginBase;
 
 /**
  * Executes a keyword search against the search index.
  *
  * @SearchPlugin(
  *   id = "search_extra_type_search",
- *   title = @Translation("Dummy search type"),
- *   path = "dummy_path"
+ *   title = @Translation("Dummy search type")
  * )
  */
-class SearchExtraTypeSearch extends SearchPluginBase implements PluginFormInterface {
-
-  /**
-   * @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);
-  }
+class SearchExtraTypeSearch extends ConfigurableSearchPluginBase {
 
   /**
    * {@inheritdoc}
@@ -138,7 +100,7 @@ public function buildConfigurationForm(array $form, array &$form_state) {
         'bi' => t('Bistromathic'),
         'ii' => t('Infinite Improbability'),
       ),
-      '#default_value' => $this->configSettings->get('boost'),
+      '#default_value' => $this->configuration['boost'],
     );
     return $form;
   }
@@ -146,16 +108,17 @@ public function buildConfigurationForm(array $form, array &$form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validateConfigurationForm(array &$form, array &$form_state) {
+  public function submitConfigurationForm(array &$form, array &$form_state) {
+    $this->configuration['boost'] = $form_state['values']['extra_type_settings']['boost'];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function submitConfigurationForm(array &$form, array &$form_state) {
-    $this->configSettings
-      ->set('boost', $form_state['values']['extra_type_settings']['boost'])
-      ->save();
+  public function defaultConfiguration() {
+    return array(
+      'boost' => 'bi',
+    );
   }
 
 }
diff --git a/core/modules/user/config/search.page.user_search.yml b/core/modules/user/config/search.page.user_search.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4823ec4c4fb94c468e2b2ea3e87f8f34a1a31aab
--- /dev/null
+++ b/core/modules/user/config/search.page.user_search.yml
@@ -0,0 +1,8 @@
+id: user_search
+label: 'Users'
+uuid: c0d6b9a7-09a7-415f-b71a-26957bef635c
+status: true
+langcode: en
+path: user
+plugin: user_search
+configuration: {  }
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php
index ea5f41798f0a81f70571441279fe5db9759fb8fe..518d9c0ba08889a43557098cf902cd348730dd04 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/Search/UserSearch.php
@@ -14,15 +14,13 @@
 use Drupal\Core\Access\AccessibleInterface;
 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"
+ *   title = @Translation("Users")
  * )
  */
 class UserSearch extends SearchPluginBase implements AccessibleInterface {