diff --git a/src/Plugin/Search/GroupSearch.php b/src/Plugin/Search/GroupSearch.php
new file mode 100644
index 0000000..fb7f3b1
--- /dev/null
+++ b/src/Plugin/Search/GroupSearch.php
@@ -0,0 +1,397 @@
+<?php
+
+namespace Drupal\group\Plugin\Search;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Config\Config;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\StatementInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\group\Entity\Group;
+use Drupal\group\Entity\GroupInterface;
+use Drupal\search\Plugin\SearchIndexingInterface;
+use Drupal\search\Plugin\SearchPluginBase;
+use Drupal\search\SearchIndexInterface;
+use Drupal\search\SearchQuery;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Executes a keyword search for groups against the {group} database table.
+ *
+ * @SearchPlugin(
+ *   id = "group_search",
+ *   title = @Translation("Groups")
+ * )
+ */
+class GroupSearch extends SearchPluginBase implements SearchIndexingInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The replica database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $databaseReplica;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * A config object for 'search.settings'.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $searchSettings;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The Renderer service to format the group.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * {@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('entity_type.manager'),
+      $container->get('module_handler'),
+      $container->get('current_user'),
+      $container->get('config.factory')->get('search.settings'),
+      $container->get('renderer'),
+      $container->get('database.replica')
+    );
+  }
+
+  /**
+   * Creates a GroupSearch object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\Core\Config\Config $search_settings
+   *   A config object for 'search.settings'.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Database\Connection|null $database_replica
+   *   (Optional) the replica database connection.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, Config $search_settings, RendererInterface $renderer, Connection $database_replica = NULL) {
+    $this->database = $database;
+    $this->databaseReplica = $database_replica ?: $database;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->moduleHandler = $module_handler;
+    $this->currentUser = $current_user;
+    $this->searchSettings = $search_settings;
+    $this->renderer = $renderer;
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->addCacheTags(['group_list']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    if ($this->isSearchExecutable()) {
+      $results = $this->findResults();
+
+      if ($results) {
+        return $this->prepareResults($results);
+      }
+    }
+
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function findResults() {
+    $results = [];
+    if (!$this->isSearchExecutable()) {
+      return $results;
+    }
+
+    // Process the keywords.
+    $keys = $this->keywords;
+    // Escape for LIKE matching.
+    $keys = $this->database->escapeLike($keys);
+    // Replace wildcards with MySQL/PostgreSQL wildcards.
+    $keys = preg_replace('!\*+!', '%', $keys);
+
+    // Build matching conditions.
+    $query = $this->databaseReplica
+      ->select('search_index', 'i')
+      ->extend('Drupal\search\SearchQuery')
+      ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
+    $query->join('groups_field_data', 'g', 'g.id = i.sid AND g.langcode = i.langcode');
+    $query->searchExpression($keys, $this->getPluginId());
+
+    // Run the query.
+    $gids = $query
+      // Add the language code of the indexed item to the result of the query,
+      // since the group will be rendered using the respective language.
+      ->fields('i', ['langcode'])
+      // And 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 $gids;
+  }
+
+  /**
+   * Prepares search results for rendering.
+   *
+   * @param \Drupal\Core\Database\StatementInterface $found
+   *   Results found from a successful search query execute() method.
+   *
+   * @return array
+   *   Array of search result item render arrays (empty array if no results).
+   */
+  protected function prepareResults(StatementInterface $found) {
+    $results = [];
+
+    $group_storage = $this->entityTypeManager->getStorage('group');
+    $group_render = $this->entityTypeManager->getViewBuilder('group');
+    $keys = $this->keywords;
+
+    foreach ($found as $item) {
+      // Render the group.
+      /** @var \Drupal\group\GroupInterface $group */
+      $group = $group_storage->load($item->sid)->getTranslation($item->langcode);
+      $build = $group_render->view($group, 'search_result', $item->langcode);
+
+      /** @var \Drupal\group\GroupTypeInterface $type*/
+      $type = $this->entityTypeManager->getStorage('group_type')->load($group->bundle());
+
+      unset($build['#theme']);
+      $build['#pre_render'][] = [$this, 'removeSubmittedInfo'];
+
+      // Fetch comments for snippet.
+      $rendered = $this->renderer->renderPlain($build);
+      $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($build));
+      $rendered .= ' ' . $this->moduleHandler->invoke('comment', 'group_update_index', [$group]);
+
+      $extra = $this->moduleHandler->invokeAll('group_search_result', [$group]);
+
+      $result = [
+        'link' => $group->toUrl('canonical', ['absolute' => TRUE])->toString(),
+        'type' => $type->label(),
+        'title' => $group->label(),
+        'group' => $group,
+        'extra' => $extra,
+        'score' => $item->calculated_score,
+        'snippet' => search_excerpt($keys, $rendered, $item->langcode),
+        'langcode' => $group->language()->getId(),
+      ];
+
+      $this->addCacheableDependency($group);
+
+      // We have to separately add the group owner's cache tags because search
+      // module doesn't use the rendering system, it does its own rendering
+      // without taking cacheability metadata into account. So we have to do it
+      // explicitly here.
+      $this->addCacheableDependency($group->getOwner());
+
+      $results[] = $result;
+
+    }
+    return $results;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getHelp() {
+    $help = [
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Group search looks for group names and partial group names. Example: mar would match group names mar, delmar, and maryjane.'),
+          $this->t('You can use * as a wildcard within your keyword. Example: m*r would match group names mar, delmar, and elementary.'),
+        ],
+      ],
+    ];
+
+    return $help;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexStatus() {
+    $total = $this->database->query('SELECT COUNT(*) FROM {groups}')->fetchField();
+    $remaining = $this->database->query("SELECT COUNT(DISTINCT g.id) FROM {groups} g LEFT JOIN {search_dataset} sd ON sd.sid = g.id AND sd.type = :type WHERE sd.sid IS NULL OR sd.reindex <> 0", [':type' => $this->getPluginId()])->fetchField();
+
+    return ['remaining' => $remaining, 'total' => $total];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexClear() {
+    \Drupal::service('search.index')->clear($this->getPluginId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markForReindex() {
+    \Drupal::service('search.index')->markForReindex($this->getPluginId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateIndex() {
+    // Interpret the cron limit setting as the maximum number of groups to index
+    // per cron run.
+    $limit = (int) $this->searchSettings->get('index.cron_limit');
+
+    $query = $this->databaseReplica->select('groups', 'g');
+    $query->addField('g', 'id');
+    $query->leftJoin('search_dataset', 'sd', 'sd.sid = g.id AND sd.type = :type', [':type' => $this->getPluginId()]);
+    $query->addExpression('CASE MAX(sd.reindex) WHEN NULL THEN 0 ELSE 1 END', 'ex');
+    $query->addExpression('MAX(sd.reindex)', 'ex2');
+    $query->condition(
+      $query->orConditionGroup()
+        ->where('sd.sid IS NULL')
+        ->condition('sd.reindex', 0, '<>')
+    );
+    $query->orderBy('ex', 'DESC')
+      ->orderBy('ex2')
+      ->orderBy('g.id')
+      ->groupBy('g.id')
+      ->range(0, $limit);
+
+    $gids = $query->execute()->fetchCol();
+    if (!$gids) {
+      return;
+    }
+
+    $group_storage = $this->entityTypeManager->getStorage('group');
+    foreach ($group_storage->loadMultiple($gids) as $group) {
+      $this->indexGroup($group);
+    }
+  }
+
+  /**
+   * Indexes a single group.
+   *
+   * @param \Drupal\group\Entity\GroupInterfacee $group
+   *   The group to index.
+   */
+  protected function indexGroup(GroupInterface $group) {
+    $languages = $group->getTranslationLanguages();
+    $group_render = $this->entityTypeManager->getViewBuilder('group');
+
+    foreach ($languages as $language) {
+      $group = $group->getTranslation($language->getId());
+
+      // Render the group.
+      $build = $group_render->view($group, 'search_index', $language->getId());
+      unset($build['#theme']);
+
+      // Add the title to text so it is searchable.
+      $build['search_title'] = [
+        '#prefix' => '<h1>',
+        '#plain_text' => $group->label(),
+        '#suffix' => '</h1>',
+        '#weight' => -1000,
+      ];
+
+      $text = $this->renderer->renderPlain($build);
+
+      // Fetch extra data normally not visible.
+      $extra = $this->moduleHandler->invokeAll('group_update_index', [$group]);
+      foreach ($extra as $t) {
+        $text .= $t;
+      }
+
+      // Update index, using search index "type" equal to the plugin ID.
+      \Drupal::service('search.index')->index($this->getPluginId(), $group->id(), $language->getId(), $text);
+    }
+  }
+
+  /**
+   * Removes the submitted by information from the build array.
+   *
+   * This information is being removed from the rendered group that is used to
+   * build the search result snippet. It just doesn't make sense to have it
+   * displayed in the snippet.
+   *
+   * @param array $build
+   *   The build array.
+   *
+   * @return array
+   *   The modified build array.
+   */
+  public function removeSubmittedInfo(array $build) {
+    unset($build['created']);
+    unset($build['uid']);
+    return $build;
+  }
+
+}
