diff --git a/core/modules/search/lib/Drupal/search/Controller/SearchController.php b/core/modules/search/lib/Drupal/search/Controller/SearchController.php
index 3b8777b4443bfc14a80ba372ad4aed3763188035..6aa58ba658e932497b4183a58635b9bd4b8c71db 100644
--- a/core/modules/search/lib/Drupal/search/Controller/SearchController.php
+++ b/core/modules/search/lib/Drupal/search/Controller/SearchController.php
@@ -68,7 +68,8 @@ public function view(Request $request, SearchPageInterface $entity, $keys = '')
 
     $plugin = $entity->getPlugin();
     $plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
-    $results = array();
+    // Default results output is an empty string.
+    $results = array('#markup' => '');
 
     // Process the search form. Note that if there is
     // \Drupal::request()->request data, search_form_submit() will cause a
@@ -87,32 +88,7 @@ public function view(Request $request, SearchPageInterface $entity, $keys = '')
     }
     // The form may be altered based on whether the search was run.
     $build['search_form'] = $this->entityManager()->getForm($entity, 'search');
-    if (count($results)) {
-      $build['search_results_title'] = array(
-        '#markup' => '<h2>' . $this->t('Search results') . '</h2>',
-      );
-    }
-
-    $build['search_results'] = array(
-      '#theme' => array('item_list__search_results__' . $plugin->getPluginId(), 'item_list__search_results'),
-      '#items' => $results,
-      '#empty' => array(
-        // @todo Revisit where this help text is added.
-        '#markup' => '<h3>' . $this->t('Your search yielded no results.') . '</h3>' . search_help('search#noresults', drupal_help_arg()),
-      ),
-      '#list_type' => 'ol',
-      '#attributes' => array(
-        'class' => array(
-          'search-results',
-          $plugin->getPluginId() . '-results',
-        ),
-      ),
-    );
-
-    $build['pager'] = array(
-      '#theme' => 'pager',
-    );
-
+    $build['search_results'] = $results;
     return $build;
   }
 
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php
index 2ff0ad70c577b47f9f9cfe1d7b6ad46dc9411795..c4738387de7db79328a5a55e7a98abbb88933875 100644
--- a/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php
@@ -72,12 +72,10 @@ public function isSearchExecutable();
   public function execute();
 
   /**
-   * Executes the search and builds render arrays for the result items.
+   * Executes the search and builds a render array.
    *
    * @return array
-   *   An array of render arrays of search result items (generally each item
-   *   has '#theme' set to 'search_result'), or an empty array if there are no
-   *   results.
+   *   The search results in a renderable array.
    */
   public function buildResults();
 
diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
index b1fcf97acf2104b4f3034d14e11be0949c918a19..ec808ad75491c8d2d071eda607a57d101d31b40e 100644
--- a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
+++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php
@@ -88,17 +88,11 @@ public function isSearchExecutable() {
    */
   public function buildResults() {
     $results = $this->execute();
-
-    $built = array();
-    foreach ($results as $result) {
-      $built[] = array(
-        '#theme' => 'search_result',
-        '#result' => $result,
-        '#plugin_id' => $this->getPluginId(),
-      );
-    }
-
-    return $built;
+    return array(
+      '#theme' => 'search_results',
+      '#results' => $results,
+      '#plugin_id' => $this->getPluginId(),
+    );
   }
 
  /**
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index 963c22fa075f5d22ebe08988699842ca1b55d427..11a5e00e17005f74c1805803ca7e6d68fa8441f9 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -108,6 +108,11 @@ function search_theme() {
       'file' => 'search.pages.inc',
       'template' => 'search-result',
     ),
+    'search_results' => array(
+      'variables' => array('results' => NULL, 'plugin_id' => NULL),
+      'file' => 'search.pages.inc',
+      'template' => 'search-results',
+    ),
   );
 }
 
diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc
index ec718554edada903a1ffe4fa70578b922b636ac5..023a825f6c65881c128447d7ef7c8d77ade92104 100644
--- a/core/modules/search/search.pages.inc
+++ b/core/modules/search/search.pages.inc
@@ -6,6 +6,35 @@
  */
 
 use Drupal\Core\Language\Language;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Prepares variables for search results templates.
+ *
+ * Default template: search-results.html.twig.
+ *
+ * @param array $variables
+ *   An array with the following elements:
+ *   - results: Search results array.
+ *   - plugin_id: Plugin the search results came from.
+ */
+function template_preprocess_search_results(&$variables) {
+  $variables['search_results'] = '';
+  if (!empty($variables['plugin_id'])) {
+    $variables['plugin_id'] = check_plain($variables['plugin_id']);
+  }
+  foreach ($variables['results'] as $result) {
+    $variables['search_results'][] = array(
+      '#theme' => 'search_result',
+      '#result' => $result,
+      '#plugin_id' => $variables['plugin_id'],
+    );
+  }
+  $variables['pager'] = array('#theme' => 'pager');
+  // @todo Revisit where this help text is added, see also
+  //   http://drupal.org/node/1918856.
+  $variables['help'] = search_help('search#noresults', drupal_help_arg());
+}
 
 /**
  * Implements hook_theme_suggestions_HOOK().
diff --git a/core/modules/search/templates/search-result.html.twig b/core/modules/search/templates/search-result.html.twig
index 732489bee1782b9d536b5d342d69abfdd8d07824..281f8b121494d5e2b19591f1d06cc34151f0611d 100644
--- a/core/modules/search/templates/search-result.html.twig
+++ b/core/modules/search/templates/search-result.html.twig
@@ -3,10 +3,9 @@
  * @file
  * Default theme implementation for displaying a single search result.
  *
- * This template renders a single search result. The list of results is
- * rendered using '#theme' => 'item_list', with suggestions of:
- * - item_list__search_results__(plugin_id)
- * - item_list__search_results
+ * This template renders a single search result and is collected into
+ * search-results.html.twig. This and the parent template are
+ * dependent to one another sharing the markup for ordered lists.
  *
  * Available variables:
  * - url: URL of the result.
@@ -58,16 +57,18 @@
  * @ingroup themeable
  */
 #}
-{{ title_prefix }}
-<h3 class="title"{{ title_attributes }}>
-  <a href="{{ url }}">{{ title }}</a>
-</h3>
-{{ title_suffix }}
-<div class="search-snippet-info">
-  {% if snippet %}
-    <p class="search-snippet"{{ content_attributes }}>{{ snippet }}</p>
-  {% endif %}
-  {% if info %}
-    <p class="search-info">{{ info }}</p>
-  {% endif %}
-</div>
+<li {{ attributes }}>
+  {{ title_prefix }}
+  <h3 class="title"{{ title_attributes }}>
+    <a href="{{ url }}">{{ title }}</a>
+  </h3>
+  {{ title_suffix }}
+  <div class="search-snippet-info">
+    {% if snippet %}
+      <p class="search-snippet"{{ content_attributes }}>{{ snippet }}</p>
+    {% endif %}
+    {% if info %}
+      <p class="search-info">{{ info }}</p>
+    {% endif %}
+  </div>
+</li>
diff --git a/core/modules/search/templates/search-results.html.twig b/core/modules/search/templates/search-results.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..4c14be7832d26481a4587fe5753b29b374c0d699
--- /dev/null
+++ b/core/modules/search/templates/search-results.html.twig
@@ -0,0 +1,35 @@
+{#
+/**
+ * @file
+ * Default theme implementation for displaying search results.
+ *
+ * This template collects each invocation of theme_search_result(). This and
+ * the child template are dependent to one another sharing the markup for
+ * definition lists.
+ *
+ * Note that modules may implement their own search type and theme function
+ * completely bypassing this template.
+ *
+ * Available variables:
+ * - search_results: All results as it is rendered through
+ *   search-result.html.twig.
+ * - plugin_id: The machine-readable name of the plugin being executed, such
+ *   as 'node_search' or 'user_search'.
+ * - pager: The pager next/prev links to display, if any.
+ * - help: HTML for help text to display when no results are found.
+ *
+ * @see template_preprocess_search_results()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if search_results %}
+  <h2>{{ 'Search results'|t }}</h2>
+  <ol class="search-results {{ plugin_id }}-results">
+    {{ search_results }}
+  </ol>
+  {{ pager }}
+{% else %}
+  <h2>{{ 'Your search yielded no results'|t }}</h2>
+  {{ help }}
+{% endif %}