diff --git a/core/modules/help_topics/config/optional/block.block.seven_help_search.yml b/core/modules/help_topics/config/optional/block.block.seven_help_search.yml
new file mode 100644
index 0000000000..6c3cfdea76
--- /dev/null
+++ b/core/modules/help_topics/config/optional/block.block.seven_help_search.yml
@@ -0,0 +1,26 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - search
+    - system
+  theme:
+    - seven
+id: seven_help_search
+theme: seven
+region: help
+weight: -4
+provider: null
+plugin: search_form_block
+settings:
+  id: search_form_block
+  label: 'Search help'
+  provider: search
+  label_display: visible
+  page_id: help_search
+visibility:
+  request_path:
+    id: request_path
+    pages: /admin/help
+    negate: false
+    context_mapping: {  }
diff --git a/core/modules/help_topics/config/optional/search.page.help_search.yml b/core/modules/help_topics/config/optional/search.page.help_search.yml
new file mode 100644
index 0000000000..38d4344448
--- /dev/null
+++ b/core/modules/help_topics/config/optional/search.page.help_search.yml
@@ -0,0 +1,11 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - help
+id: help_search
+label: Help
+path: help
+weight: 0
+plugin: help_search
+configuration: {  }
diff --git a/core/modules/help_topics/config/schema/help_topics.schema.yml b/core/modules/help_topics/config/schema/help_topics.schema.yml
new file mode 100644
index 0000000000..77f1e80a05
--- /dev/null
+++ b/core/modules/help_topics/config/schema/help_topics.schema.yml
@@ -0,0 +1,3 @@
+search.plugin.help_search:
+  type: sequence
+  label: 'Help search'
diff --git a/core/modules/help_topics/help_topics.install b/core/modules/help_topics/help_topics.install
new file mode 100644
index 0000000000..aa771f1734
--- /dev/null
+++ b/core/modules/help_topics/help_topics.install
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Install and uninstall functions for help_topics module.
+ */
+
+/**
+ * Implements hook_uninstall().
+ */
+function help_topics_uninstall() {
+  // The table help_search_items may have been created -- see
+  // help_topics_modules_installed(). If so, remove it.
+  if (\Drupal::database()->schema()->tableExists('help_search_items')) {
+    \Drupal::database()->schema()->dropTable('help_search_items');
+  }
+}
diff --git a/core/modules/help_topics/help_topics.module b/core/modules/help_topics/help_topics.module
index 5b89b36de1..1eef717f8e 100644
--- a/core/modules/help_topics/help_topics.module
+++ b/core/modules/help_topics/help_topics.module
@@ -16,9 +16,9 @@ function help_topics_help($route_name, RouteMatchInterface $route_match) {
     case 'help.page.help_topics':
       $help_home = Url::fromRoute('help.main')->toString();
       $locale_help = (\Drupal::moduleHandler()->moduleExists('locale')) ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#';
-      $output = '';
+      $search_help = (\Drupal::moduleHandler()->moduleExists('search')) ? Url::fromRoute('help.page', ['name' => 'search'])->toString() : '#';
       $output .= '<h3>' . t('About') . '</h3>';
-      $output .= '<p>' . t('The Help Topics module adds module- and theme-provided help topics to the module overviews from the core Help module. For more information, see the <a href=":online">online documentation for the Help Topics module</a>.', [':online' => 'https://www.drupal.org/modules/help_topics']) . '</p>';
+      $output .= '<p>' . t('The Help Topics module adds module- and theme-provided help topics to the module overviews from the core Help module. If the core Search module is enabled, these topics are also searchable. For more information, see the <a href=":online">online documentation for the Help Topics module</a>.', [':online' => 'https://www.drupal.org/modules/help_topics']) . '</p>';
       $output .= '<h3>' . t('Uses') . '</h3>';
       $output .= '<dl>';
       $output .= '<dt>' . t('Viewing help topics') . '</dt>';
@@ -27,6 +27,8 @@ function help_topics_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<dd>' . t("Modules and themes can provide help topics as Twig-file-based plugins in a project sub-directory called <em>help_topics</em>; plugin meta-data is provided in meta tags within each Twig file. Plugin-based help topics provided by modules and themes will automatically be updated when a module or theme is updated. Use the plugins in <em>core/modules/help_topics/help_topics</em> as a guide when writing and formatting a help topic plugin for your theme or module.") . '</dd>';
       $output .= '<dt>' . t('Translating help topics') . '</dt>';
       $output .= '<dd>' . t('The title and body text of help topics provided by contributed modules and themes are translatable using the <a href=":locale_help">Interface Translation module</a>. Topics provided by custom modules and themes are also translatable if they have been viewed at least once in a non-English language, which triggers putting their translatable text into the translation database.', [':locale_help' => $locale_help]) . '</dd>';
+      $output .= '<dt>' . t('Configuring help search') . '</dt>';
+      $output .= '<dd>' . t('To search help, you will need to install the core Search module, configure a search page (one is set up by default), and add a search block to the Help page or another administrative page. Then users with search permissions, and permission to view help, will be able to search help. See the <a href=":search_help">Search module help page</a> for more information.', [':search_help' => $search_help]) . '</dd>';
       $output .= '</dl>';
       return ['#markup' => $output];
 
@@ -51,3 +53,89 @@ function help_topics_theme() {
     ],
   ];
 }
