diff --git a/config/install/search_api_autocomplete.settings.yml b/config/install/search_api_autocomplete.settings.yml
new file mode 100644
index 0000000..a2b7cd1
--- /dev/null
+++ b/config/install/search_api_autocomplete.settings.yml
@@ -0,0 +1,2 @@
+delay: 0
+scripts: []
diff --git a/config/schema/search_api_autocomplete.schema.yml b/config/schema/search_api_autocomplete.schema.yml
new file mode 100644
index 0000000..9c6e58e
--- /dev/null
+++ b/config/schema/search_api_autocomplete.schema.yml
@@ -0,0 +1,17 @@
+search_api_autocomplete.search_api_autocomplete.*:
+  type: config_entity
+  label: 'Content type'
+  mapping:
+    id:
+      type: string
+    label:
+      type: string
+    index_id:
+      type: string
+    suggester_id:
+      type: string
+    type:
+      type: string
+    options:
+      # @todo Do we know more about the available options?
+      type: ignore
diff --git a/search_api_autocomplete.css b/css/search_api_autocomplete.css
similarity index 100%
rename from search_api_autocomplete.css
rename to css/search_api_autocomplete.css
diff --git a/search_api_autocomplete.js b/js/search_api_autocomplete.js
similarity index 100%
rename from search_api_autocomplete.js
rename to js/search_api_autocomplete.js
diff --git a/search_api_autocomplete.admin.inc b/search_api_autocomplete.admin.inc
deleted file mode 100644
index f59c7c2..0000000
--- a/search_api_autocomplete.admin.inc
+++ /dev/null
@@ -1,519 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains page callbacks and related functions for the admin UI.
- */
-
-/**
- * Form displaying an overview over all searches available for autocompletion.
- *
- * @param SearchApiIndex $index
- *   The index for which autocompletion searches should be configured.
- *
- * @see search_api_autocomplete_admin_overview_submit()
- * @see search_api_autocomplete_admin_overview_submit_delete()
- * @ingroup forms
- */
-function search_api_autocomplete_admin_overview(array $form, array &$form_state, SearchApiIndex $index) {
-  $form = array();
-  $form_state['index'] = $index;
-  $index_id = $index->machine_name;
-
-  $available_suggesters = search_api_autocomplete_suggesters_for_index($index);
-  if (!$available_suggesters) {
-    $args = array(
-      '@feature' => 'search_api_autocomplete',
-      '@service_classes_url' => url('https://www.drupal.org/node/1254698#service-classes'),
-    );
-    drupal_set_message(t('There are currently no suggester plugins installed that support this index. To solve this problem, you can either:<ul><li>move the index to a server which supports the "@feature" feature (see the <a href="@service_classes_url">available service class</a>)</li><li>or install a module providing a new suggester plugin that supports this index</li></ul>', $args), 'error');
-    if (search_api_autocomplete_search_load_multiple(FALSE, array('index_id' => $index_id))) {
-      $form['description'] = array(
-        '#type' => 'item',
-        '#title' => t('Delete autocompletion settings'),
-        '#description' => t("If you won't use autocompletion with this index anymore, you can delete all autocompletion settings associated with it. " .
-            "This will delete all autocompletion settings on this index. Settings on other indexes won't be influenced."),
-      );
-      $form['button'] = array(
-        '#type' => 'submit',
-        '#value' => t('Delete autocompletion settings'),
-        '#submit' => array('search_api_autocomplete_admin_overview_submit_delete'),
-      );
-    }
-    return $form;
-  }
-
-  $form['#tree'] = TRUE;
-  $types = search_api_autocomplete_get_types();
-  $searches = search_api_autocomplete_search_load_multiple(FALSE, array('index_id' => $index_id));
-  $show_status = FALSE;
-  foreach ($types as $type => $info) {
-    if (empty($info['list searches'])) {
-      continue;
-    }
-    $t_searches = $info['list searches']($index);
-    if (empty($t_searches)) {
-      $t_searches = array();
-    }
-    foreach ($t_searches as $id => $search) {
-      if (isset($searches[$id])) {
-        $types[$type]['searches'][$id] = $searches[$id];
-        $show_status |= $searches[$id]->hasStatus(ENTITY_IN_CODE);
-        unset($searches[$id]);
-      }
-      else {
-        reset($available_suggesters);
-        $search += array(
-          'machine_name' => $id,
-          'index_id' => $index_id,
-          'suggester_id' => key($available_suggesters),
-          'type' => $type,
-          'enabled' => 0,
-          'options' => array(),
-        );
-        $search['options'] += array(
-          'results' => TRUE,
-          'fields' => array(),
-        );
-        $types[$type]['searches'][$id] = entity_create('search_api_autocomplete_search', $search);
-      }
-    }
-  }
-  foreach ($searches as $id => $search) {
-    $type = isset($types[$search->type]) ? $search->type : '';
-    $types[$type]['searches'][$id] = $search;
-    $types[$type]['unavailable'][$id] = TRUE;
-    $show_status |= $search->hasStatus(ENTITY_IN_CODE);
-  }
-  $base_path = 'admin/config/search/search_api/index/' . $index_id . '/autocomplete/';
-  foreach ($types as $type => $info) {
-    if (empty($info['searches'])) {
-      continue;
-    }
-    if (!$type) {
-      $info += array(
-        'name' => t('Unavailable search types'),
-        'description' => t("The modules providing these searches were disabled or uninstalled. If you won't use them anymore, you can delete their settings."),
-      );
-    }
-    elseif (!empty($info['unavailable'])) {
-      $info['description'] .= '</p><p>' . t("The searches marked with an asterisk (*) are currently not available, possibly because they were deleted. If you won't use them anymore, you can delete their settings.");
-    }
-    $form[$type] = array(
-      '#type' => 'fieldset',
-      '#title' => $info['name'],
-    );
-    if (!empty($info['description'])) {
-      $form[$type]['#description'] = '<p>' . $info['description'] . '</p>';
-    }
-    $form[$type]['searches']['#theme'] = 'tableselect';
-    $form[$type]['searches']['#header'] = array();
-    if ($show_status) {
-      $form[$type]['searches']['#header']['status'] = t('Status');
-    }
-    $form[$type]['searches']['#header'] += array(
-      'name' => t('Name'),
-      'operations' => t('Operations'),
-    );
-    $form[$type]['searches']['#empty'] = '';
-    $form[$type]['searches']['#js_select'] = TRUE;
-    foreach ($info['searches'] as $id => $search) {
-      $form[$type]['searches'][$id] = array(
-        '#type' => 'checkbox',
-        '#default_value' => $search->enabled,
-        '#parents' => array('searches', $id),
-      );
-      $unavailable = !empty($info['unavailable'][$id]);
-      if ($unavailable) {
-        $form[$type]['searches'][$id]['#default_value'] = FALSE;
-        $form[$type]['searches'][$id]['#disabled'] = TRUE;
-      }
-      $form_state['searches'][$id] = $search;
-      $options = &$form[$type]['searches']['#options'][$id];
-      if ($show_status) {
-        $options['status'] = isset($search->status) ? theme('entity_status', array('status' => $search->status)) : '';;
-      }
-      $options['name'] = $search->name;
-      if ($unavailable) {
-        $options['name'] = '* ' . $options['name'];
-      }
-      $items = array();
-      if (!$unavailable && !empty($search->id)) {
-        $items[] = l(t('edit'), $base_path . $id . '/edit');
-      }
-      if (!empty($search->status) && ($search->hasStatus(ENTITY_CUSTOM))) {
-        $title = $search->hasStatus(ENTITY_IN_CODE) ? t('revert') : t('delete');
-        $items[] = l($title, $base_path . $id . '/delete');
-      }
-      if ($items) {
-        $variables = array(
-          'items' => $items,
-          'attributes' => array('class' => array('inline')),
-        );
-        $options['operations'] = theme('item_list', $variables);
-      }
-      else {
-        $options['operations'] = '';
-      }
-      unset($options);
-    }
-  }
-
-  if (!element_children($form)) {
-    $form['message']['#markup'] = '<p>' . t('There are currently no searches known for this index.') . '</p>';
-  }
-  else {
-    $form['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Save'),
-    );
-  }
-
-  return $form;
-}
-
-/**
- * Submit callback for search_api_autocomplete_admin_overview().
- *
- * @see search_api_autocomplete_admin_overview()
- */
-function search_api_autocomplete_admin_overview_submit(array $form, array &$form_state) {
-  $msg = t('The settings have been saved.');
-  foreach ($form_state['values']['searches'] as $id => $enabled) {
-    $search = $form_state['searches'][$id];
-    if ($search->enabled != $enabled) {
-      $change = TRUE;
-      if (!empty($search->is_new)) {
-        $options['query']['destination'] = $_GET['q'];
-        $options['fragment'] = 'module-search_api_autocomplete';
-        $vars['@perm_url'] = url('admin/people/permissions', $options);
-        $msg = t('The settings have been saved. Please remember to set the <a href="@perm_url">permissions</a> for the newly enabled searches.', $vars);
-      }
-      $search->enabled = $enabled;
-      $search->save();
-    }
-  }
-  drupal_set_message(empty($change) ? t('No values were changed.') : $msg);
-}
-
-/**
- * Submit callback for search_api_autocomplete_admin_overview(), when all
- * settings for the index should be deleted.
- *
- * @see search_api_autocomplete_admin_overview()
- */
-function search_api_autocomplete_admin_overview_submit_delete(array $form, array &$form_state) {
-  $index = $form_state['index'];
-  $ids = array_keys(search_api_autocomplete_search_load_multiple(FALSE, array('index_id' => $index->machine_name)));
-  if ($ids) {
-    entity_delete_multiple('search_api_autocomplete_search', $ids);
-    drupal_set_message(t('All autocompletion settings stored for this index were deleted.'));
-  }
-  else {
-    drupal_set_message(t('There were no settings to delete.'), 'warning');
-  }
-  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name;
-}
-
-/**
- * Form for editing the autocompletion settings for a search.
- *
- * @param SearchApiAutocompleteSearch $search
- *   The search whose settings should be edited.
- *
- * @ingroup forms
- *
- * @see search_api_autocomplete_admin_search_edit_validate()
- * @see search_api_autocomplete_admin_search_edit_submit()
- */
-function search_api_autocomplete_admin_search_edit(array $form, array &$form_state, SearchApiAutocompleteSearch $search) {
-  drupal_set_title(t('Configure autocompletion for %search', array('%search' => $search->name)), PASS_THROUGH);
-
-  // If this is a re-build (i.e., most likely an AJAX call due to a new
-  // suggester being selected), prepare the suggester sub-form state
-  // accordingly.
-  $selected_suggester_id = $search->getSuggesterId();
-  if (!empty($form_state['values']['suggester_id'])) {
-    $selected_suggester_id = $form_state['values']['suggester_id'];
-    // Don't let submitted values for a different suggester influence another
-    // suggester's form.
-    if ($selected_suggester_id != $form_state['values']['old_suggester_id']) {
-      unset($form_state['values']['options']['suggester_configuration']);
-      unset($form_state['input']['options']['suggester_configuration']);
-    }
-  }
-
-  $form_state['search'] = $search;
-  $form_state['type'] = $type = search_api_autocomplete_get_types($search->type);
-  if (!$type) {
-    drupal_set_message(t('No information about the type of this search was found.'), 'error');
-    return array();
-  }
-  $form['#tree'] = TRUE;
-  $form['enabled'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Enabled'),
-    '#default_value' => $search->enabled,
-  );
-  $form['suggester_id'] = array(
-    '#type' => 'radios',
-    '#title' => t('Suggester plugin'),
-    '#description' => t('Choose the suggester implementation to use.'),
-    '#options' => array(),
-    '#required' => TRUE,
-    '#default_value' => $selected_suggester_id,
-    '#ajax' => array(
-      'callback' => 'search_api_autocomplete_suggester_ajax_callback',
-      'wrapper' => 'search-api-suggester-options',
-    ),
-  );
-
-  foreach (search_api_autocomplete_suggesters_for_index($search->index()) as $suggester_id => $definition) {
-    // Load the suggester plugin. If the suggester is unchanged from the one on
-    // the saved version of the search, use the saved configuration.
-    $configuration = array();
-    if ($suggester_id == $search->getSuggesterId() && isset($search->options['suggester_configuration'])) {
-      $configuration = $search->options['suggester_configuration'];
-    }
-    $suggester = search_api_autocomplete_suggester_load($suggester_id, $search, $configuration);
-    if (!$suggester) {
-      continue;
-    }
-
-    // Add the suggester to the suggester options.
-    $form['suggester_id']['#options'][$suggester_id] = check_plain($suggester->label());
-
-    // Then, also add the configuration form for the selected suggester.
-    if ($suggester_id != $selected_suggester_id) {
-      continue;
-    }
-    $suggester_form_state = &search_api_autocomplete_get_plugin_form_state($form_state);
-    $suggester_form = $suggester->buildConfigurationForm(array(), $suggester_form_state);
-    if ($suggester_form) {
-      $form['options']['suggester_configuration'] = $suggester_form;
-      $form['options']['suggester_configuration']['#type'] = 'fieldset';
-      $form['options']['suggester_configuration']['#title'] = t('Configure the %suggester suggester plugin', array('%suggester' => $suggester->label()));
-      $form['options']['suggester_configuration']['#description'] = $suggester->getDescription();
-      $form['options']['suggester_configuration']['#collapsible'] = TRUE;
-    }
-    else {
-      $form['options']['suggester_configuration']['#type'] = 'item';
-    }
-    $form['options']['suggester_configuration']['#prefix'] = '<div id="search-api-suggester-options">';
-    $form['options']['suggester_configuration']['#suffix'] = '</div>';
-  }
-  $form['options']['suggester_configuration']['old_suggester_id'] = array(
-    '#type' => 'hidden',
-    '#value' => $selected_suggester_id,
-    '#tree' => FALSE,
-  );
-
-  // If there is only a single plugin available, hide the "suggester" option.
-  if (count($form['suggester_id']['#options']) == 1) {
-    $form['suggester_id'] = array(
-      '#type' => 'value',
-      '#value' => key($form['suggester_id']['#options']),
-    );
-  }
-
-  $form['options']['min_length'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Minimum length of keywords for autocompletion'),
-    '#description' => t('If the entered keywords are shorter than this, no autocomplete suggestions will be displayed.'),
-    '#default_value' => isset($search->options['min_length']) ? $search->options['min_length'] : 1,
-    '#validate' => array('element_validate_integer_positive'),
-  );
-  $form['options']['results'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Display result count estimates'),
-    '#description' => t('Display the estimated number of result for each suggestion. ' .
-      'This option might not have an effect for some servers or types of suggestion.'),
-    '#default_value' => !empty($search->options['results']),
-  );
-
-  $custom_form = empty($form['options']['custom']) ? array() : $form['options']['custom'];
-  if (!empty($type['config form']) && function_exists($type['config form']) && is_array($custom_form = $type['config form']($custom_form, $form_state, $search))) {
-    $form['options']['custom'] = $custom_form;
-  }
-
-  $form['advanced'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Advanced settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
-  );
-  $form['advanced']['submit_button_selector'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Search button selector'),
-    '#description' => t('<a href="@jquery_url">jQuery selector</a> to identify the search button in the search form. Use the ID attribute of the "Search" submit button to prevent issues when another button is present (e.g., "Reset"). The selector is evaluated relative to the form. The default value is ":submit".', array('@jquery_url' => 'https://api.jquery.com/category/selectors/')),
-    '#default_value' => isset($search->options['submit_button_selector']) ? $search->options['submit_button_selector'] : ':submit',
-    '#required' => TRUE,
-    '#parents' => array('options', 'submit_button_selector'),
-  );
-  $form['advanced']['autosubmit'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Enable auto-submit'),
-    '#description' => t('When enabled, the search form will automatically be submitted when a selection is made by pressing "Enter".'),
-    '#default_value' => isset($search->options['autosubmit']) ? $search->options['autosubmit'] : TRUE,
-    '#parents' => array('options', 'autosubmit'),
-  );
-
-  $form['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save'),
-  );
-
-  return $form;
-}
-
-/**
- * Form AJAX handler for search_api_autocomplete_admin_search_edit().
- */
-function search_api_autocomplete_suggester_ajax_callback(array $form, array &$form_state) {
-  return $form['options']['suggester_configuration'];
-}
-
-/**
- * Validate callback for search_api_autocomplete_admin_search_edit().
- *
- * @see search_api_autocomplete_admin_search_edit()
- * @see search_api_autocomplete_admin_search_edit_submit()
- */
-function search_api_autocomplete_admin_search_edit_validate(array $form, array &$form_state) {
-  $values = &$form_state['values'];
-  // Call the config form validation method of the selected suggester plugin,
-  // but only if it was the same plugin that created the form.
-  if ($values['suggester_id'] == $values['old_suggester_id']) {
-    $configuration = array();
-    if (!empty($values['options']['suggester_configuration'])) {
-      $configuration = $values['options']['suggester_configuration'];
-    }
-    $suggester = search_api_autocomplete_suggester_load($values['suggester_id'], $form_state['search'], $configuration);
-    $suggester_form = $form['options']['suggester_configuration'];
-    unset($suggester_form['old_suggester_id']);
-    $suggester_form_state = &search_api_autocomplete_get_plugin_form_state($form_state);
-    $suggester->validateConfigurationForm($suggester_form, $suggester_form_state);
-  }
-
-  if (!empty($form_state['type']['config form'])) {
-    $f = $form_state['type']['config form'] . '_validate';
-    if (function_exists($f)) {
-      $custom_form = empty($form['options']['custom']) ? array() : $form['options']['custom'];
-      $f($custom_form, $form_state, $values['options']['custom']);
-    }
-  }
-}
-
-/**
- * Submit callback for search_api_autocomplete_admin_search_edit().
- *
- * @see search_api_autocomplete_admin_search_edit()
- * @see search_api_autocomplete_admin_search_edit_validate()
- */
-function search_api_autocomplete_admin_search_edit_submit(array $form, array &$form_state) {
-  $values = &$form_state['values'];
-  if (!empty($form_state['type']['config form'])) {
-    $f = $form_state['type']['config form'] . '_submit';
-    if (function_exists($f)) {
-      $custom_form = empty($form['options']['custom']) ? array() : $form['options']['custom'];
-      $f($custom_form, $form_state, $values['options']['custom']);
-    }
-  }
-
-  $search = $form_state['search'];
-  $search->enabled = $values['enabled'];
-  $search->suggester_id = $values['suggester_id'];
-
-  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $search->index_id . '/autocomplete';
-
-  // Take care of custom options that aren't changed in the config form.
-  if (!empty($search->options['custom'])) {
-    if (!isset($values['options']['custom'])) {
-      $values['options']['custom'] = array();
-    }
-    $values['options']['custom'] += $search->options['custom'];
-  }
-
-  // Allow the suggester to decide how to save its configuration. If the user
-  // has disabled JS in the browser, or AJAX didn't work for some other reason,
-  // a different suggester might be selected than that which created the config
-  // form. In that case, we don't call the form submit method, save empty
-  // configuration for the plugin and stay on the page.
-  if ($values['suggester_id'] == $values['old_suggester_id']) {
-    $configuration = array();
-    if (!empty($values['options']['suggester_configuration'])) {
-      $configuration = $values['options']['suggester_configuration'];
-    }
-    $suggester = search_api_autocomplete_suggester_load($values['suggester_id'], $search, $configuration);
-    $suggester_form = $form['options']['suggester_configuration'];
-    unset($suggester_form['old_suggester_id']);
-    $suggester_form_state = &search_api_autocomplete_get_plugin_form_state($form_state);
-    $suggester->submitConfigurationForm($suggester_form, $suggester_form_state);
-    $values['options']['suggester_configuration'] = $suggester->getConfiguration();
-  }
-  else {
-    $values['options']['suggester_configuration'] = array();
-    $form_state['redirect'] = NULL;
-    drupal_set_message(t('The used suggester plugin has changed. Please review the configuration for the new plugin.'), 'warning');
-  }
-
-  $search->options = $values['options'];
-
-  $search->save();
-  drupal_set_message(t('The autocompletion settings for the search have been saved.'));
-}
-
-/**
- * Returns a new form state for the suggester configuration sub-form.
- *
- * @param array $form_state
- *   The original form state.
- *
- * @return array
- *   A form state for the sub-form.
- */
-function &search_api_autocomplete_get_plugin_form_state(array &$form_state) {
-  $sub_form_state = &$form_state['suggester_form_state'];
-
-  foreach (array('values', 'input') as $key) {
-    if (!isset($form_state[$key]['options']['suggester_configuration'])) {
-      $form_state[$key]['options']['suggester_configuration'] = array();
-    }
-    $sub_form_state[$key] = &$form_state[$key]['options']['suggester_configuration'];
-  }
-
-  foreach (array('rebuild', 'rebuild_info', 'redirect') as $key) {
-    $sub_form_state[$key] = &$form_state[$key];
-  }
-
-  return $sub_form_state;
-}
-
-/**
- * Form for deleting the autocompletion settings of a search.
- *
- * @param SearchApiAutocompleteSearch $search
- *   The search whose settings should be deleted.
- *
- * @see search_api_autocomplete_admin_search_delete_submit()
- * @ingroup forms
- */
-function search_api_autocomplete_admin_search_delete(array $form, array &$form_state, SearchApiAutocompleteSearch $search) {
-  $form_state['search'] = $search;
-  return confirm_form(
-    $form,
-    t('Do you really want to delete the autocompletion settings for search %search?', array('%search' => $search->name)),
-    'admin/config/search/search_api/index/' . $search->index_id . '/autocomplete'
-  );
-}
-
-/**
- * Submit callback for search_api_autocomplete_admin_search_delete().
- *
- * @see search_api_autocomplete_admin_search_delete()
- */
-function search_api_autocomplete_admin_search_delete_submit(array $form, array &$form_state) {
-  $form_state['search']->delete();
-  drupal_set_message(t('The autocompletion settings for the search have been deleted.'));
-  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $form_state['search']->index_id . '/autocomplete';
-}
diff --git a/search_api_autocomplete.api.php b/search_api_autocomplete.api.php
index 6f06e66..409fd24 100644
--- a/search_api_autocomplete.api.php
+++ b/search_api_autocomplete.api.php
@@ -240,12 +240,12 @@ function hook_search_api_autocomplete_suggestions_alter(array &$suggestions, arr
  *
  * @param string[] $fields
  *   The machine names of the detected fulltext fields.
- * @param SearchApiAutocompleteSearch $search
+ * @param \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search
  *   The related search autocomplete object.
  * @param view $view
  *   The view for which fulltext fields should be determined.
  */
-function hook_search_api_autocomplete_views_fulltext_fields_alter(array &$fields, SearchApiAutocompleteSearch $search, view $view) {
+function hook_search_api_autocomplete_views_fulltext_fields_alter(array &$fields, \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search, view $view) {
   // Add a new field.
   $fields[] = 'extra_search_api_views_fulltext';
 }
@@ -266,7 +266,7 @@ function hook_search_api_autocomplete_views_fulltext_fields_alter(array &$fields
  * Also, name and machine name have to respect the length constraints from
  * search_api_autocomplete_schema().
  *
- * @param SearchApiIndex $index
+ * @param \Drupal\search_api\IndexInterface $index
  *   The index whose searches should be returned.
  *
  * @return array
@@ -277,7 +277,7 @@ function hook_search_api_autocomplete_views_fulltext_fields_alter(array &$fields
  *     Type-specific options should go into the "custom" nested key in these
  *     options.
  */
-function example_list_autocomplete_searches(SearchApiIndex $index) {
+function example_list_autocomplete_searches(\Drupal\search_api\IndexInterface $index) {
   $ret = array();
   $result = db_query('SELECT name, machine_name, extra FROM {example_searches} WHERE index_id = :id', array($index->machine_name));
   foreach ($result as $row) {
@@ -295,21 +295,21 @@ function example_list_autocomplete_searches(SearchApiIndex $index) {
 /**
  * Create the query that would be issued for the given search for the complete keys.
  *
- * @param SearchApiAutocompleteSearch $search
+ * @param \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search
  *   The search for which to create the query.
  * @param $complete
  *   A string containing the complete search keys.
  * @param $incomplete
  *   A string containing the incomplete last search key.
  *
- * @return SearchApiQueryInterface
+ * @return \Drupal\search_api\Query\QueryInterface
  *   The query that would normally be executed when only $complete was entered
  *   as the search keys for the given search.
  *
- * @throws SearchApiException
+ * @throws \Drupal\search_api\SearchApiException
  *   If the query couldn't be created.
  */
-function example_create_autocomplete_query(SearchApiAutocompleteSearch $search, $complete, $incomplete) {
+function example_create_autocomplete_query(\Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $complete, $incomplete) {
   $query = search_api_query($search->index_id);
   if ($complete) {
     $query->keys($complete);
@@ -340,7 +340,7 @@ function example_create_autocomplete_query(SearchApiAutocompleteSearch $search,
  * @see example_autocomplete_config_form_validate()
  * @see example_autocomplete_config_form_submit()
  */
-function example_autocomplete_config_form(array $form, array &$form_state, SearchApiAutocompleteSearch $search) {
+function example_autocomplete_config_form(array $form, array &$form_state, \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search) {
   $form['user_filters'] = array(
     '#type' => 'textarea',
     '#title' => t('Custom filters'),
@@ -420,7 +420,7 @@ function example_form_example_search_form_alter(array &$form, array &$form_state
   // 'list searches' callback.
   $search_id = 'example_' . $form_state['search id'];
   // Look up the corresponding autocompletion configuration, if it exists.
-  $search = search_api_autocomplete_search_load($search_id);
+  $search = \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch::load($search_id);
   // Check whether autocompletion for the search is enabled.
   // (This is also checked automatically later, so could be skipped here.)
   if (!empty($search->enabled)) {
diff --git a/search_api_autocomplete.entity.php b/search_api_autocomplete.entity.php
deleted file mode 100644
index b1356e8..0000000
--- a/search_api_autocomplete.entity.php
+++ /dev/null
@@ -1,292 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains SearchApiAutocompleteSearch.
- */
-
-/**
- * Describes the autocomplete settings for a certain search.
- */
-class SearchApiAutocompleteSearch extends Entity {
-
-  // Entity properties, loaded from the database:
-
-  /**
-   * @var integer
-   */
-  public $id;
-
-  /**
-   * @var string
-   */
-  public $machine_name;
-
-  /**
-   * @var string
-   */
-  public $name;
-
-  /**
-   * @var integer
-   */
-  public $index_id;
-
-  /**
-   * @var string
-   */
-  public $suggester_id;
-
-  /**
-   * @var string
-   */
-  public $type;
-
-  /**
-   * @var boolean
-   */
-  public $enabled;
-
-  /**
-   * An array of options for this search, containing any of the following:
-   * - results: Boolean indicating whether to also list the estimated number of
-   *   results for each suggestion (if possible).
-   * - fields: Array containing the fulltext fields to use for autocompletion.
-   * - custom: An array of type-specific settings.
-   *
-   * @var array
-   */
-  public $options = array();
-
-  // Inferred properties, for caching:
-
-  /**
-   * @var SearchApiIndex
-   */
-  protected $index;
-
-  /**
-   * @var SearchApiServer
-   */
-  protected $server;
-
-  /**
-   * The suggester plugin this search uses.
-   *
-   * @var SearchApiAutocompleteSuggesterInterface
-   */
-  protected $suggester;
-
-  /**
-   * Constructs a SearchApiAutocompleteSearch.
-   *
-   * @param array $values
-   *   The entity properties.
-   */
-  public function __construct(array $values = array()) {
-    parent::__construct($values, 'search_api_autocomplete_search');
-  }
-
-  /**
-   * @return SearchApiIndex
-   *   The index this search belongs to.
-   */
-  public function index() {
-    if (!isset($this->index)) {
-      $this->index = search_api_index_load($this->index_id);
-      if (!$this->index) {
-        $this->index = FALSE;
-      }
-    }
-    return $this->index;
-  }
-
-  /**
-   * Retrieves the server this search would at the moment be executed on.
-   *
-   * @return SearchApiServer
-   *   The server this search would at the moment be executed on.
-   *
-   * @throws SearchApiException
-   *   If a server is set for the index but it doesn't exist.
-   */
-  public function server() {
-    if (!isset($this->server)) {
-      if (!$this->index() || !$this->index()->server) {
-        $this->server = FALSE;
-      }
-      else {
-        $this->server = $this->index()->server();
-        if (!$this->server) {
-          $this->server = FALSE;
-        }
-      }
-    }
-    return $this->server;
-  }
-
-  /**
-   * Retrieves the ID of the suggester plugin for this search.
-   *
-   * @return string
-   *   This search's suggester plugin's ID.
-   */
-  public function getSuggesterId() {
-    return $this->suggester_id;
-  }
-
-  /**
-   * Retrieves the suggester plugin for this search.
-   *
-   * @param bool $reset
-   *   (optional) If TRUE, clear the internal static cache and reload the
-   *   suggester.
-   *
-   * @return SearchApiAutocompleteSuggesterInterface|null
-   *   This search's suggester plugin, or NULL if it could not be loaded.
-   */
-  public function getSuggester($reset = FALSE) {
-    if (!isset($this->suggester) || $reset) {
-      $configuration = !empty($this->options['suggester_configuration']) ? $this->options['suggester_configuration'] : array();
-      $this->suggester = search_api_autocomplete_suggester_load($this->suggester_id, $this, $configuration);
-      if (!$this->suggester) {
-        $variables['@search'] = $this->machine_name;
-        $variables['@index'] = $this->index() ? $this->index()->label() : $this->index_id;
-        $variables['@suggester_id'] = $this->suggester_id;
-        watchdog('search_api_autocomplete', 'Autocomplete search @search on index @index specifies an invalid suggester plugin @suggester_id.', $variables, WATCHDOG_ERROR);
-        $this->suggester = FALSE;
-      }
-    }
-    return $this->suggester ? $this->suggester : NULL;
-  }
-
-  /**
-   * Determines whether autocompletion is currently supported for this search.
-   *
-   * @return bool
-   *   TRUE if autocompletion is possible for this search with the current
-   *   settings; FALSE otherwise.
-   */
-  public function supportsAutocompletion() {
-    return $this->index() && $this->getSuggester() && $this->getSuggester()->supportsIndex($this->index());
-  }
-
-  /**
-   * Helper method for altering a textfield form element to use autocompletion.
-   */
-  public function alterElement(array &$element, array $fields = array()) {
-    if (search_api_autocomplete_access($this)) {
-      // Add option defaults (in case of updates from earlier versions).
-      $options = $this->options + array(
-        'submit_button_selector' => ':submit',
-        'autosubmit' => TRUE,
-        'min_length' => 1,
-      );
-
-      $fields_string = $fields ? implode(' ', $fields) : '-';
-
-      $module_path = drupal_get_path('module', 'search_api_autocomplete');
-      $autocomplete_path = 'search_api_autocomplete/' . $this->machine_name . '/' . $fields_string;
-
-      $js_settings = array();
-      if ($options['submit_button_selector'] != ':submit') {
-        $js_settings['selector'] = $options['submit_button_selector'];
-      }
-      if (($delay = variable_get('search_api_autocomplete_delay')) !== NULL) {
-        $js_settings['delay'] = $delay;
-      }
-
-      // Allow overriding of the default handler with a custom script.
-      $path_overrides = variable_get('search_api_autocomplete_scripts', array());
-      if (!empty($path_overrides[$this->machine_name])) {
-        $autocomplete_path = NULL;
-        $override = $path_overrides[$this->machine_name];
-        if (is_scalar($override)) {
-          $autocomplete_path = url($override, array('absolute' => TRUE, 'query' => array('machine_name' => $this->machine_name)));
-        }
-        elseif (!empty($override['#callback']) && is_callable($override['#callback'])) {
-          $autocomplete_path = call_user_func($override['#callback'], $this, $element, $override);
-        }
-        if (!$autocomplete_path) {
-          return;
-        }
-        $js_settings['custom_path'] = TRUE;
-      }
-
-      $element['#attached']['css'][] = $module_path . '/search_api_autocomplete.css';
-      $element['#attached']['js'][] = $module_path . '/search_api_autocomplete.js';
-      if ($js_settings) {
-        $element['#attached']['js'][] = array(
-          'type' => 'setting',
-          'data' => array(
-            'search_api_autocomplete' => array(
-              $this->machine_name => $js_settings,
-            ),
-          ),
-        );
-      }
-
-      $element['#autocomplete_path'] = $autocomplete_path;
-      $element += array('#attributes' => array());
-      $element['#attributes'] += array('class'=> array());
-      if ($options['autosubmit']) {
-        $element['#attributes']['class'][] = 'auto_submit';
-      }
-      $element['#attributes']['data-search-api-autocomplete-search'] = $this->machine_name;
-      if ($options['min_length'] > 1) {
-        $element['#attributes']['data-min-autocomplete-length'] = $options['min_length'];
-      }
-    }
-  }
-
-  /**
-   * Split a string with search keywords into two parts.
-   *
-   * The first part consists of all words the user has typed completely, the
-   * second one contains the beginning of the last, possibly incomplete word.
-   *
-   * @return array
-   *   An array with $keys split into exactly two parts, both of which may be
-   *   empty.
-   */
-  public function splitKeys($keys) {
-    $keys = ltrim($keys);
-    // If there is whitespace or a quote on the right, all words have been
-    // completed.
-    if (rtrim($keys, " \"") != $keys) {
-      return array(rtrim($keys, ' '), '');
-    }
-    if (preg_match('/^(.*?)\s*"?([\S]*)$/', $keys, $m)) {
-      return array($m[1], $m[2]);
-    }
-    return array('', $keys);
-  }
-
-  /**
-   * Create the query that would be issued for this search for the complete keys.
-   *
-   * @param $complete
-   *   A string containing the complete search keys.
-   * @param $incomplete
-   *   A string containing the incomplete last search key.
-   *
-   * @return SearchApiQueryInterface
-   *   The query that would normally be executed when only $complete was entered
-   *   as the search keys for this search.
-   *
-   * @throws SearchApiException
-   *   If the query couldn't be created.
-   */
-  public function getQuery($complete, $incomplete) {
-    $info = search_api_autocomplete_get_types($this->type);
-    if (empty($info['create query'])) {
-      return NULL;
-    }
-    $query = $info['create query']($this, $complete, $incomplete);
-    if ($complete && !$query->getKeys()) {
-      $query->keys($complete);
-    }
-    return $query;
-  }
-
-}
diff --git a/search_api_autocomplete.info b/search_api_autocomplete.info
deleted file mode 100644
index 0d6ab24..0000000
--- a/search_api_autocomplete.info
+++ /dev/null
@@ -1,10 +0,0 @@
-name = Search API autocomplete
-description = "Adds autocomplete functionality to searches."
-dependencies[] = search_api
-core = 7.x
-package = Search
-
-files[] = search_api_autocomplete.entity.php
-files[] = src/SearchApiAutocompleteSuggesterInterface.php
-files[] = src/SearchApiAutocompleteSuggesterPluginBase.php
-files[] = src/SearchApiAutocompleteServerSuggester.php
diff --git a/search_api_autocomplete.info.yml b/search_api_autocomplete.info.yml
new file mode 100644
index 0000000..08ca10a
--- /dev/null
+++ b/search_api_autocomplete.info.yml
@@ -0,0 +1,7 @@
+name: Search API autocomplete
+description: "Adds autocomplete functionality to searches."
+dependencies:
+  - search_api
+core: 8.x
+type: module
+package: Search
diff --git a/search_api_autocomplete.install b/search_api_autocomplete.install
deleted file mode 100644
index 4a52e49..0000000
--- a/search_api_autocomplete.install
+++ /dev/null
@@ -1,203 +0,0 @@
-<?php
-
-/**
- * @file
- * Install, update and uninstall functions for the Search API autocomplete module.
- */
-
-/**
- * Implements hook_schema().
- */
-function search_api_autocomplete_schema() {
-  $schema['search_api_autocomplete_search'] = array(
-    'description' => 'Stores autocomplete settings for searches on Search API indexes.',
-    'fields' => array(
-      'id' => array(
-        'description' => 'The primary identifier for a search.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ),
-      'machine_name' => array(
-        'description' => 'A string identifier for a search.',
-        'type' => 'varchar',
-        'length' => 100,
-        'not null' => TRUE,
-      ),
-      'name' => array(
-        'description' => 'A human-readable name for the search.',
-        'type' => 'varchar',
-        'length' => 50,
-      ),
-      'index_id' => array(
-        'description' => 'The {search_api_index}.machine_name this search belongs to.',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => TRUE,
-      ),
-      'suggester_id' => array(
-        'description' => 'The plugin ID of the suggester this search should use.',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => TRUE,
-      ),
-      'type' => array(
-        'description' => 'The type of search, usually a module name.',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => TRUE,
-      ),
-      'enabled' => array(
-        'description' => 'A flag indicating whether autocompletion for this search is enabled.',
-        'type' => 'int',
-        'size' => 'tiny',
-        'not null' => TRUE,
-        'default' => 1,
-      ),
-      'options' => array(
-        'description' => 'The options used to configure autocompletion for this search.',
-        'type' => 'text',
-        'serialize' => TRUE,
-        'not null' => TRUE,
-      ),
-      'status' => array(
-        'description' => 'The exportable status of the entity.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0x01,
-        'size' => 'tiny',
-      ),
-      'module' => array(
-        'description' => 'The name of the providing module if the entity has been defined in code.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-      ),
-    ),
-    'primary key' => array('id'),
-    'unique keys' => array(
-      'machine_name' => array('machine_name'),
-    ),
-    'indexes' => array(
-      'enabled' => array('enabled'),
-    ),
-  );
-
-  return $schema;
-}
-
-/**
- * Implements hook_uninstall().
- */
-function search_api_autocomplete_uninstall() {
-  variable_del('search_api_autocomplete_delay');
-  variable_del('search_api_autocomplete_scripts');
-}
-
-/**
- * Update permissions to the new system with search-specific permissions.
- */
-function search_api_autocomplete_update_7101() {
-  $roles = db_select('role_permission', 'r')
-    ->fields('r', array('rid'))
-    ->condition('permission', 'use search_api_autocomplete')
-    ->execute()
-    ->fetchCol();
-  $searches = db_select('search_api_autocomplete_search', 's')
-    ->fields('s', array('machine_name'))
-    ->execute()
-    ->fetchCol();
-  try {
-    $tx = db_transaction();
-
-    db_delete('role_permission')
-      ->condition('permission', 'use search_api_autocomplete')
-      ->execute();
-    if ($roles && $searches) {
-      $insert = db_insert('role_permission')
-        ->fields(array('rid', 'permission', 'module'));
-      foreach ($roles as $rid) {
-        foreach ($searches as $id) {
-          $insert->values(array(
-            'rid' => $rid,
-            'permission' => 'use search_api_autocomplete for ' . $id,
-            'module' => 'search_api_autocomplete',
-          ));
-        }
-      }
-      $insert->execute();
-    }
-  }
-  catch (PDOException $e) {
-    $tx->rollback();
-    throw new DrupalUpdateException(t('A database error occurred during update: @msg', array('@msg' => $e->getMessage())));
-  }
-}
-
-/**
- * Convert settings for search pages to use machine names instead of IDs.
- */
-function search_api_autocomplete_update_7102() {
-  if (!db_table_exists('search_api_page')) {
-    return;
-  }
-
-  $tx = db_transaction();
-
-  try {
-    $pages = db_query('SELECT * FROM {search_api_page}')->fetchAllAssoc('id');
-    $searches = db_query('SELECT * FROM {search_api_autocomplete_search} WHERE type = :type', array(':type' => 'search_api_page'));
-    foreach ($searches as $search) {
-      $page_id = (int) substr($search->machine_name, 16);
-      if (isset($pages[$page_id])) {
-        $machine_name = 'search_api_page_' . $pages[$page_id]->machine_name;
-        $options = unserialize($search->options);
-        $options['custom']['page_id'] = $pages[$page_id]->machine_name;
-        db_update('search_api_autocomplete_search')
-          ->fields(array(
-            'machine_name' => $machine_name,
-            'options' => serialize($options),
-          ))
-          ->condition('id', $search->id)
-          ->execute();
-      }
-    }
-  }
-  catch (PDOException $e) {
-    $tx->rollback();
-    throw new DrupalUpdateException(t('A database error occurred during update: @msg', array('@msg' => $e->getMessage())));
-  }
-}
-
-/**
- * Add the {search_api_autocomplete_search}.suggester_id column.
- */
-function search_api_autocomplete_update_7103() {
-  // Set "server" as the suggester for all existing searches, then remove the
-  // default again from the schema.
-  $spec = array(
-    'description' => 'The plugin ID of the suggester this search should use.',
-    'type' => 'varchar',
-    'length' => 50,
-    'not null' => TRUE,
-    'default' => 'server',
-  );
-  db_add_field('search_api_autocomplete_search', 'suggester_id', $spec);
-  unset($spec['default']);
-  db_change_field('search_api_autocomplete_search', 'suggester_id', 'suggester_id', $spec);
-}
-
-/**
- * Fix the {search_api_autocomplete_search}.suggester_id field, where necessary.
- */
-function search_api_autocomplete_update_7104() {
-  $or = db_or();
-  $or->condition('suggester_id', '');
-  $or->isNull('suggester_id');
-  db_update('search_api_autocomplete_search')
-    ->fields(array(
-      'suggester_id' => 'server',
-    ))
-    ->condition($or)
-    ->execute();
-}
diff --git a/search_api_autocomplete.libraries.yml b/search_api_autocomplete.libraries.yml
new file mode 100644
index 0000000..7883d15
--- /dev/null
+++ b/search_api_autocomplete.libraries.yml
@@ -0,0 +1,6 @@
+search_api_autocomplete:
+  js:
+    js/search_api_autocomplete.js: {}
+  css:
+    theme:
+      css/search_api_autocomplete.css: {}
diff --git a/search_api_autocomplete.links.task.yml b/search_api_autocomplete.links.task.yml
new file mode 100644
index 0000000..9924021
--- /dev/null
+++ b/search_api_autocomplete.links.task.yml
@@ -0,0 +1,5 @@
+search_api_autocomplete.admin_overview:
+  route_name: search_api_autocomplete.admin_overview
+  base_route: entity.search_api_index.canonical
+  title: 'Autocomplete'
+  weight: 20
diff --git a/search_api_autocomplete.module b/search_api_autocomplete.module
index d107552..0370872 100644
--- a/search_api_autocomplete.module
+++ b/search_api_autocomplete.module
@@ -5,316 +5,43 @@
  * Adds autocomplete capabilities for Search API searches.
  */
 
+use Drupal\Core\Url;
+
 // Include the files with the module-specific implementations.
 require_once('search_api_autocomplete.search_api_page.inc');
 require_once('search_api_autocomplete.search_api_views.inc');
 
 /**
- * Implements hook_search_api_autocomplete_types().
- *
- * Adds search types for search pages and views. The actual implementations lie
- * in the above include files.
- */
-function search_api_autocomplete_search_api_autocomplete_types() {
-  $types = array();
-  if (module_exists('search_api_page')) {
-    $types['search_api_page'] = array(
-      'name' => t('Search pages'),
-      'description' => t('Searches provided by the <em>Search pages</em> module.'),
-      'list searches' => 'search_api_autocomplete_pages_searches',
-      'create query' => 'search_api_autocomplete_pages_query',
-    );
-  }
-  if (module_exists('search_api_views')) {
-    $types['search_api_views'] = array(
-      'name' => t('Search views'),
-      'description' => t('Searches provided by the <em>Search views</em> module.'),
-      'list searches' => 'search_api_autocomplete_views_searches',
-      'create query' => 'search_api_autocomplete_views_query',
-      'config form' => 'search_api_autocomplete_views_config_form',
-    );
-  }
-
-  return $types;
-}
-
-/**
- * Implements hook_search_api_autocomplete_suggester_info().
- */
-function search_api_autocomplete_search_api_autocomplete_suggester_info() {
-  $suggesters['server'] = array(
-    'label' => t('Retrieve from server'),
-    'description' => t('For compatible servers, ask the server for autocomplete suggestions.'),
-    'class' => 'SearchApiAutocompleteServerSuggester',
-  );
-
-  return $suggesters;
-}
-
-/**
- * Implements hook_menu().
- */
-function search_api_autocomplete_menu() {
-  // Autocompletion path
-  $items['search_api_autocomplete/%search_api_autocomplete_search'] = array(
-    'title' => 'Search API autocomplete',
-    'page callback' => 'search_api_autocomplete_autocomplete',
-    'page arguments' => array(1),
-    'access callback' => 'search_api_autocomplete_access',
-    'access arguments' => array(1),
-    'type' => MENU_CALLBACK,
-    'file' => 'search_api_autocomplete.pages.inc',
-  );
-
-  // Admin UI
-  $items['admin/config/search/search_api/index/%search_api_index/autocomplete'] = array(
-    'title' => 'Autocomplete',
-    'description' => 'Add autocompletion to searches for this index.',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('search_api_autocomplete_admin_overview', 5),
-    'access arguments' => array('administer search_api'),
-    'weight' => -1,
-    'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
-    'file' => 'search_api_autocomplete.admin.inc',
-  );
-  $items['admin/config/search/search_api/index/%/autocomplete/%search_api_autocomplete_search/edit'] = array(
-    'title' => 'Edit autocompletion settings',
-    'description' => 'Edit the autocompletion settings of a search.',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('search_api_autocomplete_admin_search_edit', 7),
-    'access arguments' => array('administer search_api'),
-    'file' => 'search_api_autocomplete.admin.inc',
-  );
-  $items['admin/config/search/search_api/index/%/autocomplete/%search_api_autocomplete_search/delete'] = array(
-    'title' => 'Delete autocompletion settings',
-    'description' => 'Delete the autocompletion settings of a search.',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('search_api_autocomplete_admin_search_delete', 7),
-    'access arguments' => array('administer search_api'),
-    'file' => 'search_api_autocomplete.admin.inc',
-  );
-
-  return $items;
-}
-
-/**
  * Implements hook_theme().
  */
 function search_api_autocomplete_theme() {
-  $themes['search_api_autocomplete_suggestion'] = array(
-    'variables' => array(
+  $themes['search_api_autocomplete_suggestion'] = [
+    'variables' => [
       'keys' => NULL,
       'prefix' => NULL,
       'suggestion_prefix' => '',
       'user_input' => '',
       'suggestion_suffix' => '',
       'results' => NULL,
-    ),
-    'file' => 'search_api_autocomplete.pages.inc',
-  );
+    ],
+  ];
 
   return $themes;
 }
 
 /**
- * Implements hook_entity_info().
- */
-function search_api_autocomplete_entity_info() {
-  $info['search_api_autocomplete_search'] = array(
-    'label' => t('Autocomplete search'),
-    'controller class' => 'EntityAPIControllerExportable',
-    'entity class' => 'SearchApiAutocompleteSearch',
-    'base table' => 'search_api_autocomplete_search',
-    'uri callback' => 'search_api_autocomplete_search_url',
-    'module' => 'search_api_autocomplete',
-    'exportable' => TRUE,
-    'entity keys' => array(
-      'id' => 'id',
-      'name' => 'machine_name',
-      'label' => 'name',
-    ),
-  );
-
-  return $info;
-}
-
-/**
- * Implements hook_permission().
- */
-function search_api_autocomplete_permission() {
-  $perms = array();
-  foreach (search_api_autocomplete_search_load_multiple() as $id => $search) {
-    $perms['use search_api_autocomplete for ' . $id] = array(
-      'title' => t('Use autocomplete for the %search search', array('%search' => $search->name)),
-    );
-  }
-  return $perms;
-}
-
-/**
- * Access callback for search autocompletion.
- *
- * @param SearchApiAutocompleteSearch $search
- *   The autocomplete search which is being accessed.
- * @param object|null $account
- *   (optional) The account to check, if not given use currently logged in user.
- *
- * @return bool
- *   TRUE, if the search is enabled, supports autocompletion and the user has
- *   the necessary permission. FALSE otherwise.
- */
-function search_api_autocomplete_access(SearchApiAutocompleteSearch $search, $account = NULL) {
-  return $search->enabled && user_access('use search_api_autocomplete for ' . $search->machine_name, $account) && $search->supportsAutocompletion();
-}
-
-/**
- * Implements hook_search_api_index_delete().
+ * Implements hook_entity_operation().
  */
-function search_api_autocomplete_search_api_index_delete(SearchApiIndex $index) {
-  if (!$index->hasStatus(ENTITY_IN_CODE)) {
-    $ids = db_query('SELECT id FROM {search_api_autocomplete_search} WHERE index_id = :id',
-        array(':id' => $index->machine_name))->fetchCol();
-    if ($ids) {
-      entity_delete_multiple('search_api_autocomplete_search', $ids);
-    }
+function search_api_autocomplete_entity_operation(\Drupal\Core\Entity\EntityInterface $entity) {
+  if ($entity->getEntityTypeId() != 'search_api_index') {
+    return [];
   }
-}
 
-/**
- * Retrieves information about all search types, or a specific one.
- *
- * @param $type
- *   (optional) The name of a type.
- *
- * @return array|null
- *   If $type was not given, an array containing information about all search
- *   types. Otherwise, either information about the specified type, or NULL if
- *   the type is not known.
- *
- * @see hook_search_api_autocomplete_types()
- */
-function search_api_autocomplete_get_types($type = NULL) {
-  $types = &drupal_static(__FUNCTION__);
-  if (!isset($types)) {
-    $types = module_invoke_all('search_api_autocomplete_types');
-  }
-  if (isset($type)) {
-    return isset($types[$type]) ? $types[$type] : NULL;
-  }
-  return $types;
-}
-
-/**
- * Retrieves the definitions of all suggester plugins, or a specific one.
- *
- * @param string|null $suggester_id
- *   (optional) The ID of the suggester plugin whose definition should be
- *   retrieved. Or NULL to return all known definitions.
- *
- * @return array|null
- *   If $suggester_id was given, either the definition of the given suggester
- *   plugin, or NULL if it isn't known. Otherwise, an associative array of all
- *   known suggester plugin definitions, keyed by ID.
- */
-function search_api_autocomplete_suggester_info($suggester_id = NULL) {
-  $suggesters = &drupal_static(__FUNCTION__);
-
-  if (!isset($suggesters)) {
-    $suggesters = module_invoke_all('search_api_autocomplete_suggester_info');
-    drupal_alter('search_api_autocomplete_suggester_info', $suggesters);
-    foreach ($suggesters as $i => $definition) {
-      if (empty($definition['class']) || !class_exists($definition['class']) || !in_array('SearchApiAutocompleteSuggesterInterface', class_implements($definition['class']))) {
-        $variables['@suggester_id'] = $i;
-        watchdog('search_api_autocomplete', 'Autocomplete suggester plugin with ID @suggester_id does not specify a valid plugin class.', $variables, WATCHDOG_ERROR);
-        unset($suggesters[$i]);
-      }
-    }
-  }
-
-  if (!isset($suggester_id)) {
-    return $suggesters;
-  }
-  return isset($suggesters[$suggester_id]) ? $suggesters[$suggester_id] : NULL;
-}
-
-/**
- * Retrieves all suggester plugin definitions that support the given index.
- *
- * @param SearchApiIndex $index
- *   The index for which to check.
- *
- * @return array|null
- *   If $suggester_id was given, either the definition of the given suggester
- *   plugin, or NULL if it isn't known. Otherwise, an associative array of all
- *   known suggester plugin definitions, keyed by ID.
- */
-function search_api_autocomplete_suggesters_for_index(SearchApiIndex $index) {
-  $suggesters = search_api_autocomplete_suggester_info();
-
-  foreach ($suggesters as $suggester_id => $definition) {
-    if (!call_user_func(array($definition['class'], 'supportsIndex'), $index)) {
-      unset($suggesters[$suggester_id]);
-    }
-  }
-
-  return $suggesters;
-}
-
-/**
- * Loads the specified suggester plugin.
- *
- * @param string $suggester_id
- *   The ID of the suggester plugin to load.
- * @param SearchApiAutocompleteSearch $search
- *   The search for which to create a suggester.
- * @param array $configuration
- *   The configuration for the search.
- *
- * @return SearchApiAutocompleteSuggesterInterface|null
- *   The loaded suggester plugin, or NULL if it could not be loaded.
- */
-function search_api_autocomplete_suggester_load($suggester_id, SearchApiAutocompleteSearch $search, array $configuration) {
-  $definition = search_api_autocomplete_suggester_info($suggester_id);
-  if (!$definition) {
-    return NULL;
-  }
-  return call_user_func(array($definition['class'], 'create'), $search, $configuration, $suggester_id, $definition);
-}
-
-/**
- * Loads an autocomplete search entity.
- *
- * @param int|string $id
- *   Either the ID or machine name of an autocomplete search.
- * @param bool $reset
- *   Whether to reset the internal cache.
- *
- * @return SearchApiAutocompleteSearch
- *   The specified autocomplete search entity, or FALSE if it doesn't exist.
- */
-function search_api_autocomplete_search_load($id, $reset = FALSE) {
-  $ret = search_api_autocomplete_search_load_multiple(array($id), array(), $reset);
-  return $ret ? reset($ret) : FALSE;
-}
-
-/**
- * Loads autocomplete search entities.
- *
- * @param array|false $ids
- *   (optional) An array of IDs or machine names, or FALSE to load all searches.
- * @param array $conditions
- *   (optional) An associative array of conditions on the
- *   {search_api_autocomplete_search} table.
- * @param bool $reset
- *   (optional) Whether to reset the internal cache.
- *
- * @return SearchApiAutocompleteSearch[]
- *   An array of all autocomplete search entities that meet the criteria.
- *
- * @see entity_load()
- */
-function search_api_autocomplete_search_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
-  $searches = entity_load('search_api_autocomplete_search', $ids, $conditions, $reset);
-  return entity_key_array_by_property($searches, 'machine_name');
+  $operations = [];
+  $operations['autocomplete'] = [
+    'title' => t('Autocomplete'),
+    'weight' => 30,
+    'url' => Url::fromRoute('search_api_autocomplete.admin_overview', ['search_api_index' => $entity->id()]),
+  ];
+  return $operations;
 }
diff --git a/search_api_autocomplete.pages.inc b/search_api_autocomplete.pages.inc
deleted file mode 100644
index 3cb2821..0000000
--- a/search_api_autocomplete.pages.inc
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains page callbacks and theme functions for the frontend UI.
- */
-
-/**
- * Page callback for getting autocomplete suggestions.
- *
- * @param SearchApiAutocompleteSearch $search
- *   The search for which to retrieve autocomplete suggestions.
- * @param string $fields
- *   A comma-separated list of fields on which to do autocompletion. Or "-" to
- *   use all fulltext fields.
- * @param string $keys
- *   The user input so far.
- */
-function search_api_autocomplete_autocomplete(SearchApiAutocompleteSearch $search, $fields, $keys = '') {
-  $ret = array();
-  try {
-    if ($search->supportsAutocompletion()) {
-      list($complete, $incomplete) = $search->splitKeys($keys);
-      $query = $search->getQuery($complete, $incomplete);
-      if ($query) {
-        // @todo Maybe make range configurable?
-        $query->range(0, 10);
-        $query->setOption('search id', 'search_api_autocomplete:' . $search->machine_name);
-        if (!empty($search->options['fields'])) {
-          $query->fields($search->options['fields']);
-        }
-        elseif ($fields != '-') {
-          $fields = explode(' ', $fields);
-          $query->fields($fields);
-        }
-        $query->preExecute();
-        $suggestions = $search->getSuggester()->getAutocompleteSuggestions($query, $incomplete, $keys);
-        if ($suggestions) {
-          foreach ($suggestions as $suggestion) {
-            // Convert suggestion strings into an array.
-            if (is_string($suggestion)) {
-              $pos = strpos($suggestion, $keys);
-              if ($pos === FALSE) {
-                $suggestion = array(
-                  'user_input' => '',
-                  'suggestion_suffix' => $suggestion,
-                );
-              }
-              else {
-                $suggestion = array(
-                  'suggestion_prefix' => substr($suggestion, 0, $pos),
-                  'user_input' => $keys,
-                  'suggestion_suffix' => substr($suggestion, $pos + strlen($keys)),
-                );
-              }
-            }
-            // Add defaults.
-            $suggestion += array(
-              'url' => NULL,
-              'keys' => NULL,
-              'prefix' => NULL,
-              'suggestion_prefix' => '',
-              'user_input' => $keys,
-              'suggestion_suffix' => '',
-              'results' => NULL,
-            );
-            if (empty($search->options['results'])) {
-              unset($suggestion['results']);
-            }
-
-            // Decide what the action of the suggestion is – entering specific
-            // search terms or redirecting to a URL.
-            if (isset($suggestion['url'])) {
-              $key = ' ' . $suggestion['url'];
-            }
-            else {
-              // Also set the "keys" key so it will always be available in alter
-              // hooks and the theme function.
-              if (!isset($suggestion['keys'])) {
-                $suggestion['keys'] = $suggestion['suggestion_prefix'] . $suggestion['user_input'] . $suggestion['suggestion_suffix'];
-              }
-              $key = trim($suggestion['keys']);
-            }
-
-            if (!isset($ret[$key])) {
-              $ret[$key] = $suggestion;
-            }
-          }
-
-          $alter_params = array(
-            'query' => $query,
-            'search' => $search,
-            'incomplete_key' => $incomplete,
-            'user_input' => $keys,
-          );
-          drupal_alter('search_api_autocomplete_suggestions', $ret, $alter_params);
-
-          foreach ($ret as $key => $suggestion) {
-            if (isset($suggestion['render'])) {
-              $ret[$key] = render($suggestion['render']);
-            }
-            else {
-              $escaped_variables = array('keys', 'suggestion_prefix', 'user_input', 'suggestion_suffix');
-              foreach ($escaped_variables as $variable) {
-                if ($suggestion[$variable]) {
-                  $suggestion[$variable] = check_plain($suggestion[$variable]);
-                }
-              }
-              $ret[$key] = theme('search_api_autocomplete_suggestion', $suggestion);
-            }
-          }
-        }
-      }
-    }
-  }
-  catch (SearchApiException $e) {
-    watchdog_exception('search_api_autocomplete', $e, '%type while retrieving autocomplete suggestions: !message in %function (line %line of %file).');
-  }
-
-  drupal_json_output($ret);
-}
-
-/**
- * Returns HTML for an autocomplete suggestion.
- *
- * @param array $variables
- *   An associative array containing:
- *   - keys: The keyword (or keywords) this suggestion will autocomplete to.
- *   - prefix: For special suggestions, some kind of HTML prefix describing
- *     them.
- *   - suggestion_prefix: A suggested prefix for the entered input.
- *   - user_input: The input entered by the user.
- *   - suggestion_suffix: A suggested suffix for the entered input.
- *   - results: If available, the estimated number of results for these keys.
- */
-function theme_search_api_autocomplete_suggestion(array $variables) {
-  $prefix = $variables['prefix'];
-  $suggestion_prefix = $variables['suggestion_prefix'];
-  $user_input = $variables['user_input'];
-  $suggestion_suffix = $variables['suggestion_suffix'];
-  $results = $variables['results'];
-
-  $output = '';
-  if ($prefix) {
-    $output .= "<span class=\"autocomplete-suggestion-note\">$prefix</span> ";
-  }
-  if ($suggestion_prefix) {
-    $output .= "<span class=\"autocomplete-suggestion-prefix\">$suggestion_prefix</span>";
-  }
-  if ($user_input) {
-    $output .= "<span class=\"autocomplete-user-input\">$user_input</span>";
-  }
-  if ($suggestion_suffix) {
-    $output .= "<span class=\"autocomplete-suggestion-suffix\">$suggestion_suffix</span>";
-  }
-  if ($results) {
-    $output .= " <span class=\"autocomplete-suggestion-results\">$results</span>";
-  }
-
-  return "<div class=\"search-api-autocomplete-suggestion\">$output</div>";
-}
diff --git a/search_api_autocomplete.permission.yml b/search_api_autocomplete.permission.yml
new file mode 100644
index 0000000..db39676
--- /dev/null
+++ b/search_api_autocomplete.permission.yml
@@ -0,0 +1,2 @@
+permission_callbacks:
+  - Drupal\search_api_autocomplete\Permissions::bySearch
diff --git a/search_api_autocomplete.routing.yml b/search_api_autocomplete.routing.yml
new file mode 100644
index 0000000..7656e00
--- /dev/null
+++ b/search_api_autocomplete.routing.yml
@@ -0,0 +1,15 @@
+search_api_autocomplete.autocomplete:
+  path: /search_api_autocomplete/{search_api_autocomplete_settings}/{fields}
+  defaults:
+    _controller: \Drupal\search_api_autocomplete\Controller\AutocompleteController::autocomplete
+    fields: ''
+  requirements:
+    _custom_access: \Drupal\search_api_autocomplete\Controller\AutocompleteController::access
+
+search_api_autocomplete.admin_overview:
+  path: /admin/config/search/search-api/index/{search_api_index}/autocomplete
+  defaults:
+    _form: \Drupal\search_api_autocomplete\Form\AutocompleteSearchAdminOverview
+    title: 'Edit the autocompletion settings of a search.'
+  requirements:
+    _entity_access: search_api_index.update
diff --git a/search_api_autocomplete.search_api_page.inc b/search_api_autocomplete.search_api_page.inc
index 87fcce2..f19027a 100644
--- a/search_api_autocomplete.search_api_page.inc
+++ b/search_api_autocomplete.search_api_page.inc
@@ -5,71 +5,19 @@
  * Contains code for integrating with the "Search pages" module.
  */
 
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+
 /**
  * Implements hook_form_FORM_ID_alter().
  *
  * Adds autocompletion to the keywords field on search pages, if enabled by the
  * user.
  */
-function search_api_autocomplete_form_search_api_page_search_form_alter(array &$form, array &$form_state) {
-  if (isset($form['form'])) {
-    $form = &$form['form'];
-  }
-  $id = 'search_api_page_' . $form_state['build_info']['args'][0]->machine_name;
-  $search = search_api_autocomplete_search_load($id);
-  if ($search && $search->enabled) {
-    $search->alterElement($form['keys_' . $form['id']['#value']]);
-  }
-}
-
-/**
- * Returns a list of search pages for the given index.
- *
- * @param SearchApiIndex $index
- *   The index whose searches should be returned.
- *
- * @return array
- *   An array of searches, keyed by their machine name. The values are arrays
- *   with the following keys:
- *   - name: A human-readable name for this search.
- *   - options: (optional) An array of options to use for this search.
- *     Type-specific options should go into the "custom" nested key in these
- *     options.
- */
-function search_api_autocomplete_pages_searches(SearchApiIndex $index) {
-  $ret = array();
-  foreach (search_api_page_load_multiple(FALSE, array('index_id' => $index->machine_name)) as $page) {
-    $id = 'search_api_page_' . $page->machine_name;
-    $ret[$id]['name'] = $page->name;
-    $ret[$id]['options']['custom']['page_id'] = $page->machine_name;
-  }
-  return $ret;
-}
-
-/**
- * Create the query that would be issued for the given search for the complete keys.
- *
- * @param SearchApiAutocompleteSearch $search
- *   The search for which to create the query.
- * @param $complete
- *   A string containing the complete search keys.
- * @param $incomplete
- *   A string containing the incomplete last search key.
- *
- * @return SearchApiQueryInterface
- *   The query that would normally be executed when only $complete was entered
- *   as the search keys for the given search.
- *
- * @throws SearchApiException
- *   If the query couldn't be created.
- */
-function search_api_autocomplete_pages_query(SearchApiAutocompleteSearch $search, $complete, $incomplete) {
-  $page = search_api_page_load($search->options['custom']['page_id']);
-  // Copied from search_api_page_search_execute().
-  $query = search_api_query($page->index_id, array('parse mode' => $page->options['mode']))
-    ->keys($complete);
-  if (!empty($page->options['fields'])) {
-    $query->fields($page->options['fields']);
+function search_api_autocomplete_form_search_api_page_block_form_alter(array &$form, FormStateInterface $form_state) {
+  $id = 'search_api_page_' . $form_state->getBuildInfo()['args'][0]['search_api_page'];
+  $search = SearchApiAutocompleteSearch::load($id);
+  if ($search && $search->status()) {
+    $search->alterElement($form['keys']);
   }
-  return $query;
 }
diff --git a/search_api_autocomplete.search_api_views.inc b/search_api_autocomplete.search_api_views.inc
index aa4d806..4fd9149 100644
--- a/search_api_autocomplete.search_api_views.inc
+++ b/search_api_autocomplete.search_api_views.inc
@@ -5,41 +5,51 @@
  * Contains code for integrating with the "Search views" module.
  */
 
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api_autocomplete\Controller\AutocompleteController;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+use Drupal\views\Views;
+
 /**
  * Implements hook_form_FORM_ID_alter().
  *
  * Adds autocompletion to input fields for fulltext keywords on views with
  * exposed filters.
  */
-function search_api_autocomplete_form_views_exposed_form_alter(array &$form, array &$form_state) {
-  $view = $form_state['view'];
-  if (substr($view->base_table, 0, 17) != 'search_api_index_') {
+function search_api_autocomplete_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state) {
+  /** @var \Drupal\views\ViewExecutable $view */
+  $view = $form_state->get('view');
+  if (substr($view->storage->get('base_table'), 0, 17) != 'search_api_index_') {
     return;
   }
-  $search_id = 'search_api_views_' . $view->name;
-  $search = search_api_autocomplete_search_load($search_id);
-  if (!$search || !search_api_autocomplete_access($search)) {
+  $search_id = 'search_api_views_' . $view->id();
+  $search = SearchApiAutocompleteSearch::load($search_id);
+  $controller = new AutocompleteController();
+  if (!$search || !$controller->access($search, \Drupal::currentUser())->isAllowed()) {
     return;
   }
-  $index_id = substr($view->base_table, 17);
-  $index = search_api_index_load($index_id);
-  if (empty($index->options['fields'])) {
+  $index_id = substr($view->storage->get('base_table'), 17);
+  
+  $index = Index::load($index_id);
+  if (!$fields = $index->getFulltextFields()) {
     return;
   }
-  $fields = $index->getFulltextFields(TRUE);
   // Add the "Search: Fulltext search" filter as another text field.
-  $fields[] = 'search_api_views_fulltext';
-  drupal_alter('search_api_autocomplete_views_fulltext_fields', $fields, $search, $view);
+  $fields[] = 'search_api_fulltext';
+  \Drupal::moduleHandler()->alter('search_api_autocomplete_views_fulltext_fields', $fields, $search, $view);
   foreach ($view->filter as $filter_name => $filter) {
-    if (in_array($filter->real_field, $fields)) {
+    if (in_array($filter->realField, $fields)) {
       if (!empty($filter->options['expose']['identifier'])) {
         $key = $filter->options['expose']['identifier'];
         if (isset($form[$key]) && $form[$key]['#type'] == 'textfield') {
-          if ($filter->real_field == 'search_api_views_fulltext') {
+          if ($filter->realField == 'search_api_fulltext') {
             $autocomplete_fields = $filter->options['fields'];
           }
           else {
-            $autocomplete_fields = array($filter->real_field);
+            $autocomplete_fields = [$filter->realField];
           }
           $search->alterElement($form[$key], $autocomplete_fields);
         }
@@ -47,113 +57,3 @@ function search_api_autocomplete_form_views_exposed_form_alter(array &$form, arr
     }
   }
 }
-
-/**
- * Returns a list of search views for the given index.
- *
- * @param SearchApiIndex $index
- *   The index whose searches should be returned.
- *
- * @return array
- *   An array of searches, keyed by their machine name. The values are arrays
- *   with the following keys:
- *   - name: A human-readable name for this search.
- *   - options: (optional) An array of options to use for this search.
- *     Type-specific options should go into the "custom" nested key in these
- *     options.
- */
-function search_api_autocomplete_views_searches(SearchApiIndex $index) {
-  $ret = array();
-  $base_table = 'search_api_index_' . $index->machine_name;
-  foreach (views_get_all_views() as $name => $view) {
-    if ($view->base_table === $base_table) {
-      // @todo Check whether there is an exposed fulltext filter
-      $ret['search_api_views_' . $name] = array(
-        'name' => !empty($view->human_name) ? $view->human_name : $name,
-      );
-    }
-  }
-  return $ret;
-}
-
-/**
- * Create the query that would be issued for the given search for the complete keys.
- *
- * @param SearchApiAutocompleteSearch $search
- *   The search for which to create the query.
- * @param $complete
- *   A string containing the complete search keys.
- * @param $incomplete
- *   A string containing the incomplete last search key.
- *
- * @return SearchApiQueryInterface
- *   The query that would normally be executed when only $complete was entered
- *   as the search keys for the given search.
- *
- * @throws SearchApiException
- *   If the query couldn't be created.
- */
-function search_api_autocomplete_views_query(SearchApiAutocompleteSearch $search, $complete, $incomplete) {
-  $views_id = substr($search->machine_name, 17);
-  $view = views_get_view($views_id);
-  if (!$view) {
-    $vars['@view'] = $views_id;
-    throw new SearchApiException(t('Could not load view @view.', $vars));
-  }
-  $view->set_display(isset($search->options['custom']['display']) ? $search->options['custom']['display'] : NULL);
-  $view->pre_execute();
-  $view->build();
-  $query = $view->query->getSearchApiQuery();
-  if (!$query) {
-    $vars['@view'] = !empty($view->human_name) ? $view->human_name : $views_id;
-    throw new SearchApiException(t('Could not create query for view @view.', $vars));
-  }
-  $query->keys($complete);
-  return $query;
-}
-
-/**
- * Form callback for a Views-specific autocomplete configuration form.
- *
- * @param SearchApiAutocompleteSearch $search
- *   The search being configured.
- *
- * @see example_autocomplete_config_form()
- * @see search_api_autocomplete_views_config_form_submit()
- */
-function search_api_autocomplete_views_config_form(array $form, array &$form_state, SearchApiAutocompleteSearch $search) {
-  $views_id = substr($search->machine_name, 17);
-  $view = views_get_view($views_id);
-  $options = array();
-  foreach ($view->display as $id => $display) {
-    $options[$id] = $display->display_title;
-  }
-  $form['display'] = array(
-    '#type' => 'select',
-    '#title' => t('Views display'),
-    '#description' => t('Please select the Views display whose settings should be used for autocomplete queries.<br />' .
-        "<strong>Note:</strong> Autocompletion doesn't work well with contextual filters. Please see the <a href='@readme_url'>README.txt</a> file for details.",
-        array('@readme_url' => url(drupal_get_path('module', 'search_api_autocomplete') . '/README.txt'))),
-    '#options' => $options,
-    '#default_value' => isset($search->options['custom']['display']) ? $search->options['custom']['display'] : 'default',
-  );
-
-  return $form;
-}
-
-/**
- * Submit callback for search_api_autocomplete_views_config_form().
- *
- * @see example_autocomplete_config_form_submit()
- * @see search_api_autocomplete_views_config_form()
- */
-function search_api_autocomplete_views_config_form_submit(array $form, array &$form_state, array &$values) {
-  $views_id = substr($form_state['search']->machine_name, 17);
-  $view = views_get_view($views_id);
-  $view->set_display($values['display']);
-  $view->pre_execute();
-  if ($view->argument) {
-    drupal_set_message(t('You have selected a display with contextual filters. This can lead to various problems. Please see the <a href="@readme_url">README.txt</a> file for details.',
-        array('@readme_url' => url(drupal_get_path('module', 'search_api_autocomplete') . '/README.txt'))), 'warning');
-  }
-}
diff --git a/search_api_autocomplete.services.yml b/search_api_autocomplete.services.yml
new file mode 100644
index 0000000..9af9fc9
--- /dev/null
+++ b/search_api_autocomplete.services.yml
@@ -0,0 +1,7 @@
+services:
+  plugin_manager.search_api_autocomplete_type:
+    class: \Drupal\search_api_autocomplete\Plugin\AutocompleteTypeManager
+    parent: default_plugin_manager
+  plugin_manager.search_api_autocomplete_suggester:
+    class: \Drupal\search_api_autocomplete\Plugin\AutocompleteSuggesterManager
+    parent: default_plugin_manager
diff --git a/src/Annotation/AutocompleteSuggester.php b/src/Annotation/AutocompleteSuggester.php
new file mode 100644
index 0000000..dbcc219
--- /dev/null
+++ b/src/Annotation/AutocompleteSuggester.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * @Annotation
+ */
+class AutocompleteSuggester extends Plugin {
+
+  public $label;
+
+  public $description;
+
+}
diff --git a/src/Annotation/AutocompleteType.php b/src/Annotation/AutocompleteType.php
new file mode 100644
index 0000000..78fa32e
--- /dev/null
+++ b/src/Annotation/AutocompleteType.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * @Annotation
+ */
+class AutocompleteType extends Plugin {
+
+  public $label;
+
+  public $description;
+
+}
diff --git a/src/SearchApiAutocompleteSuggesterInterface.php b/src/AutocompleteSuggesterInterface.php
similarity index 55%
rename from src/SearchApiAutocompleteSuggesterInterface.php
rename to src/AutocompleteSuggesterInterface.php
index 8552aa9..c468fba 100644
--- a/src/SearchApiAutocompleteSuggesterInterface.php
+++ b/src/AutocompleteSuggesterInterface.php
@@ -1,45 +1,26 @@
 <?php
 
-/**
- * @file
- * Contains SearchApiAutocompleteSuggesterInterface.
- */
+namespace Drupal\search_api_autocomplete;
+
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
 
 /**
  * Represents a plugin for creating autocomplete suggestions.
- *
- * @see SearchApiAutocompleteSuggesterPluginBase
- * @see hook_search_api_autocomplete_suggester_info()
  */
-interface SearchApiAutocompleteSuggesterInterface {
-
-  /**
-   * Creates a new instance of this class.
-   *
-   * @param SearchApiAutocompleteSearch $search
-   *   The search to which this suggester is attached.
-   * @param array $configuration
-   *   An associative array containing the suggester's configuration, if any.
-   * @param string $plugin_id
-   *   The suggester's plugin ID.
-   * @param array $plugin_definition
-   *   The suggester plugin's definition.
-   *
-   * @return static
-   *   A new instance of this class.
-   */
-  public static function create(SearchApiAutocompleteSearch $search, array $configuration, $plugin_id, array $plugin_definition);
+interface AutocompleteSuggesterInterface {
 
   /**
    * Determines whether this plugin class supports the given index.
    *
-   * @param SearchApiIndex $index
+   * @param \Drupal\search_api\IndexInterface $index
    *   The search index in question.
    *
    * @return bool
    *   TRUE if this plugin supports the given search index, FALSE otherwise.
    */
-  public static function supportsIndex(SearchApiIndex $index);
+  public static function supportsIndex(IndexInterface $index);
 
   /**
    * Retrieves the plugin's ID.
@@ -60,7 +41,7 @@ interface SearchApiAutocompleteSuggesterInterface {
   /**
    * Retrieves the search this plugin is configured for.
    *
-   * @return SearchApiAutocompleteSearch
+   * @return \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch
    *   The search this plugin is configured for.
    */
   public function getSearch();
@@ -82,69 +63,6 @@ interface SearchApiAutocompleteSuggesterInterface {
   public function getDescription();
 
   /**
-   * Retrieves the plugin's configuration.
-   *
-   * @return array
-   *   An associative array containing the plugin's configuration.
-   */
-  public function getConfiguration();
-
-  /**
-   * Sets the plugin's configuration.
-   *
-   * @param array $configuration
-   *   An associative array containing the plugin's configuration.
-   *
-   * @return $this
-   */
-  public function setConfiguration(array $configuration);
-
-  /**
-   * Retrieves the default configuration for this plugin.
-   *
-   * @return array
-   *   An associative array containing the plugin's default configuration.
-   */
-  public function defaultConfiguration();
-
-  /**
-   * Constructs the plugin's configuration form.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param array $form_state
-   *   The current state of the form.
-   *
-   * @return array
-   *   An associative array containing the structure of the form. An empty array
-   *   if the plugin has no configuration form.
-   */
-  public function buildConfigurationForm(array $form, array &$form_state);
-
-  /**
-   * Validates the plugin's configuration form.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param array $form_state
-   *   The current state of the form.
-   */
-  public function validateConfigurationForm(array $form, array &$form_state);
-
-  /**
-   * Submits the plugin's configuration form.
-   *
-   * Should take care of calling setConfiguration() with the new configuration
-   * values as appropriate.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param array $form_state
-   *   The current state of the form.
-   */
-  public function submitConfigurationForm(array $form, array &$form_state);
-
-  /**
    * Retrieves autocompletion suggestions for some user input.
    *
    * For example, when given the user input "teach us", with "us" being
@@ -168,7 +86,7 @@ interface SearchApiAutocompleteSuggesterInterface {
    *   );
    * @endcode
    *
-   * @param SearchApiQueryInterface $query
+   * @param \Drupal\search_api\Query\QueryInterface $query
    *   A query representing the completed user input so far.
    * @param string $incomplete_key
    *   The start of another fulltext keyword for the search, which should be
@@ -202,6 +120,6 @@ interface SearchApiAutocompleteSuggesterInterface {
    *   "keys", "url", "suggestion_prefix", "user_input" or "suggestion_suffix"
    *   has to be present.
    */
-  public function getAutocompleteSuggestions(SearchApiQueryInterface $query, $incomplete_key, $user_input);
+  public function getAutocompleteSuggestions(QueryInterface $query, $incomplete_key, $user_input);
 
 }
diff --git a/src/AutocompleteTypeInterface.php b/src/AutocompleteTypeInterface.php
new file mode 100644
index 0000000..c01c655
--- /dev/null
+++ b/src/AutocompleteTypeInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\search_api_autocomplete;
+
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+
+interface AutocompleteTypeInterface {
+
+  public function getLabel();
+
+  public function getDescription();
+
+  public function listSearches(IndexInterface $index);
+
+  /**
+   * @return \Drupal\search_api\Query\QueryInterface
+   */
+  public function createQuery(SearchApiAutocompleteSearch $search, $complete, $incomplete);
+
+}
diff --git a/src/Controller/AutocompleteController.php b/src/Controller/AutocompleteController.php
new file mode 100644
index 0000000..898b8a0
--- /dev/null
+++ b/src/Controller/AutocompleteController.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Controller;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableJsonResponse;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+
+class AutocompleteController {
+
+  /**
+   * Page callback for getting autocomplete suggestions.
+   *
+   * @param \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search_api_autocomplete_settings
+   *   The search for which to retrieve autocomplete suggestions.
+   * @param string $fields
+   *   A comma-separated list of fields on which to do autocompletion. Or "-"
+   *   to use all fulltext fields.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Drupal\Core\Cache\CacheableJsonResponse
+   *   The autocompletion response.
+   *
+   */
+  public function autocomplete(SearchApiAutocompleteSearch $search_api_autocomplete_settings, $fields, Request $request) {
+    $matches = [];
+    try {
+      if ($search_api_autocomplete_settings->supportsAutocompletion()) {
+        $keys = $request->query->get('q');
+        list($complete, $incomplete) = $search_api_autocomplete_settings->splitKeys($keys);
+        $query = $search_api_autocomplete_settings->getQuery($complete, $incomplete);
+        if ($query) {
+          // @todo Maybe make range configurable?
+          $query->range(0, 10);
+          $query->setOption('search id', 'search_api_autocomplete:' . $search_api_autocomplete_settings->id());
+          if (!empty($search_api_autocomplete_settings->getOption('fields'))) {
+            $query->setFulltextFields($search_api_autocomplete_settings->getOption('fields'));
+          }
+          elseif ($fields != '-') {
+            $fields = explode(' ', $fields);
+            $query->setFulltextFields($fields);
+          }
+          $query->preExecute();
+          $suggestions = $search_api_autocomplete_settings->getSuggester()->getAutocompleteSuggestions($query, $incomplete, $keys);
+          if ($suggestions) {
+            foreach ($suggestions as $suggestion) {
+              // Convert suggestion strings into an array.
+              if (is_string($suggestion)) {
+                $pos = strpos($suggestion, $keys);
+                if ($pos === FALSE) {
+                  $suggestion = [
+                    'user_input' => '',
+                    'suggestion_suffix' => $suggestion,
+                  ];
+                }
+                else {
+                  $suggestion = [
+                    'suggestion_prefix' => substr($suggestion, 0, $pos),
+                    'user_input' => $keys,
+                    'suggestion_suffix' => substr($suggestion, $pos + strlen($keys)),
+                  ];
+                }
+              }
+              // Add defaults.
+              $suggestion += [
+                'url' => NULL,
+                'keys' => NULL,
+                'prefix' => NULL,
+                'suggestion_prefix' => '',
+                'user_input' => $keys,
+                'suggestion_suffix' => '',
+                'results' => NULL,
+              ];
+              if (empty($search_api_autocomplete_settings->getOption('results'))) {
+                unset($suggestion['results']);
+              }
+
+              // Decide what the action of the suggestion is – entering specific
+              // search terms or redirecting to a URL.
+              if (isset($suggestion['url'])) {
+                $key = ' ' . $suggestion['url'];
+              }
+              else {
+                // Also set the "keys" key so it will always be available in alter
+                // hooks and the theme function.
+                if (!isset($suggestion['keys'])) {
+                  $suggestion['keys'] = $suggestion['suggestion_prefix'] . $suggestion['user_input'] . $suggestion['suggestion_suffix'];
+                }
+                $key = trim($suggestion['keys']);
+              }
+
+              if (!isset($ret[$key])) {
+                $ret[$key] = $suggestion;
+              }
+            }
+
+            $alter_params = [
+              'query' => $query,
+              'search' => $search_api_autocomplete_settings,
+              'incomplete_key' => $incomplete,
+              'user_input' => $keys,
+            ];
+            \Drupal::moduleHandler()->alter('search_api_autocomplete_suggestions', $ret, $alter_params);
+
+            foreach ($ret as $key => $suggestion) {
+              if (isset($suggestion['render'])) {
+                $matches[] = [
+                  'value' => $key,
+                  'label' => \Drupal::service('renderer')->render($suggestion['render']),
+                ];
+              }
+              else {
+                $ret[$key] = [
+                  '#theme' => 'search_api_autocomplete_suggestion',
+                ]
+                  //  Convert the suggestion into a suitable variable for
+                  // templates, by adding # in front.
+                  + array_combine(array_map(function ($key) {
+                    return '#' . $key;
+                  }, array_keys($suggestion)), array_values($suggestion));
+                $matches[] = [
+                  'value' => $key,
+                  'label' => \Drupal::service('renderer')->render($ret[$key]),
+                ];
+              }
+            }
+          }
+        }
+      }
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api_autocomplete', $e, '%type while retrieving autocomplete suggestions: !message in %function (line %line of %file).');
+    }
+
+    return new JsonResponse($matches);
+  }
+
+  /**
+   * Checks access to the autocompletion route.
+   *
+   * @param \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search_api_autocomplete_settings
+   *   The configured autocompletion search.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The account.
+   *
+   * @return \Drupal\Core\Access\AccessResult
+   *   The access result.
+   */
+  public function access(SearchApiAutocompleteSearch $search_api_autocomplete_settings, AccountInterface $account) {
+    $access = AccessResult::allowedIf($search_api_autocomplete_settings->status())
+      ->andIf(AccessResult::allowedIfHasPermission($account, 'use search_api_autocomplete for ' . $search_api_autocomplete_settings->id()))
+      ->andIf(AccessResult::allowedIf($search_api_autocomplete_settings->supportsAutocompletion()));
+    return $access;
+  }
+
+}
diff --git a/src/Entity/SearchApiAutocompleteSearch.php b/src/Entity/SearchApiAutocompleteSearch.php
new file mode 100644
index 0000000..bfe8a20
--- /dev/null
+++ b/src/Entity/SearchApiAutocompleteSearch.php
@@ -0,0 +1,412 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Entity;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api_autocomplete\Controller\AutocompleteController;
+
+/**
+ * Describes the autocomplete settings for a certain search.
+ *
+ * @ConfigEntityType(
+ *   id = "search_api_autocomplete_settings",
+ *   label = @Translation("Autocomplete search"),
+ *   handlers = {
+ *     "form" = {
+ *       "default" = "\Drupal\search_api_autocomplete\Form\AutocompleteSearchEditForm",
+ *       "edit" = "\Drupal\search_api_autocomplete\Form\AutocompleteSearchEditForm",
+ *       "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
+ *     },
+ *     "list_builder" = "\Drupal\Core\Entity\EntityListBuilder",
+ *     "route_provider" = {
+ *       "default" = "\Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
+ *     },
+ *   },
+ *   admin_permission = "administer search_api_autocomplete",
+ *   config_prefix = "search_api_autocomplete",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   },
+ *   links = {
+ *     "edit-form" = "/admin/config/search/search-api/index/autocomplete/{search_api_autocomplete_settings}/edit",
+ *     "delete-form" = "/admin/config/search/search-api/index/autocomplete/{search_api_autocomplete_settings}/delete",
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "label",
+ *     "index_id",
+ *     "suggester_id",
+ *     "status",
+ *     "type",
+ *     "options",
+ *   }
+ * )
+ */
+class SearchApiAutocompleteSearch extends ConfigEntityBase {
+
+  // Entity properties, loaded from the database:
+
+  /**
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * @var integer
+   */
+  protected $index_id;
+
+  /**
+   * @var string
+   */
+  protected $suggester_id;
+
+  /**
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * An array of options for this search, containing any of the following:
+   * - results: Boolean indicating whether to also list the estimated number of
+   *   results for each suggestion (if possible).
+   * - fields: Array containing the fulltext fields to use for autocompletion.
+   * - custom: An array of type-specific settings.
+   *
+   * @var array
+   */
+  protected $options = array();
+
+  // Inferred properties, for caching:
+
+  /**
+   * @var \Drupal\search_api\IndexInterface
+   */
+  protected $index;
+
+  /**
+   * @var \Drupal\search_api\ServerInterface
+   */
+  protected $server;
+
+  /**
+   * The suggester plugin this search uses.
+   *
+   * @var \Drupal\search_api_autocomplete\SearchApiAutocompleteSuggesterInterface
+   */
+  protected $suggester;
+
+  /**
+   * @return \Drupal\search_api\IndexInterface
+   *   The index this search belongs to.
+   */
+  public function index() {
+    if (!isset($this->index)) {
+      $this->index = Index::load($this->index_id);
+      if (!$this->index) {
+        $this->index = FALSE;
+      }
+    }
+    return $this->index;
+  }
+
+  /**
+   * Retrieves the server this search would at the moment be executed on.
+   *
+   * @return \Drupal\search_api\ServerInterface
+   *   The server this search would at the moment be executed on.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   If a server is set for the index but it doesn't exist.
+   */
+  public function server() {
+    if (!isset($this->server)) {
+      if (!$this->index() || !$this->index()->getServerInstance()) {
+        $this->server = FALSE;
+      }
+      else {
+        $this->server = $this->index()->getServerInstance();
+        if (!$this->server) {
+          $this->server = FALSE;
+        }
+      }
+    }
+    return $this->server;
+  }
+
+  /**
+   * Retrieves the ID of the suggester plugin for this search.
+   *
+   * @return string
+   *   This search's suggester plugin's ID.
+   */
+  public function getSuggesterId() {
+    return $this->suggester_id;
+  }
+
+  public function setSuggesterId($suggester_id) {
+    $this->suggester_id = $suggester_id;
+    unset($this->suggester);
+    return $this;
+  }
+
+  /**
+   * Retrieves the suggester plugin for this search.
+   *
+   * @param bool $reset
+   *   (optional) If TRUE, clear the internal static cache and reload the
+   *   suggester.
+   *
+   * @return \Drupal\search_api_autocomplete\SearchApiAutocompleteSuggesterInterface|null
+   *   This search's suggester plugin, or NULL if it could not be loaded.
+   */
+  public function getSuggester($reset = FALSE) {
+    if (!isset($this->suggester) || $reset) {
+      $configuration = !empty($this->options['suggester_configuration']) ? $this->options['suggester_configuration'] : [];
+      $this->suggester = $this->getSuggesterManager()->createInstance($this->suggester_id, [
+        'search' => $this,
+      ] + $configuration);
+      if (!$this->suggester) {
+        $variables['@search'] = $this->id();
+        $variables['@index'] = $this->index() ? $this->index()->label() : $this->index_id;
+        $variables['@suggester_id'] = $this->suggester_id;
+        \Drupal::logger('search_api_autocomplete')->error('Autocomplete search @search on index @index specifies an invalid suggester plugin @suggester_id.', $variables);
+        $this->suggester = FALSE;
+      }
+    }
+    return $this->suggester ? $this->suggester : NULL;
+  }
+
+  /**
+   * @return \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected function getSuggesterManager() {
+    return \Drupal::service('plugin_manager.search_api_autocomplete_suggester');
+  }
+
+  /**
+   * Determines whether autocompletion is currently supported for this search.
+   *
+   * @return bool
+   *   TRUE if autocompletion is possible for this search with the current
+   *   settings; FALSE otherwise.
+   */
+  public function supportsAutocompletion() {
+    return $this->index() && $this->getSuggester() && $this->getSuggester()->supportsIndex($this->index());
+  }
+
+  /**
+   * Helper method for altering a textfield form element to use autocompletion.
+   *
+   * @param array $element
+   * @param array $fields
+   */
+  public function alterElement(array &$element, array $fields = []) {
+    // @todo find a cleaner solution.
+    $controller = new AutocompleteController();
+    $config = \Drupal::configFactory()->get('search_api_autocomplete.settings');
+    if ($controller->access($this, \Drupal::currentUser())->isAllowed()) {
+      // Add option defaults (in case of updates from earlier versions).
+      $options = $this->options + [
+        'submit_button_selector' => ':submit',
+        'autosubmit' => TRUE,
+        'min_length' => 1,
+        ];
+
+      $fields_string = $fields ? implode(' ', $fields) : '-';
+
+      $autocomplete_route_name = 'search_api_autocomplete.autocomplete';
+      $autocomplete_route_parameters = ['search_api_autocomplete_settings' => $this->id(), 'fields' => $fields_string];
+
+      $js_settings = [];
+      if ($options['submit_button_selector'] != ':submit') {
+        $js_settings['selector'] = $options['submit_button_selector'];
+      }
+      if ($delay = $config->get('delay') !== NULL) {
+        $js_settings['delay'] = $delay;
+      }
+
+      // Allow overriding of the default handler with a custom script.
+      // @todo implement that.
+//      $path_overrides = $config->get('scripts') ?: [];
+//      if (!empty($path_overrides[$this->id()])) {
+//        $autocomplete_path = NULL;
+//        $override = $path_overrides[$this->id()];
+//        if (is_scalar($override)) {
+//          $autocomplete_path = url($override, ['absolute' => TRUE, 'query' => ['machine_name' => $this->id()]]);
+//        }
+//        elseif (!empty($override['#callback']) && is_callable($override['#callback'])) {
+//          $autocomplete_path = call_user_func($override['#callback'], $this, $element, $override);
+//        }
+//        if (!$autocomplete_path) {
+//          return;
+//        }
+//        $js_settings['custom_path'] = TRUE;
+//      }
+
+      $element['#attached']['library'][] = 'search_api_autocomplete/search_api_autocomplete';
+      if ($js_settings) {
+        $element['#attached']['drupalSettings'][] = [
+          'search_api_autocomplete' => [
+            $this->id() => $js_settings,
+          ],
+        ];
+      }
+
+      $element['#autocomplete_route_name'] = $autocomplete_route_name;
+      $element['#autocomplete_route_parameters'] = $autocomplete_route_parameters;
+      $element += ['#attributes' => []];
+      $element['#attributes'] += ['class'=> []];
+      if ($options['autosubmit']) {
+        $element['#attributes']['class'][] = 'auto_submit';
+      }
+      $element['#attributes']['data-search-api-autocomplete-search'] = $this->id();
+      if ($options['min_length'] > 1) {
+        $element['#attributes']['data-min-autocomplete-length'] = $options['min_length'];
+      }
+    }
+  }
+
+  /**
+   * Split a string with search keywords into two parts.
+   *
+   * The first part consists of all words the user has typed completely, the
+   * second one contains the beginning of the last, possibly incomplete word.
+   *
+   * @return array
+   *   An array with $keys split into exactly two parts, both of which may be
+   *   empty.
+   */
+  public function splitKeys($keys) {
+    $keys = ltrim($keys);
+    // If there is whitespace or a quote on the right, all words have been
+    // completed.
+    if (rtrim($keys, " \"") != $keys) {
+      return array(rtrim($keys, ' '), '');
+    }
+    if (preg_match('/^(.*?)\s*"?([\S]*)$/', $keys, $m)) {
+      return array($m[1], $m[2]);
+    }
+    return array('', $keys);
+  }
+
+  /**
+   * Create the query that would be issued for this search for the complete keys.
+   *
+   * @param $complete
+   *   A string containing the complete search keys.
+   * @param $incomplete
+   *   A string containing the incomplete last search key.
+   *
+   * @return \Drupal\search_api\Query\QueryInterface
+   *   The query that would normally be executed when only $complete was entered
+   *   as the search keys for this search.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   If the query couldn't be created.
+   */
+  public function getQuery($complete, $incomplete) {
+    $type = $this->getTypeInstance();
+    $query = $type->createQuery($this, $complete, $incomplete);
+    if ($complete && !$query->getKeys()) {
+      $query->keys($complete);
+    }
+    return $query;
+  }
+
+  /**
+   * @return \Drupal\search_api_autocomplete\AutocompleteTypeInterface
+   */
+  protected function getTypeInstance() {
+    return \Drupal::service('plugin_manager.search_api_autocomplete_type')->createInstance($this->getType());
+  }
+
+  /**
+   * @param string $label
+   */
+  public function setLabel($label) {
+    $this->label = $label;
+  }
+
+  /**
+   * @return int
+   */
+  public function getIndexId() {
+    return $this->index_id;
+  }
+
+  public function getIndexInstance() {
+    if (!isset($this->index)) {
+      $this->index = Index::load($this->getIndexId());
+    }
+    return $this->index;
+  }
+
+  /**
+   * @param int $index_id
+   */
+  public function setIndexId($index_id) {
+    $this->index_id = $index_id;
+  }
+
+  /**
+   * @return string
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * @param string $type
+   */
+  public function setType($type) {
+    $this->type = $type;
+    return $this;
+  }
+
+  /**
+   * @return array
+   */
+  public function getOptions() {
+    return $this->options;
+  }
+
+  /**
+   * @param array $options
+   */
+  public function setOptions($options) {
+    $this->options = $options;
+    return $this;
+  }
+
+  /**
+   * @param $key
+   *
+   * @return mixed|null
+   */
+  public function getOption($key, $default = NULL) {
+    $parts = explode('.', $key);
+    return NestedArray::getValue($this->options, $parts) ?: $default;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    parent::calculateDependencies();
+
+    $this->index()->calculateDependencies();
+    $this->addDependencies($this->index()->getDependencies());
+    return $this;
+  }
+
+}
diff --git a/src/Form/AutocompleteSearchAdminOverview.php b/src/Form/AutocompleteSearchAdminOverview.php
new file mode 100644
index 0000000..812fbde
--- /dev/null
+++ b/src/Form/AutocompleteSearchAdminOverview.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Url;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api_autocomplete\AutocompleteSuggesterInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+
+class AutocompleteSearchAdminOverview extends FormBase {
+
+  /**
+   * @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch
+   */
+  protected $entity;
+
+  protected function getSuggestersForIndex(IndexInterface $index) {
+    /** @var \Drupal\Component\Plugin\PluginManagerInterface $manager */
+    $manager = \Drupal::service('plugin_manager.search_api_autocomplete_suggester');
+
+    $suggesters = array_map(function ($suggester_info) use ($manager) {
+      return $suggester_info['class'];
+    }, $manager->getDefinitions());
+    $suggesters = array_filter($suggesters, function ($suggester_class) use ($index) {
+      return $suggester_class::supportsIndex($index);
+    });
+    return $suggesters;
+  }
+
+  /**
+   * @param string $index_id
+   *   The index ID.
+   *
+   * @return \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch[]
+   */
+  protected function loadAutocompleteSearchByIndex($index_id) {
+    return \Drupal::entityTypeManager()->getStorage('search_api_autocomplete_settings')->loadByProperties([
+      'index_id' => $index_id,
+    ]);
+  }
+
+  public function submitDelete(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\search_api\IndexInterface $index */
+    $index = $form_state->get('index');
+    $ids = array_keys($this->loadAutocompleteSearchByIndex($index->id()));
+    if ($ids) {
+      entity_delete_multiple('search_api_autocomplete_search', $ids);
+      drupal_set_message($this->t('All autocompletion settings stored for this index were deleted.'));
+    }
+    else {
+      drupal_set_message($this->t('There were no settings to delete.'), 'warning');
+    }
+    $form_state->setRedirectUrl($index->toUrl());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, IndexInterface $search_api_index = NULL) {
+    $form_state->set('index', $search_api_index);
+    $index_id = $search_api_index->id();
+
+    $available_suggesters = $this->getSuggestersForIndex($search_api_index);
+    if (!$available_suggesters) {
+      $args = [
+        '@feature' => 'search_api_autocomplete',
+        ':service_classes_url' => 'https://www.drupal.org/node/1254698#service-classes',
+      ];
+      drupal_set_message(t('There are currently no suggester plugins installed that support this index. To solve this problem, you can either:<ul><li>move the index to a server which supports the "@feature" feature (see the <a href=":service_classes_url">available service class</a>)</li><li>or install a module providing a new suggester plugin that supports this index</li></ul>', $args), 'error');
+      if ($this->loadAutocompleteSearchByIndex($index_id)) {
+        $form['description'] = array(
+          '#type' => 'item',
+          '#title' => $this->t('Delete autocompletion settings'),
+          '#description' => $this->t("If you won't use autocompletion with this index anymore, you can delete all autocompletion settings associated with it. " .
+            "This will delete all autocompletion settings on this index. Settings on other indexes won't be influenced."),
+        );
+        $form['button'] = [
+          '#type' => 'submit',
+          '#value' => t('Delete autocompletion settings'),
+          '#submit' => [$this, 'submitDelete'],
+        ];
+      }
+      return $form;
+    }
+
+    $form['#tree'] = TRUE;
+    /** @var \Drupal\search_api_autocomplete\Plugin\AutocompleteTypeManager $type_manager */
+    $type_manager = \Drupal::service('plugin_manager.search_api_autocomplete_type');
+    $types = array_map(function ($definition) use ($type_manager) {
+      return $type_manager->createInstance($definition['id']);
+    }, $type_manager->getDefinitions());
+    $searches = $this->loadAutocompleteSearchByIndex($index_id);
+    /** @var \Drupal\search_api_autocomplete\AutocompleteTypeInterface $autocomplete_type */
+    $searches_by_type = [];
+    $unavailables_by_type = [];
+    foreach ($types as $type => $autocomplete_type) {
+      $t_searches = $autocomplete_type->listSearches($search_api_index);
+      foreach ($t_searches as $id => $search) {
+        if (isset($searches[$id])) {
+          $searches_by_type[$type][$id] = $searches[$id];
+          unset($searches[$id]);
+        }
+        else {
+          reset($available_suggesters);
+          $search += [
+            'id' => $id,
+            'label' => $id,
+            'index_id' => $index_id,
+            'suggester_id' => key($available_suggesters),
+            'type' => $type,
+            'status' => 0,
+            'options' => [],
+          ];
+          $search['options'] += [
+            'results' => TRUE,
+            'fields' => [],
+          ];
+          // @todo this is ugly!
+          $searches_by_type[$type][$id] = SearchApiAutocompleteSearch::create($search);
+        }
+      }
+    }
+    /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search */
+    foreach ($searches as $id => $search) {
+      $type = isset($types[$search->getType()]) ? $search->getType() : '';
+      $searches_by_type[$type][$id] = $search;
+      $unavailables_by_type[$type][$id] = TRUE;
+    }
+    /** @var \Drupal\search_api_autocomplete\AutocompleteTypeInterface $autocomplete_type */
+    foreach ($types as $type => $autocomplete_type) {
+
+      if (empty($searches_by_type[$type])) {
+        continue;
+      }
+      if (!$type) {
+        $info = [];
+        $info += [
+          'name' => $this->t('Unavailable search types'),
+          'description' => $this->t("The modules providing these searches were disabled or uninstalled. If you won't use them anymore, you can delete their settings."),
+        ];
+      }
+      elseif (!empty($info['unavailable'])) {
+        $info['description'] .= '</p><p>' . t("The searches marked with an asterisk (*) are currently not available, possibly because they were deleted. If you won't use them anymore, you can delete their settings.");
+      }
+      $form[$type] = [
+        '#type' => 'fieldset',
+        '#title' => $autocomplete_type->getLabel(),
+      ];
+      if ($description = $autocomplete_type->getDescription()) {
+        $form[$type]['#description'] = '<p>' . $description . '</p>';
+      }
+      $form[$type]['searches']['#type'] = 'tableselect';
+      $form[$type]['searches']['#header'] = [
+        'label' => t('label'),
+        'operations' => t('Operations'),
+      ];
+      $form[$type]['searches']['#empty'] = '';
+      $form[$type]['searches']['#js_select'] = TRUE;
+      /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search */
+      foreach ($searches_by_type[$type] as $id => $search) {
+        $form[$type]['searches'][$id] = [
+          '#type' => 'checkbox',
+          '#default_value' => $search->status(),
+          '#parents' => ['searches', $id],
+        ];
+        $unavailable = !empty($info['unavailable'][$id]);
+        if ($unavailable) {
+          $form[$type]['searches'][$id]['#default_value'] = FALSE;
+          $form[$type]['searches'][$id]['#disabled'] = TRUE;
+        }
+        $form_state->set(['searches', $id], $search);
+        $options = &$form[$type]['searches']['#options'][$id];
+        $options['label'] = $search->label();
+        if ($unavailable) {
+          $options['label'] = '* ' . $options['label'];
+        }
+        $items = array();
+        if (!$unavailable && !empty($search->status())) {
+          $items[] = [
+            'title' => t('Edit'),
+            'url' => $search->toUrl('edit-form'),
+          ];
+          $items[] = [
+            'title' => t('Delete'),
+            'url' => $search->toUrl('delete-form'),
+          ];
+        }
+            
+
+        if ($items) {
+          $options['operations'] = ['data' => [
+            '#type' => 'operations',
+            '#links' => $items,
+          ]];
+        }
+        else {
+          $options['operations'] = '';
+        }
+        unset($options);
+      }
+    }
+
+    if (!Element::children($form)) {
+      $form['message']['#markup'] = '<p>' . $this->t('There are currently no searches known for this index.') . '</p>';
+    }
+    else {
+      $form['submit'] = [
+        '#type' => 'submit',
+        '#value' => t('Save'),
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $messages = t('The settings have been saved.');
+    foreach ($form_state->getValue('searches') as $id => $enabled) {
+      /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search */
+      $search = $form_state->get(['searches', $id]);
+      if ($search->status() != $enabled) {
+        $change = TRUE;
+        if (!empty($search)) {
+          $options['query'] = \Drupal::destination()->getAsArray();
+          $options['fragment'] = 'module-search_api_autocomplete';
+          $vars[':perm_url'] = Url::fromRoute('user.admin_permissions', [], $options)->toString();
+          $messages = t('The settings have been saved. Please remember to set the <a href=":perm_url">permissions</a> for the newly enabled searches.', $vars);
+        }
+        $search->setStatus($enabled);
+        $search->save();
+      }
+    }
+    drupal_set_message(empty($change) ? t('No values were changed.') : $messages);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'search_api_autocomplete_admin_overview';
+  }
+
+}
diff --git a/src/Form/AutocompleteSearchEditForm.php b/src/Form/AutocompleteSearchEditForm.php
new file mode 100644
index 0000000..adc091a
--- /dev/null
+++ b/src/Form/AutocompleteSearchEditForm.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\search_api\Form\SubFormState;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+
+class AutocompleteSearchEditForm extends EntityForm {
+
+  /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch */
+  protected $entity;
+
+  /**
+   * @return \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected function getAutocompleteTypeManager() {
+    return \Drupal::service('plugin_manager.search_api_autocomplete_type');
+  }
+
+  /**
+   * @return \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected function getSuggesterManager() {
+    return \Drupal::service('plugin_manager.search_api_autocomplete_suggester');
+  }
+
+  protected function getSuggestersForIndex(IndexInterface $index) {
+    $suggesters = array_map(function ($suggester_info) {
+      return $suggester_info['class'];
+    }, $this->getSuggesterManager()->getDefinitions());
+    $suggesters = array_filter($suggesters, function ($suggester_class) use ($index) {
+      return $suggester_class::supportsIndex($index);
+    });
+    return $suggesters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form =  parent::buildForm($form, $form_state);
+    $search = $search_api_autocomplete_settings = $this->entity;
+    $form['#title'] = t('Configure autocompletion for %search', ['%search' => $search_api_autocomplete_settings->label()]);
+
+    // If this is a re-build (i.e., most likely an AJAX call due to a new
+    // suggester being selected), prepare the suggester sub-form state
+    // accordingly.
+    $selected_suggester_id = $search->getSuggesterId();
+    if (!empty($form_state->getValue('suggester_id'))) {
+      $selected_suggester_id = $form_state->getValue('suggester_id');
+      // Don't let submitted values for a different suggester influence another
+      // suggester's form.
+      if ($selected_suggester_id != $form_state->getValue('old_suggester_id')) {
+        $form_state->setValue(['options', 'suggester_configuration'], NULL);
+        $form_state->set(['options', 'suggester_configuration'], NULL);
+      }
+    }
+
+    /** @var \Drupal\search_api_autocomplete\AutocompleteTypeInterface $type */
+    $type = $this->getAutocompleteTypeManager()
+      ->createInstance($search->getType());
+    $form_state->set(['type'], $type);
+    if (!$type) {
+      drupal_set_message(t('No information about the type of this search was found.'), 'error');
+      return [];
+    }
+    $form['#tree'] = TRUE;
+    $form['enabled'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enabled'),
+      '#default_value' => $search->status(),
+    ];
+    $form['suggester_id'] = [
+      '#type' => 'radios',
+      '#title' => t('Suggester plugin'),
+      '#description' => t('Choose the suggester implementation to use.'),
+      '#options' => [],
+      '#required' => TRUE,
+      '#default_value' => $selected_suggester_id,
+      '#ajax' => [
+        'callback' => 'search_api_autocomplete_suggester_ajax_callback',
+        'wrapper' => 'search-api-suggester-options',
+      ],
+    ];
+
+    foreach ($this->getSuggestersForIndex($search->index()) as $suggester_id => $definition) {
+      // Load the suggester plugin. If the suggester is unchanged from the one on
+      // the saved version of the search, use the saved configuration.
+      $configuration = [];
+      if ($suggester_id == $search->getSuggesterId() && $search->getOption('suggester_configuration')) {
+        $configuration = $search->getOption('suggester_configuration');
+      }
+      $suggester = $this->getSuggesterManager()->createInstance($suggester_id, [
+        'search' => $search,
+      ] + $configuration);
+      if (!$suggester) {
+        continue;
+      }
+
+      // Add the suggester to the suggester options.
+      $form['suggester_id']['#options'][$suggester_id] = $suggester->label();
+
+      // Then, also add the configuration form for the selected suggester.
+      if ($suggester_id != $selected_suggester_id) {
+        continue;
+      }
+      $suggester_form_state = new SubFormState($form_state, ['options', 'suggester_configuration']);
+      $suggester_form = $suggester->buildConfigurationForm([], $suggester_form_state);
+      if ($suggester_form) {
+        $form['options']['suggester_configuration'] = $suggester_form;
+        $form['options']['suggester_configuration']['#type'] = 'fieldset';
+        $form['options']['suggester_configuration']['#title'] = t('Configure the %suggester suggester plugin', array('%suggester' => $suggester->label()));
+        $form['options']['suggester_configuration']['#description'] = $suggester->getDescription();
+        $form['options']['suggester_configuration']['#collapsible'] = TRUE;
+      }
+      else {
+        $form['options']['suggester_configuration']['#type'] = 'item';
+      }
+      $form['options']['suggester_configuration']['#prefix'] = '<div id="search-api-suggester-options">';
+      $form['options']['suggester_configuration']['#suffix'] = '</div>';
+    }
+    $form['options']['suggester_configuration']['old_suggester_id'] = [
+      '#type' => 'hidden',
+      '#value' => $selected_suggester_id,
+      '#tree' => FALSE,
+    ];
+
+    // If there is only a single plugin available, hide the "suggester" option.
+    if (count($form['suggester_id']['#options']) == 1) {
+      $form['suggester_id'] = [
+        '#type' => 'value',
+        '#value' => key($form['suggester_id']['#options']),
+      ];
+    }
+
+    $form['options']['min_length'] = [
+      '#type' => 'textfield',
+      '#title' => t('Minimum length of keywords for autocompletion'),
+      '#description' => t('If the entered keywords are shorter than this, no autocomplete suggestions will be displayed.'),
+      '#default_value' => $search->getOption('min_length', 1),
+      '#validate' => ['element_validate_integer_positive'],
+    ];
+    $form['options']['results'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Display result count estimates'),
+      '#description' => t('Display the estimated number of result for each suggestion. ' .
+        'This option might not have an effect for some servers or types of suggestion.'),
+      '#default_value' => (bool) $search->getOption('results', FALSE),
+    ];
+
+    $custom_form = empty($form['options']['custom']) ? [] : $form['options']['custom'];
+    if ($type instanceof PluginFormInterface) {
+      $type_form_state = new SubFormState($form_state, ['options', 'custom']);
+      $form['options']['custom'] = $type->buildConfigurationForm($custom_form, $type_form_state, $search);
+    }
+
+    $form['advanced'] = [
+      '#type' => 'fieldset',
+      '#title' => t('Advanced settings'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+    ];
+    $form['advanced']['submit_button_selector'] = [
+      '#type' => 'textfield',
+      '#title' => t('Search button selector'),
+      '#description' => t('<a href="@jquery_url">jQuery selector</a> to identify the search button in the search form. Use the ID attribute of the "Search" submit button to prevent issues when another button is present (e.g., "Reset"). The selector is evaluated relative to the form. The default value is ":submit".', ['@jquery_url' => 'https://api.jquery.com/category/selectors/']),
+      '#default_value' => $search->getOption('submit_button_selector', ':submit'),
+      '#required' => TRUE,
+      '#parents' => ['options', 'submit_button_selector'],
+    ];
+    $form['advanced']['autosubmit'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Enable auto-submit'),
+      '#description' => t('When enabled, the search form will automatically be submitted when a selection is made by pressing "Enter".'),
+      '#default_value' => $search->getOption('autosubmit', TRUE),
+      '#parents' => ['options', 'autosubmit'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Form AJAX handler for search_api_autocomplete_admin_search_edit().
+   */
+  function search_api_autocomplete_suggester_ajax_callback(array $form, array &$form_state) {
+    return $form['options']['suggester_configuration'];
+  }
+
+  /**
+   * Validate callback for search_api_autocomplete_admin_search_edit().
+   *
+   * @see search_api_autocomplete_admin_search_edit()
+   * @see search_api_autocomplete_admin_search_edit_submit()
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $values = &$form_state->getValues();
+    // Call the config form validation method of the selected suggester plugin,
+    // but only if it was the same plugin that created the form.
+    if ($values['suggester_id'] == $values['old_suggester_id']) {
+      $configuration = [];
+      if (!empty($values['options']['suggester_configuration'])) {
+        $configuration = $values['options']['suggester_configuration'];
+      }
+      $suggester = $this->getSuggesterManager()->createInstance($values['suggester_id'], ['search' => $this->entity] + $configuration);
+      $suggester_form = $form['options']['suggester_configuration'];
+      unset($suggester_form['old_suggester_id']);
+      $suggester_form_state = new SubFormState($form_state, ['options', 'suggester_configuration']);
+      $suggester->validateConfigurationForm($suggester_form, $suggester_form_state);
+    }
+
+    /** @var \Drupal\search_api_autocomplete\AutocompleteTypeInterface $type */
+    $type = $form_state->get('type');
+    if ($type instanceof PluginFormInterface) {
+      $custom_form = empty($form['options']['custom']) ? [] : $form['options']['custom'];
+      $type_form_state = new SubFormState($form_state, ['options', 'custom']);
+      $type->validateConfigurationForm($custom_form, $type_form_state);
+    }
+  }
+
+  public function buildEntity(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $entity */
+    $search = $this->entity;
+
+    $values = &$form_state->getValues();
+    // Take care of custom options that aren't changed in the config form.
+    if (!empty($search->getOption('custom'))) {
+      if (!isset($values['options']['custom'])) {
+        $values['options']['custom'] = [];
+      }
+      $values['options']['custom'] += $search->getOption('custom');
+    }
+
+    $search->setStatus($values['enabled']);
+    $search->setSuggesterId($values['suggester_id']);
+    $search->setOptions($values['options']);
+
+    return $search;
+  }
+
+
+  /**
+   * Submit callback for search_api_autocomplete_admin_search_edit().
+   *
+   * @see search_api_autocomplete_admin_search_edit()
+   * @see search_api_autocomplete_admin_search_edit_validate()
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $values = &$form_state->getValues();
+    $type = $form_state->get('type');
+    if ($type instanceof PluginFormInterface) {
+      $custom_form = empty($form['options']['custom']) ? [] : $form['options']['custom'];
+      $type_form_state = new SubFormState($form_state, ['options', 'custom']);
+      $type->submitConfigurationForm($custom_form, $type_form_state);
+    }
+
+    $search = $this->entity;
+
+    // @fixme
+//    $form_state['redirect'] = 'admin/config/search/search_api/index/' . $search->index_id . '/autocomplete';
+
+    // Allow the suggester to decide how to save its configuration. If the user
+    // has disabled JS in the browser, or AJAX didn't work for some other reason,
+    // a different suggester might be selected than that which created the config
+    // form. In that case, we don't call the form submit method, save empty
+    // configuration for the plugin and stay on the page.
+    if ($values['suggester_id'] == $values['old_suggester_id']) {
+      $configuration = [];
+      if (!empty($values['options']['suggester_configuration'])) {
+        $configuration = $values['options']['suggester_configuration'];
+      }
+      $suggester = $this->getSuggesterManager()->createInstance($values['suggester_id'], [
+        'search' => $search,
+      ] + $configuration);
+      $suggester_form = $form['options']['suggester_configuration'];
+      unset($suggester_form['old_suggester_id']);
+      $suggester_form_state = new SubFormState($form_state, ['options', 'suggester_configuration']);
+      $suggester->submitConfigurationForm($suggester_form, $suggester_form_state);
+      $values['options']['suggester_configuration'] = $suggester->getConfiguration();
+    }
+    else {
+      $values['options']['suggester_configuration'] = [];
+      $form_state['redirect'] = NULL;
+      drupal_set_message(t('The used suggester plugin has changed. Please review the configuration for the new plugin.'), 'warning');
+    }
+
+    drupal_set_message(t('The autocompletion settings for the search have been saved.'));
+  }
+
+}
diff --git a/src/Permissions.php b/src/Permissions.php
new file mode 100644
index 0000000..307b637
--- /dev/null
+++ b/src/Permissions.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\search_api_autocomplete;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class Permissions implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity storage interface.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Creates a new Permissions instance.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The storage.
+   */
+  public function __construct(EntityStorageInterface $storage) {
+    $this->storage = $storage;
+  }
+
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')->getStorage('search_api_autocomplete_settings')
+    );
+  }
+
+
+  public function bySearch() {
+    $perms = [];
+    /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search */
+    foreach ($this->storage->loadMultiple() as $id => $search) {
+      $perms['use search_api_autocomplete for ' . $id] = [
+        'title' => $this->t('Use autocomplete for the %search search', ['%search' => $search->id()]),
+      ];
+    }
+    return $perms;
+  }
+
+}
diff --git a/src/Plugin/AutocompleteSuggesterManager.php b/src/Plugin/AutocompleteSuggesterManager.php
new file mode 100644
index 0000000..2b4499f
--- /dev/null
+++ b/src/Plugin/AutocompleteSuggesterManager.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Plugin;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\search_api_autocomplete\Annotation\AutocompleteSuggester;
+use Drupal\search_api_autocomplete\Annotation\AutocompleteType;
+use Drupal\search_api_autocomplete\AutocompleteSuggesterInterface;
+use Drupal\search_api_autocomplete\AutocompleteTypeInterface;
+
+class AutocompleteSuggesterManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new AutocompleteSuggesterManager.
+   *
+   * @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\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/search_api_autocomplete/AutocompleteSuggester', $namespaces, $module_handler, AutocompleteSuggesterInterface::class, AutocompleteSuggester::class);
+
+    $this->setCacheBackend($cache_backend, 'search_api_autocomplete_suggester');
+    $this->alterInfo('search_api_autocomplete_suggester');
+  }
+
+}
diff --git a/src/Plugin/AutocompleteTypeManager.php b/src/Plugin/AutocompleteTypeManager.php
new file mode 100644
index 0000000..c5c4f0b
--- /dev/null
+++ b/src/Plugin/AutocompleteTypeManager.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Plugin;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\search_api_autocomplete\Annotation\AutocompleteType;
+use Drupal\search_api_autocomplete\AutocompleteTypeInterface;
+
+class AutocompleteTypeManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new AutocompleteTypeManagerr.
+   *
+   * @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\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/search_api_autocomplete/AutocompleteType', $namespaces, $module_handler, AutocompleteTypeInterface::class, AutocompleteType::class);
+
+    $this->setCacheBackend($cache_backend, 'search_api_autocomplete_type');
+    $this->alterInfo('search_api_autocomplete_type');
+  }
+
+}
diff --git a/src/Plugin/search_api_autocomplete/AutocompleteSuggester/Server.php b/src/Plugin/search_api_autocomplete/AutocompleteSuggester/Server.php
new file mode 100644
index 0000000..581a79c
--- /dev/null
+++ b/src/Plugin/search_api_autocomplete/AutocompleteSuggester/Server.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Plugin\search_api_autocomplete\AutocompleteSuggester;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api_autocomplete\AutocompleteSuggesterInterface;
+use Drupal\search_api_autocomplete\SearchApiAutocompleteInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a suggester plugin that retrieves suggestions from the server.
+ *
+ * The server needs to support the "search_api_autocomplete" feature for this to
+ * work.
+ *
+ * @AutocompleteSuggester(
+ *   id = "server",
+ *   label = @Translation("Retrieve from server"),
+ *   description = @Translation("For compatible servers, ask the server for autocomplete suggestions."),
+ * )
+ */
+class Server extends SuggesterPluginBase implements AutocompleteSuggesterInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $search = $configuration['search'];
+    return new static(
+      $search,
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function supportsIndex(IndexInterface $index) {
+    try {
+      return $index->getServerInstance() && $index->getServerInstance()->supportsFeature('search_api_autocomplete');
+    }
+    catch (SearchApiException $e) {
+      return FALSE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'fields' => [],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    // Add a list of fields to include for autocomplete searches.
+    $search = $this->getSearch();
+    $fields = $search->index()->getFields();
+    $fulltext_fields = $search->index()->getFulltextFields();
+    $options = [];
+    foreach ($fulltext_fields as $field) {
+      $options[$field] = $fields[$field]->getFieldIdentifier();
+    }
+    $form['fields'] = [
+      '#type' => 'checkboxes',
+      '#title' => t('Override used fields'),
+      '#description' => t('Select the fields which should be searched for matches when looking for autocompletion suggestions. Leave blank to use the same fields as the underlying search.'),
+      '#options' => $options,
+      '#default_value' => array_combine($this->getConfiguration()['fields'], $this->getConfiguration()['fields']),
+      '#attributes' => ['class' => ['search-api-checkboxes-list']],
+    ];
+    $form['#attached']['library'][] = 'search_api/drupal.search_api.admin_css';
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::submitConfigurationForm($form, $form_state);
+
+    $values = $form_state->getValues();
+    $values['fields'] = array_keys(array_filter($values['fields']));
+    $this->setConfiguration($values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAutocompleteSuggestions(QueryInterface $query, $incomplete_key, $user_input) {
+    if ($this->configuration['fields']) {
+      $query->setFulltextFields($this->configuration['fields']);
+    }
+
+    if (($server = $query->getIndex()->getServerInstance()) && $server->supportsFeature('search_api_autocomplete')) {
+      return $server->getBackend()->getAutocompleteSuggestions($query, $this->getSearch(), $incomplete_key, $user_input);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+  }
+
+}
diff --git a/src/SearchApiAutocompleteSuggesterPluginBase.php b/src/Plugin/search_api_autocomplete/AutocompleteSuggester/SuggesterPluginBase.php
similarity index 54%
rename from src/SearchApiAutocompleteSuggesterPluginBase.php
rename to src/Plugin/search_api_autocomplete/AutocompleteSuggester/SuggesterPluginBase.php
index e1c0938..6120b90 100644
--- a/src/SearchApiAutocompleteSuggesterPluginBase.php
+++ b/src/Plugin/search_api_autocomplete/AutocompleteSuggester/SuggesterPluginBase.php
@@ -1,22 +1,23 @@
 <?php
 
-/**
- * @file
- * Contains SearchApiAutocompleteSuggesterPluginBase.
- */
+namespace Drupal\search_api_autocomplete\Plugin\search_api_autocomplete\AutocompleteSuggester;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Plugin\ConfigurablePluginInterface;
+use Drupal\search_api_autocomplete\AutocompleteSuggesterInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
 
 /**
  * Provides a base class for suggester plugins.
- *
- * @see SearchApiAutocompleteSuggesterInterface
- * @see hook_search_api_autocomplete_suggester_info()
  */
-abstract class SearchApiAutocompleteSuggesterPluginBase implements SearchApiAutocompleteSuggesterInterface {
+abstract class SuggesterPluginBase extends PluginBase implements AutocompleteSuggesterInterface, ConfigurablePluginInterface {
 
   /**
    * The search this suggester is attached to.
    *
-   * @var SearchApiAutocompleteSearch
+   * @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch
    */
   protected $search;
 
@@ -32,29 +33,19 @@ abstract class SearchApiAutocompleteSuggesterPluginBase implements SearchApiAuto
    *
    * @var array
    */
-  protected $pluginDefinition = array();
+  protected $pluginDefinition = [];
 
   /**
    * The suggester plugin's configuration.
    *
    * @var array
    */
-  protected $configuration = array();
+  protected $configuration = [];
 
   /**
    * {@inheritdoc}
    */
-  public static function create(SearchApiAutocompleteSearch $search, array $configuration, $plugin_id, array $plugin_definition) {
-    // It seems there is no way to have "new static()"-like functionality in
-    // PHP 5.2, so we have to use this workaround instead.
-    $class = $plugin_definition['class'];
-    return new $class($search, $configuration, $plugin_id, $plugin_definition);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function supportsIndex(SearchApiIndex $index) {
+  public static function supportsIndex(IndexInterface $index) {
     return TRUE;
   }
 
@@ -71,88 +62,78 @@ abstract class SearchApiAutocompleteSuggesterPluginBase implements SearchApiAuto
    *   The suggester plugin's definition.
    */
   public function __construct(SearchApiAutocompleteSearch $search, array $configuration, $plugin_id, array $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->search = $search;
-    $this->pluginId = $plugin_id;
-    $this->pluginDefinition = $plugin_definition;
-    $this->setConfiguration($configuration);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getPluginId() {
-    return $this->pluginId;
+  public function getSearch() {
+    return $this->search;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getPluginDefinition() {
-    return $this->pluginDefinition;
+  public function getDescription() {
+    return isset($this->pluginDefinition['description']) ? $this->pluginDefinition['description'] : NULL;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getSearch() {
-    return $this->search;
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration + $this->defaultConfiguration();
+    return $this;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function label() {
-    return $this->pluginDefinition['label'];
+  public function defaultConfiguration() {
+    return [];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getDescription() {
-    return isset($this->pluginDefinition['description']) ? $this->pluginDefinition['description'] : NULL;
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    return [];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getConfiguration() {
-    return $this->configuration;
-  }
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {}
 
   /**
    * {@inheritdoc}
    */
-  public function setConfiguration(array $configuration) {
-    $this->configuration = $configuration + $this->defaultConfiguration();
-    return $this;
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->setConfiguration($form_state->getValues());
   }
 
   /**
    * {@inheritdoc}
    */
-  public function defaultConfiguration() {
-    return array();
+  public function label() {
+    return $this->pluginDefinition['label'];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function buildConfigurationForm(array $form, array &$form_state) {
-    return array();
+  public function getConfiguration() {
+    return $this->configuration + $this->defaultConfiguration();
   }
 
   /**
    * {@inheritdoc}
    */
-  public function validateConfigurationForm(array $form, array &$form_state) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitConfigurationForm(array $form, array &$form_state) {
-    if (!empty($form_state['values'])) {
-      $this->setConfiguration($form_state['values']);
-    }
+  public function calculateDependencies() {
+    return [];
   }
 
+
 }
diff --git a/src/Plugin/search_api_autocomplete/AutocompleteType/Page.php b/src/Plugin/search_api_autocomplete/AutocompleteType/Page.php
new file mode 100644
index 0000000..13c406b
--- /dev/null
+++ b/src/Plugin/search_api_autocomplete/AutocompleteType/Page.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Plugin\search_api_autocomplete\AutocompleteType;
+
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Utility;
+use Drupal\search_api_autocomplete\AutocompleteTypeInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+use Drupal\search_api_page\Entity\SearchApiPage;
+
+/**
+ * @AutocompleteType(
+ *   id = "page",
+ *   label = @Translation("Search pages"),
+ *   description = @Translation("Searches provided by the <em>Search pages</em> module."),
+ *   provider = "search_api_page",
+ * )
+ */
+class Page extends PluginBase implements AutocompleteTypeInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return $this->pluginDefinition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->pluginDefinition['description'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function listSearches(IndexInterface $index) {
+    $ret = array();
+    $storage = \Drupal::entityTypeManager()->getStorage('search_api_page');
+    foreach ($storage->loadByProperties(['index' => $index->id()]) as $page) {
+      $id = 'search_api_page_' . $page->id();
+      $ret[$id]['name'] = $page->label();
+      $ret[$id]['options']['custom']['page_id'] = $page->id();
+    }
+    return $ret;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createQuery(SearchApiAutocompleteSearch $search, $complete, $incomplete) {
+    $page = SearchApiPage::load($search->getOption('custom.page_id'));
+    // Copied from search_api_page_search_execute().
+    $query = Utility::createQuery(Index::load($page->getIndex()));
+    $query
+      ->keys($complete);
+    if ($page->getFulltextFields()) {
+      $query->setFulltextFields($page->getSearchedFields());
+    }
+    return $query;
+  }
+
+}
diff --git a/src/Plugin/search_api_autocomplete/AutocompleteType/Views.php b/src/Plugin/search_api_autocomplete/AutocompleteType/Views.php
new file mode 100644
index 0000000..9fdb314
--- /dev/null
+++ b/src/Plugin/search_api_autocomplete/AutocompleteType/Views.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace Drupal\search_api_autocomplete\Plugin\search_api_autocomplete\AutocompleteType;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Url;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api_autocomplete\AutocompleteTypeInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+use Drupal\views\Views as ViewsViews;
+
+/**
+ * @AutocompleteType(
+ *   id = "views",
+ *   label = @Translation("Search views"),
+ *   description = @Translation("Searches provided by views"),
+ *   provider = "search_api",
+ * )
+ */
+class Views extends PluginBase implements AutocompleteTypeInterface, ConfigurablePluginInterface, PluginFormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return $this->pluginDefinition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->pluginDefinition['description'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'display' => 'default',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search */
+    $search = $form_state->getFormObject()->getEntity();
+    $views_id = substr($search->id(), 17);
+    $view = ViewsViews::getView($views_id);
+    $options = array();
+    $view->initDisplay();
+    foreach ($view->displayHandlers as $id => $display) {
+      /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase $display */
+      $options[$id] = $display->display['display_title'];
+    }
+    $form['display'] = [
+      '#type' => 'select',
+      '#title' => t('Views display'),
+      '#description' => t('Please select the Views display whose settings should be used for autocomplete queries.<br />' .
+        "<strong>Note:</strong> Autocompletion doesn't work well with contextual filters. Please see the <a href=':readme_url'>README.txt</a> file for details.",
+        [':readme_url' => Url::fromUri('base://' . drupal_get_path('module', 'search_api_autocomplete') . '/README.txt')->toString()]),
+      '#options' => $options,
+      '#default_value' => $search->getOption('custom.display') ? : 'default',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search */
+    $search = $form_state->getFormObject()->getEntity();
+    $views_id = substr($search->id(), 17);
+    $view = ViewsViews::getView($views_id);
+    $view->setDisplay($form_state->getValue('display'));
+    $view->preExecute();
+    if ($view->argument) {
+      drupal_set_message(t('You have selected a display with contextual filters. This can lead to various problems. Please see the <a href=":readme_url">README.txt</a> file for details.',
+        [':readme_url' => Url::fromUri('base://' . drupal_get_path('module', 'search_api_autocomplete') . '/README.txt')->toString()]), 'warning');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function listSearches(IndexInterface $index) {
+    $ret = array();
+    $base_table = 'search_api_index_' . $index->id();
+    foreach (ViewsViews::getAllViews() as $id => $view) {
+      if ($view->get('base_table') === $base_table) {
+        // @todo Check whether there is an exposed fulltext filter
+        $ret['search_api_views_' . $id] = [
+          'name' => $id,
+        ];
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createQuery(SearchApiAutocompleteSearch $search, $complete, $incomplete) {
+    $views_id = substr($search->id(), 17);
+    $view = ViewsViews::getView($views_id);
+    if (!$view) {
+      $vars['@view'] = $views_id;
+      throw new SearchApiException(t('Could not load view @view.', $vars));
+    }
+    $view->setDisplay($search->getOption('custom.display'));
+    $view->preExecute();
+    $view->build();
+    /** @var \Drupal\search_api\Query\Query $query */
+    $query = $view->getQuery()->getSearchApiQuery();
+    if (!$query) {
+      $vars['@view'] = $view->storage->label() ?: $views_id;
+      throw new SearchApiException(t('Could not create query for view @view.', $vars));
+    }
+    // $query->setFulltextFields([$complete]);
+    // @todo What are the right values to use here?
+    $query->setFulltextFields();
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return [];
+  }
+
+}
diff --git a/src/RouteProvider.php b/src/RouteProvider.php
new file mode 100644
index 0000000..50ce47a
--- /dev/null
+++ b/src/RouteProvider.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\search_api_autocomplete;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
+
+class RouteProvider extends DefaultHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditFormRoute(EntityTypeInterface $entity_type) {
+//    $route = parent::getEditFormRoute($entity_type);
+//    $route->setOption('parameters', ['search_api_index' => [
+//      'type' => 'entity:' . 'search_api_index',
+//    ]]);
+//    $route->setRequirement('_entity_access', 'search_api_index.update');
+//
+//    return $route;
+  }
+
+
+}
diff --git a/search_api_autocomplete.interface.php b/src/SearchApiAutocompleteInterface.php
similarity index 76%
rename from search_api_autocomplete.interface.php
rename to src/SearchApiAutocompleteInterface.php
index 9898a53..be27ddc 100644
--- a/search_api_autocomplete.interface.php
+++ b/src/SearchApiAutocompleteInterface.php
@@ -1,9 +1,10 @@
 <?php
 
-/**
- * @file
- * Contains SearchApiAutocompleteInterface.
- */
+namespace Drupal\search_api_autocomplete;
+
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\Backend\BackendInterface;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
 
 
 /**
@@ -12,7 +13,7 @@
  * Please note that this interface is purely documentational. You shouldn't, and
  * can't, implement it explicitly (unless the module is depending on this one).
  */
-interface SearchApiAutocompleteInterface extends SearchApiServiceInterface {
+interface SearchApiAutocompleteInterface extends BackendInterface {
 
   /**
    * Retrieves autocompletion suggestions for some user input.
@@ -38,9 +39,9 @@ interface SearchApiAutocompleteInterface extends SearchApiServiceInterface {
    *   );
    * @endcode
    *
-   * @param SearchApiQueryInterface $query
+   * @param \Drupal\search_api\Query\QueryInterface $query
    *   A query representing the completed user input so far.
-   * @param SearchApiAutocompleteSearch $search
+   * @param \Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch $search
    *   An object containing details about the search the user is on, and
    *   settings for the autocompletion. See the class documentation for details.
    *   Especially $search->options should be checked for settings, like whether
@@ -57,6 +58,6 @@ interface SearchApiAutocompleteInterface extends SearchApiServiceInterface {
    *   An array of suggestions, as defined by
    *   SearchApiAutocompleteSuggesterInterface::getAutocompleteSuggestions().
    */
-  public function getAutocompleteSuggestions(SearchApiQueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input);
+  public function getAutocompleteSuggestions(QueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input);
 
 }
diff --git a/src/SearchApiAutocompleteServerSuggester.php b/src/SearchApiAutocompleteServerSuggester.php
index 545a620..2ba5777 100644
--- a/src/SearchApiAutocompleteServerSuggester.php
+++ b/src/SearchApiAutocompleteServerSuggester.php
@@ -1,81 +1,13 @@
 <?php
 
-/**
- * @file
- * Contains SearchApiAutocompleteServerSuggester.
- */
+namespace Drupal\search_api_autocomplete;
+
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\SearchApiException;
 
 /**
- * Provides a suggester plugin that retrieves suggestions from the server.
- *
- * The server needs to support the "search_api_autocomplete" feature for this to
- * work.
  */
 class SearchApiAutocompleteServerSuggester extends SearchApiAutocompleteSuggesterPluginBase {
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function supportsIndex(SearchApiIndex $index) {
-    try {
-      return $index->server() && $index->server()->supportsFeature('search_api_autocomplete');
-    }
-    catch (SearchApiException $e) {
-      return FALSE;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function defaultConfiguration() {
-    return array(
-      'fields' => array(),
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildConfigurationForm(array $form, array &$form_state) {
-    // Add a list of fields to include for autocomplete searches.
-    $search = $this->getSearch();
-    $fields = $search->index()->getFields();
-    $fulltext_fields = $search->index()->getFulltextFields();
-    $options = array();
-    foreach ($fulltext_fields as $field) {
-      $options[$field] = check_plain($fields[$field]['name']);
-    }
-    $form['fields'] = array(
-      '#type' => 'checkboxes',
-      '#title' => t('Override used fields'),
-      '#description' => t('Select the fields which should be searched for matches when looking for autocompletion suggestions. Leave blank to use the same fields as the underlying search.'),
-      '#options' => $options,
-      '#default_value' => drupal_map_assoc($this->configuration['fields']),
-      '#attributes' => array('class' => array('search-api-checkboxes-list')),
-    );
-    $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitConfigurationForm(array $form, array &$form_state) {
-    $values = $form_state['values'];
-    $values['fields'] = array_keys(array_filter($values['fields']));
-    $this->setConfiguration($values);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAutocompleteSuggestions(SearchApiQueryInterface $query, $incomplete_key, $user_input) {
-    if ($this->configuration['fields']) {
-      $query->fields($this->configuration['fields']);
-    }
-    return $query->getIndex()->server()->getAutocompleteSuggestions($query, $this->getSearch(), $incomplete_key, $user_input);
-  }
-
 }
diff --git a/templates/search-api-autocomplete-suggestion.html.twig b/templates/search-api-autocomplete-suggestion.html.twig
new file mode 100644
index 0000000..ccd3343
--- /dev/null
+++ b/templates/search-api-autocomplete-suggestion.html.twig
@@ -0,0 +1,17 @@
+<div class="search-api-autocomplete-suggestion">
+{% if prefix %}
+  <span class="autocomplete-suggestion-note">{{ prefix }}</span>
+{% endif %}
+{% if suggestion_prefix %}
+  <span class="autocomplete-suggestion-suggestion-prefix">{{ suggestion_prefix }}</span>
+{% endif %}
+{% if user_input %}
+  <span class="autocomplete-suggestion-user-input">{{ user_input }}</span>
+{% endif %}
+{% if suggestion_suffix %}
+  <span class="autocomplete-suggestion-suggestion-suffix">{{ suggestion_suffix }}</span>
+{% endif %}
+{% if results %}
+  <span class="autocomplete-suggestion-results">{{ results }}</span>
+{% endif %}
+</div>
diff --git a/tests/src/Kernel/SearchApiAutocompleteSearchCrudTest.php b/tests/src/Kernel/SearchApiAutocompleteSearchCrudTest.php
new file mode 100644
index 0000000..47494fe
--- /dev/null
+++ b/tests/src/Kernel/SearchApiAutocompleteSearchCrudTest.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\Tests\search_api_autocomplete\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\search_api_autocomplete\Entity\SearchApiAutocompleteSearch;
+
+/**
+ * Tests saving a search api autocomplete config entity.
+ *
+ * @group search_api_autocomplete
+ */
+class SearchApiAutocompleteSearchCrudTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['search_api_autocomplete', 'search_api', 'user'];
+
+  public function testCreate() {
+    $autocomplete_search = SearchApiAutocompleteSearch::create([
+      'id' => 'muh',
+      'label' => 'Meh',
+      'index_id' => 'index1',
+      'suggester_id' => 'suggstester1',
+      'type' => 'test_type',
+      'options' => ['key' => 'value'],
+    ]);
+    $autocomplete_search->save();
+  }
+
+}