+
+/**
+ * Implements hook_rebuild().
+ */
+function help_topics_rebuild() {
+  // If the Search module is also enabled, we need to trigger a search reindex
+  // when a module or theme is installed, uninstalled, or updated; and when
+  // languages are added, translations are changed, or string overrides are
+  // changed. These situations can cause an indexed help item to have different
+  // text, or for help items to be added or removed. So, we use a state
+  // variable to keep track of the need to reindex.
+
+  if (\Drupal::moduleHandler()->moduleExists('search')) {
+    Drupal::state()->set('help_search_reindex_needed', TRUE);
+  }
+}
+
+
+/**
+ * Implements hook_modules_installed().
+ */
+function help_topics_modules_installed(array $modules) {
+  // If the search module and this module are both installed, make sure
+  // the help_search_items table is present.
+  if (!\Drupal::moduleHandler()->moduleExists('search') ||
+    \Drupal::database()->schema()->tableExists('help_search_items')) {
+    return;
+  }
+
+  $schema = [
+    // These two elements are normally added by drupal_install_schema().
+    'module' => 'help_topics',
+    'name' => 'help_search_items',
+    'description' => 'Stores information about indexed help search items',
+    'fields' => [
+      'sid' => [
+        'description' => 'Numeric index of this item in the search index',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'plugin_type' => [
+        'description' => 'The help plugin type the item comes from',
+        'type' => 'varchar_ascii',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ],
+      'permission' => [
+        'description' => 'The permission needed to view this item',
+        'type' => 'varchar_ascii',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ],
+      'id' => [
+        'description' => 'The plugin ID of the item',
+        'type' => 'varchar_ascii',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ],
+    ],
+    'primary key' => ['sid'],
+    'indexes' => [
+      'plugin_type' => ['plugin_type'],
+      'id' => ['id'],
+    ],
+  ];
+
+  \Drupal::database()->schema()->createTable($schema['name'], $schema);
+}
+
+/**
+ * Implements hook_modules_uninstalled().
+ */
+function help_topics_modules_uninstalled(array $modules) {
+  // If the search module is no longer present, remove the help_search_items
+  // table.
+  if (!\Drupal::moduleHandler()->moduleExists('search') &&
+    \Drupal::database()->schema()->tableExists('help_search_items')) {
+    \Drupal::database()->schema()->dropTable('help_search_items');
+  }
+
+}
diff --git a/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php b/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php
index c68f5ff11f..8e82535384 100644
--- a/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php
+++ b/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php
@@ -3,10 +3,15 @@
 namespace Drupal\help_topics\Plugin\HelpSection;
 
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\help_topics\SearchableHelpInterface;
 use Drupal\help_topics\HelpTopicPluginInterface;
 use Drupal\help_topics\HelpTopicPluginManagerInterface;
+use Drupal\Core\Language\LanguageDefault;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
+use Drupal\Core\Render\RendererInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -25,7 +30,7 @@
  *   experimental modules and development releases of contributed modules.
  *   See https://www.drupal.org/core/experimental for more information.
  */
-class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface {
+class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface, SearchableHelpInterface {
 
   /**
    * The plugin manager.
@@ -46,6 +51,27 @@ class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactory
    */
   protected $cacheableMetadata;
 
+  /**
+   * The Renderer service to format the username and node.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The default language object.
+   *
+   * @var \Drupal\Core\Language\LanguageDefault
+   */
+  protected $defaultLanguage;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
   /**
    * Constructs a HelpTopicSection object.
    *
@@ -57,10 +83,19 @@ class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactory
    *   The plugin implementation definition.
    * @param \Drupal\help_topics\HelpTopicPluginManagerInterface $plugin_manager
    *   The help topic plugin manager service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Language\LanguageDefault $default_language
+   *   The default language object.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, HelpTopicPluginManagerInterface $plugin_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, HelpTopicPluginManagerInterface $plugin_manager, RendererInterface $renderer, LanguageDefault $default_language, LanguageManagerInterface $language_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->pluginManager = $plugin_manager;
+    $this->renderer = $renderer;
+    $this->defaultLanguage = $default_language;
+    $this->languageManager = $language_manager;
   }
 
   /**
@@ -71,7 +106,10 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('plugin.manager.help_topic')
+      $container->get('plugin.manager.help_topic'),
+      $container->get('renderer'),
+      $container->get('language.default'),
+      $container->get('language_manager')
     );
   }
 
@@ -110,7 +148,7 @@ public function listTopics() {
    * Gets the top level help topic plugins.
    *
    * @return \Drupal\help_topics\HelpTopicPluginInterface[]
-   *   The top level help topic plugins
+   *   The top level help topic plugins.
    */
   protected function getPlugins() {
     if (!isset($this->topLevelPlugins)) {
@@ -137,6 +175,60 @@ protected function getPlugins() {
     return $this->topLevelPlugins;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function listSearchableTopics() {
+    $definitions = $this->pluginManager->getDefinitions();
+    $ids = [];
+    foreach ($definitions as $definition) {
+      $ids[] = $definition['id'];
+    }
+    return $ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderTopicForSearch($id, LanguageInterface $language) {
+    $plugin = $this->pluginManager->createInstance($id);
+    if (!$plugin) {
+      return [];
+    }
+
+    // We are rendering this topic for search indexing or search results,
+    // possibly in a different language than the current language. The topic
+    // title and body come from translatable things in the Twig template, so we
+    // need to set the default language to the desired language, render them,
+    // then reset the default language so we do not screw up other cron things.
+    $old_language = $this->defaultLanguage->get();
+    $this->defaultLanguage->set($language);
+    $topic = [];
+
+    // Render the title in this language.
+    $title_build = [
+      'title' => [
+        '#type' => '#markup',
+        '#markup' => $plugin->getLabel(),
+      ],
+    ];
+    $topic['title'] = $this->renderer->renderPlain($title_build);
+
+    // Render the body in this language.
+    $build = [
+      'body' => $plugin->getBody(),
+    ];
+    $topic['text'] = $this->renderer->renderPlain($build);
+
+    // Add the other information.
+    $topic['url'] = $plugin->toUrl()->toString();
+    $topic['cache_dependency'] = $plugin;
+
+    // Reset the language.
+    $this->defaultLanguage->set($old_language);
+    return $topic;
+  }
+
   /**
    * Gets the merged CacheableMetadata for all the top level help topic plugins.
    *
diff --git a/core/modules/help_topics/src/Plugin/Search/HelpSearch.php b/core/modules/help_topics/src/Plugin/Search/HelpSearch.php
new file mode 100644
index 0000000000..c299fcf773
--- /dev/null
+++ b/core/modules/help_topics/src/Plugin/Search/HelpSearch.php
@@ -0,0 +1,501 @@
+<?php
+
+namespace Drupal\help_topics\Plugin\Search;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Config\Config;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\StatementInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\State\StateInterface;
+use Drupal\help\HelpSectionManager;
+use Drupal\help\HelpSectionPluginInterface;
+use Drupal\help_topics\SearchableHelpInterface;
+use Drupal\search\Plugin\SearchPluginBase;
+use Drupal\search\Plugin\SearchIndexingInterface;
+use Drupal\Search\SearchQuery;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Handles searching for help using the Search module index.
+ *
+ * Help items are indexed if their HelpSection plugin implements
+ * \Drupal\help\HelpSearchInterface.
+ *
+ * @see \Drupal\help\HelpSearchInterface
+ * @see \Drupal\help\HelpSectionPluginInterface
+ *
+ * @SearchPlugin(
+ *   id = "help_search",
+ *   title = @Translation("Help")
+ * )
+ */
+class HelpSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface {
+
+  /**
+   * The current database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * A config object for 'search.settings'.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $searchSettings;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * The Drupal account to use for checking for access to search.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * The state object.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The help section plugin manager.
+   *
+   * @var \Drupal\help\HelpSectionManager
+   */
+  protected $helpManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('database'),
+      $container->get('config.factory')->get('search.settings'),
+      $container->get('language_manager'),
+      $container->get('messenger'),
+      $container->get('current_user'),
+      $container->get('state'),
+      $container->get('plugin.manager.help_section')
+    );
+  }
+
+  /**
+   * Constructs a \Drupal\help_search\Plugin\Search\HelpSearch object.
+   *
+   * @param array $configuration
+   *   Configuration for the plugin.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The current database connection.
+   * @param \Drupal\Core\Config\Config $search_settings
+   *   A config object for 'search.settings'.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The $account object to use for checking for access to view help.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state object.
+   * @param \Drupal\help\HelpSectionManager $help_manager
+   *   The help section manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, Config $search_settings, LanguageManagerInterface $language_manager, MessengerInterface $messenger, AccountInterface $account = NULL, StateInterface $state, HelpSectionManager $help_manager) {
+    $this->database = $database;
+    $this->searchSettings = $search_settings;
+    $this->languageManager = $language_manager;
+    $this->messenger = $messenger;
+    $this->account = $account;
+    $this->state = $state;
+    $this->helpManager = $help_manager;
+    $this->addCacheableDependency($this->helpManager);
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
+    $result = AccessResult::allowedIfHasPermission($account, 'access administration pages');
+    return $return_as_object ? $result : $result->isAllowed();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getType() {
+    return $this->getPluginId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    if ($this->isSearchExecutable()) {
+      $results = $this->findResults();
+
+      if ($results) {
+        return $this->prepareResults($results);
+      }
+    }
+
+    return [];
+  }
+
+  /**
+   * Finds the search results.
+   *
+   * @return \Drupal\Core\Database\StatementInterface|null
+   *   Results from search query execute() method, or NULL if the search
+   *   failed.
+   */
+  protected function findResults() {
+    // We need to check access for the current user to see the topics that
+    // could get returned by search. Each entry in the help_search_items
+    // database has an optional permission that comes from the HelpSection
+    // plugin, in addition to the generic 'access administration pages'
+    // permission. In order to enforce these permissions so only topics that
+    // the current user has permission to view are selected by the query, make
+    // a list of the permission strings and pre-check those permissions.
+    $this->addCacheableDependency($this->account);
+    if (!$this->account->hasPermission('access administration pages')) {
+      return NULL;
+    }
+    $permissions = $this->database
+      ->select('help_search_items', 'hsi')
+      ->fields('hsi', ['permission'])
+      ->groupBy('hsi.permission')
+      ->execute()
+      ->fetchCol();
+    $denied_permissions = [];
+    if ($permissions) {
+      foreach ($permissions as $permission) {
+        if ($permission && !$this->account->hasPermission($permission)) {
+          $denied_permissions[] = $permission;
+        }
+      }
+    }
+
+    $keys = $this->getKeywords();
+    $query = $this->database
+      ->select('search_index', 'i')
+      ->extend('Drupal\search\SearchQuery')
+      ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
+    $query->join('help_search_items', 'hsi', 'i.sid = hsi.sid AND i.type = :type', [':type' => $this->getPluginId()]);
+    if (count($denied_permissions)) {
+      $query->condition('hsi.permission', $denied_permissions, 'NOT IN');
+    }
+    $query->searchExpression($keys, $this->getPluginId());
+
+    $find = $query
+      ->fields('i', ['langcode'])
+      // Since SearchQuery makes these into GROUP BY queries, if we add
+      // a field, for PostgreSQL we also need to make it an aggregate or a
+      // GROUP BY. In this case, we want GROUP BY.
+      ->groupBy('i.langcode')
+      ->limit(10)
+      ->execute();
+
+    // Check query status and set messages if needed.
+    $status = $query->getStatus();
+
+    if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
+      $this->messenger->addWarning($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', ['@count' => $this->searchSettings->get('and_or_limit')]));
+    }
+
+    if ($status & SearchQuery::LOWER_CASE_OR) {
+      $this->messenger->addWarning($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'));
+    }
+
+    if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
+      $this->messenger->addWarning($this->formatPlural($this->searchSettings->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
+    }
+
+    return $find;
+  }
+
+  /**
+   * Prepares search results for display.
+   *
+   * @param \Drupal\Core\Database\StatementInterface $found
+   *   Results found from a successful search query execute() method.
+   *
+   * @return array
+   *   List of search result render arrays, with links, snippets, etc.
+   */
+  protected function prepareResults(StatementInterface $found) {
+    $results = [];
+    $plugins = [];
+    $keys = $this->getKeywords();
+    foreach ($found as $item) {
+      $result = $this->database->select('help_search_items', 'hsi')
+        ->condition('sid', $item->sid)
+        ->fields('hsi', ['plugin_type', 'id'])
+        ->execute();
+      foreach ($result as $record) {
+        $type = $record->plugin_type;
+        if (!isset($plugins[$type])) {
+          $plugins[$type] = $this->getSectionPlugin($type);
+        }
+        if ($plugins[$type]) {
+          $language = $this->languageManager->getLanguage($item->langcode);
+          $topic = $plugins[$type]->renderTopicForSearch($record->id, $language);
+          if ($topic) {
+            if (isset($topic['cache_dependency'])) {
+              $this->addCacheableDependency($topic['cache_dependency']);
+            }
+            $results[] = [
+              'title' => $topic['title'],
+              'snippet' => search_excerpt($keys, $topic['title'] . ' ' . $topic['text'], $item->langcode),
+              'langcode' => $item->langcode,
+            ];
+          }
+        }
+      }
+    }
+
+    return $results;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateIndex() {
+    // See if we need to update the list of items to be indexed.
+    if ($this->state->get('help_search_reindex_needed', TRUE)) {
+      $this->markForReindex();
+    }
+
+    // Find some items that need to be updated. Start with ones that have
+    // never been indexed, and if there is still space in the limit, add in
+    // ones that are marked to reindex.
+    $limit = (int) $this->searchSettings->get('index.cron_limit');
+
+    $query = $this->database->select('help_search_items', 'hsi');
+    $query->fields('hsi', ['sid', 'plugin_type', 'id']);
+    $query->leftJoin('search_dataset', 'sd', 'sd.sid = hsi.sid AND sd.type = :type', [':type' => $this->getPluginId()]);
+    $query->where('sd.sid IS NULL');
+    $query->groupBy('hsi.sid')
+      ->groupBy('hsi.plugin_type')
+      ->groupBy('hsi.id')
+      ->range(0, $limit);
+    $items = $query->execute()->fetchAll();
+
+    if (count($items) < $limit) {
+      $query = $this->database->select('help_search_items', 'hsi');
+      $query->fields('hsi', ['sid', 'plugin_type', 'id']);
+      $query->leftJoin('search_dataset', 'sd', 'sd.sid = hsi.sid AND sd.type = :type', [':type' => $this->getPluginId()]);
+      $query->condition('sd.reindex', 0, '<>');
+      $query->groupBy('hsi.sid')
+        ->groupBy('hsi.plugin_type')
+        ->groupBy('hsi.id')
+        ->range(0, $limit - count($items));
+      $items = $items + $query->execute()->fetchAll();
+    }
+
+    // Index items, in all available languages.
+    $language_list = $this->languageManager->getLanguages(LanguageInterface::STATE_CONFIGURABLE);
+    $plugins = [];
+
+    foreach ($items as $item) {
+      $type = $item->plugin_type;
+      if (!isset($plugins[$type])) {
+        $plugins[$type] = $this->getSectionPlugin($type);
+      }
+
+      if (!$plugins[$type]) {
+        $this->removeItemsFromIndex($item->sid);
+        continue;
+      }
+
+      $plugin = $plugins[$type];
+      search_index_clear($this->getPluginId(), $item->sid);
+      foreach ($language_list as $langcode => $language) {
+        $topic = $plugin->renderTopicForSearch($item->id, $language);
+        if ($topic) {
+          // Index the title plus body text.
+          $text = '<h1>' . $topic['title'] . '</h1>' . "\n" . $topic['text'];
+          search_index($this->getPluginId(), $item->sid, $langcode, $text);
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexClear() {
+    search_index_clear($this->getPluginId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markForReindex() {
+    // Update the list of help items. Start by fetching the existing list,
+    // so we can remove items not found at the end.
+    $old_list = $this->database->select('help_search_items', 'hsi')
+      ->fields('hsi', ['sid', 'id', 'plugin_type', 'permission'])
+      ->execute()
+      ->fetchAll();
+    $old_list_ordered = [];
+    $max_sid = 0;
+    $sids_to_remove = [];
+    foreach ($old_list as $item) {
+      $old_list_ordered[$item->plugin_type][$item->id] = $item;
+      if ($item->sid > $max_sid) {
+        $max_sid = $item->sid;
+      }
+      $sids_to_remove[$item->sid] = $item->sid;
+    }
+
+    $plugins = $this->helpManager->getDefinitions();
+    foreach ($plugins as $plugin_id => $plugin_definition) {
+      /** @var \Drupal\help\HelpSectionPluginInterface $plugin */
+      $plugin = $this->getSectionPlugin($plugin_id);
+      if (!$plugin) {
+        continue;
+      }
+      $permission = (isset($plugin_definition['permission']) ? $plugin_definition['permission'] : '');
+      $items = $plugin->listSearchableTopics();
+      if (!count($items)) {
+        continue;
+      }
+      foreach ($items as $id) {
+        if (isset($old_list_ordered[$plugin_id][$id])) {
+          $old_item = $old_list_ordered[$plugin_id][$id];
+          if ($old_item->permission == $permission) {
+            // Record has not changed.
+            unset($sids_to_remove[$old_item->sid]);
+          }
+          else {
+            // Permission has changed, update record.
+            $this->database->update('help_search_items')
+              ->condition('sid', $old_item->sid)
+              ->fields(['permission' => $permission])
+              ->execute();
+          }
+        }
+        else {
+          // New record, create it.
+          $this->database->insert('help_search_items')
+            ->fields([
+              'sid' => $max_sid + 1,
+              'plugin_type' => $plugin_id,
+              'permission' => $permission,
+              'id' => $id,
+            ])
+            ->execute();
+          $max_sid++;
+        }
+      }
+    }
+
+    // Remove remaining items from the index.
+    $this->removeItemsFromIndex($sids_to_remove);
+
+    // Mark all items currently in the search index database as needing
+    // reindex.
+    search_mark_for_reindex($this->getPluginId());
+    $this->state->set('help_search_reindex_needed', FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexStatus() {
+    if ($this->state->get('help_search_reindex_needed', TRUE)) {
+      $this->markForReindex();
+    }
+
+    $total = $this->database->select('help_search_items', 'hsi')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+
+    $query = $this->database->select('help_search_items', 'hsi');
+    $query->addExpression('COUNT(DISTINCT(hsi.sid))');
+    $query->leftJoin('search_dataset', 'sd', 'hsi.sid = sd.sid AND sd.type = :type', [':type' => $this->getPluginId()]);
+    $condition = new Condition('OR');
+    $condition->condition('sd.reindex', 0, '<>')
+      ->isNull('sd.sid');
+    $query->condition($condition);
+    $remaining = $query->execute()->fetchField();
+
+    return [
+      'remaining' => $remaining,
+      'total' => $total,
+    ];
+  }
+
+  /**
+   * Removes an item or items from the search index.
+   *
+   * @param int|int[] $sids
+   *   Search ID (sid) of item or items to remove.
+   */
+  protected function removeItemsFromIndex($sids) {
+    if (!is_array($sids)) {
+      $sids = [$sids];
+    }
+
+    // Remove items in batches of 100.
+    for ($start = 0; $start < count($sids); $start += 100) {
+      $this_list = array_slice($sids, $start, 100);
+      $this->database->delete('help_search_items')
+        ->condition('sid', $this_list, 'IN')
+        ->execute();
+      search_index_clear($this->getPluginId(), $this_list);
+    }
+  }
+
+  /**
+   * Instantiates a help section plugin and verifies it is searchable.
+   *
+   * @param string $plugin_type
+   *   Type of plugin to instantiate.
+   *
+   * @return \Drupal\help\HelpSectionPluginInterface|false
+   *   Plugin object, or FALSE if it is not a searchable type.
+   */
+  protected function getSectionPlugin($plugin_type) {
+    /** @var \Drupal\help\HelpSectionPluginInterface $plugin */
+    $plugin = $this->helpManager->createInstance($plugin_type);
+    if ($plugin &&
+      $plugin instanceof HelpSectionPluginInterface &&
+      $plugin instanceof SearchableHelpInterface) {
+      return $plugin;
+    }
+
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/help_topics/src/SearchableHelpInterface.php b/core/modules/help_topics/src/SearchableHelpInterface.php
new file mode 100644
index 0000000000..899a185061
--- /dev/null
+++ b/core/modules/help_topics/src/SearchableHelpInterface.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\help_topics;
+
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Provides an interface for a HelpSection plugin that also supports search.
+ *
+ * @see \Drupal\help\HelpSectionPluginInterface
+ */
+interface SearchableHelpInterface {
+
+  /**
+   * Returns the IDs of topics that should be indexed for searching.
+   *
+   * @return string[]
+   *   An array of topic IDs that should be searchable. IDs need to be
+   *   unique within this HelpSection plugin type.
+   */
+  public function listSearchableTopics();
+
+  /**
+   * Renders one topic for search indexing or search results.
+   *
+   * @param string $id
+   *   The ID of the topic to be indexed.
+   * @param \Drupal\Core\LanguageInterface $language
+   *   The language to render the topic in.
+   *
+   * @return array
+   *   An array of information about the topic, with elements:
+   *   - title: The title of the topic in this language.
+   *   - text: The text of the topic in this language.
+   *   - url: The URL of the topic.
+   *   - cache_dependency: (optional) An object to add as a cache dependency
+   *     if this topic is shown in search results.
+   */
+  public function renderTopicForSearch($id, LanguageInterface $language);
+
+}
