diff --git a/config/schema/search_api.index.schema.yml b/config/schema/search_api.index.schema.yml
index eab6700..f7d6664 100644
--- a/config/schema/search_api.index.schema.yml
+++ b/config/schema/search_api.index.schema.yml
@@ -50,17 +50,17 @@ search_api.index.*:
             - type: mapping
               label: 'A processor'
               mapping:
-                status:
-                  type: string
-                  label: 'Status of the processor'
-                weight:
-                  type: integer
-                  label: 'The weight of the processor'
-                processorPluginId:
+                processor_id:
                   type: string
                   label: 'The plugin ID of the processor'
+                weights:
+                  type: sequence
+                  label: 'The processor''s weights for the different processing stages'
+                  sequence:
+                    - type: integer
+                      label: 'The processor''s weight for this stage'
                 settings:
-                  type: search_api.processor.plugin.[%parent.processorPluginId]
+                  type: search_api.processor.plugin.[%parent.processor_id]
     datasources:
       type: sequence
       label: 'Datasource plugin IDs'
diff --git a/css/search_api.admin.css b/css/search_api.admin.css
index 9987671..58f98a4 100644
--- a/css/search_api.admin.css
+++ b/css/search_api.admin.css
@@ -65,6 +65,23 @@
 }
 
 /*
+ * Processors page
+ */
+.search-api-stage-wrapper.form-item {
+  width: 31%;
+  min-width: 16em;
+  float: left;
+  margin-right: 1%;
+}
+
+.search-api-stage-wrapper-preprocess_query {
+}
+
+.search-api-stage-wrapper-postprocess.form-item {
+  margin-right: 0;
+}
+
+/*
  * Miscellaneous
  */
 .search-api-checkboxes-list div {
diff --git a/js/index-active-formatters.js b/js/index-active-formatters.js
index 4220599..119c58b 100644
--- a/js/index-active-formatters.js
+++ b/js/index-active-formatters.js
@@ -9,44 +9,33 @@
 
   Drupal.behaviors.searchApiIndexFormatter = {
     attach: function (context, settings) {
-      $('.search-api-status-wrapper input.form-checkbox', context).once('search-api-status', function () {
+      $('.search-api-status-wrapper input.form-checkbox', context).each(function () {
         var $checkbox = $(this);
-        // Retrieve the table row belonging to this processor.
-        var $row = $('#' + $checkbox.attr('id').replace(/-status$/, '-weight'), context).closest('tr');
-        // Retrieve the vertical tab belonging to this processor.
-        var $tab = $('#' + $checkbox.attr('id').replace(/-status$/, '-settings'), context).data('verticalTab');
+        var processor_id = $checkbox.data('id');
+
+        var $rows = $('.search-api-processor-weight--' + processor_id, context);
+        var tab = $('.search-api-processor-settings-' + processor_id, context).data('verticalTab');
 
         // Bind a click handler to this checkbox to conditionally show and hide
         // the filter's table row and vertical tab pane.
-        $checkbox.bind('click.searchApiUpdate', function () {
+        $checkbox.on('click.searchApiUpdate', function () {
           if ($checkbox.is(':checked')) {
-            $('#edit-order').show();
-            $('.tabledrag-toggle-weight-wrapper').show();
-            $row.show();
-            if ($tab) {
-              $tab.tabShow().updateSummary();
+            $rows.show();
+            if (tab) {
+              tab.tabShow().updateSummary();
             }
           }
           else {
-            var $enabled_processors = $('.search-api-status-wrapper input.form-checkbox:checked').length;
-
-            if (!$enabled_processors) {
-              $('#edit-order').hide();
-              $('.tabledrag-toggle-weight-wrapper').hide();
-            }
-
-            $row.hide();
-            if ($tab) {
-              $tab.tabHide().updateSummary();
+            $rows.hide();
+            if (tab) {
+              tab.tabHide().updateSummary();
             }
           }
-          // Re-stripe the table after toggling visibility of table row.
-          Drupal.tableDrag['edit-order'].restripeTable();
         });
 
         // Attach summary for configurable items (only for screen-readers).
-        if ($tab) {
-          $tab.details.drupalSetSummary(function () {
+        if (tab) {
+          tab.details.drupalSetSummary(function () {
             return $checkbox.is(':checked') ? Drupal.t('Enabled') : Drupal.t('Disabled');
           });
         }
diff --git a/search_api.services.yml b/search_api.services.yml
index 71b5ee2..567fc9b 100644
--- a/search_api.services.yml
+++ b/search_api.services.yml
@@ -9,7 +9,7 @@ services:
 
   plugin.manager.search_api.processor:
     class: Drupal\search_api\Processor\ProcessorPluginManager
-    parent: default_plugin_manager
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@string_translation']
 
   plugin.manager.search_api.tracker:
     class: Drupal\search_api\Tracker\TrackerPluginManager
diff --git a/src/Annotation/SearchApiProcessor.php b/src/Annotation/SearchApiProcessor.php
index 950890b..feee140 100644
--- a/src/Annotation/SearchApiProcessor.php
+++ b/src/Annotation/SearchApiProcessor.php
@@ -12,6 +12,11 @@ use Drupal\Component\Annotation\Plugin;
 /**
  * Defines a Search API processor annotation object.
  *
+ * @see \Drupal\search_api\Processor\ProcessorPluginManager
+ * @see \Drupal\search_api\Processor\ProcessorInterface
+ * @see \Drupal\search_api\Processor\ProcessorPluginBase
+ * @see plugin_api
+ *
  * @Annotation
  */
 class SearchApiProcessor extends Plugin {
@@ -41,4 +46,16 @@ class SearchApiProcessor extends Plugin {
    */
   public $description;
 
+  /**
+   * The stages this processor will run in, along with their default weights.
+   *
+   * This is represented as an associative array, mapping one or more of the
+   * stage identifiers to the default weight for that stage. For the available
+   * stages, see
+   * \Drupal\search_api\Processor\ProcessorPluginManager::getProcessingStages().
+   *
+   * @var int[]
+   */
+  public $stages;
+
 }
diff --git a/src/Annotation/SearchApiTracker.php b/src/Annotation/SearchApiTracker.php
index 75076c1..0781f92 100644
--- a/src/Annotation/SearchApiTracker.php
+++ b/src/Annotation/SearchApiTracker.php
@@ -12,6 +12,11 @@ use Drupal\Component\Annotation\Plugin;
 /**
  * Defines a Search API tracker annotation object.
  *
+ * @see \Drupal\search_api\Tracker\TrackerPluginManager
+ * @see \Drupal\search_api\Tracker\TrackerInterface
+ * @see \Drupal\search_api\Tracker\TrackerPluginBase
+ * @see plugin_api
+ *
  * @Annotation
  */
 class SearchApiTracker extends Plugin {
diff --git a/src/Entity/Index.php b/src/Entity/Index.php
index 1bea80e..030a838 100644
--- a/src/Entity/Index.php
+++ b/src/Entity/Index.php
@@ -18,6 +18,7 @@ use Drupal\Core\TypedData\ListDataDefinitionInterface;
 use Drupal\search_api\SearchApiException;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\Item\GenericFieldInterface;
+use Drupal\search_api\Processor\ProcessorInterface;
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\Query\ResultSetInterface;
 use Drupal\search_api\ServerInterface;
@@ -224,11 +225,11 @@ class Index extends ConfigEntityBase implements IndexInterface {
   protected $fulltextFields;
 
   /**
-   * The index's processor plugins.
+   * Cached information about the processors available for this index.
    *
    * @var \Drupal\search_api\Processor\ProcessorInterface[]|null
    *
-   * @see getProcessors()
+   * @see loadProcessors()
    */
   protected $processors;
 
@@ -441,68 +442,88 @@ class Index extends ConfigEntityBase implements IndexInterface {
   /**
    * {@inheritdoc}
    */
-  public function getProcessors($all = FALSE, $sortBy = 'weight') {
-    /** @var $processorPluginManager \Drupal\search_api\Processor\ProcessorPluginManager */
-    $processorPluginManager = \Drupal::service('plugin.manager.search_api.processor');
-    $processor_definitions = $processorPluginManager->getDefinitions();
-    $processors_settings = $this->getOption('processors', array());
-
-    // Only do this if we do not already have our processors
-    foreach ($processor_definitions as $name => $processor_definition) {
-      // Instantiate the processors
-      if (class_exists($processor_definition['class'])) {
-
-        // Give it some sensible weight default so we can return them in order
-        if (empty($processors_settings[$name])) {
-          $processors_settings[$name] = array('weight' => 0, 'status' => 0);
+  public function getProcessors($only_enabled = TRUE) {
+    $processors = $this->loadProcessors();
+
+    // Filter processors by status if required. Enabled processors are those
+    // which have settings in the "processors" option.
+    if ($only_enabled) {
+      $processors_settings = $this->getOption('processors', array());
+      $processors = array_intersect_key($processors, $processors_settings);
+    }
+
+    return $processors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProcessorsByStage($stage, $only_enabled = TRUE) {
+    $processors = $this->loadProcessors();
+    $processor_settings = $this->getOption('processors', array());
+    $processor_weights = array();
+
+    // Get a list of all processors meeting the criteria (stage and, optionally,
+    // enabled) along with their effective weights (user-set or default).
+    foreach ($processors as $name => $processor) {
+      if ($processor->supportsStage($stage) && !($only_enabled && empty($processor_settings[$name]['status']))) {
+        if (!empty($processor_settings[$name]['weights'][$stage])) {
+          $processor_weights[$name] = $processor_settings[$name]['weights'][$stage];
         }
+        else {
+          $processor_weights[$name] = $processor->getDefaultWeight($stage);
+        }
+      }
+    }
+
+    // Sort requested processors by weight.
+    asort($processor_weights);
+
+    $return_processors = array();
+    foreach ($processor_weights as $name => $weight) {
+      $return_processors[$name] = $processors[$name];
+    }
+    return $return_processors;
+  }
+
+  /**
+   * Retrieves all processors supported by this index.
+   *
+   * @return \Drupal\search_api\Processor\ProcessorInterface[]
+   *   The loaded processors, keyed by processor ID.
+   */
+  protected function loadProcessors() {
+    if (!isset($this->processors)) {
+      /** @var $processor_plugin_manager \Drupal\search_api\Processor\ProcessorPluginManager */
+      $processor_plugin_manager = \Drupal::service('plugin.manager.search_api.processor');
+      $processor_settings = $this->getOption('processors', array());
 
-        if (empty($this->processors[$name])) {
-          // Create our settings for this processor
-          $settings = empty($processors_settings[$name]['settings']) ? array() : $processors_settings[$name]['settings'];
+      foreach ($processor_plugin_manager->getDefinitions() as $name => $processor_definition) {
+        if (class_exists($processor_definition['class']) && empty($this->processors[$name])) {
+          // Create our settings for this processor.
+          $settings = empty($processor_settings[$name]['settings']) ? array() : $processor_settings[$name]['settings'];
           $settings['index'] = $this;
 
           /** @var $processor \Drupal\search_api\Processor\ProcessorInterface */
-          $processor = $processorPluginManager->createInstance($name, $settings);
+          $processor = $processor_plugin_manager->createInstance($name, $settings);
           if ($processor->supportsIndex($this)) {
             $this->processors[$name] = $processor;
           }
         }
-      }
-      else {
-        \Drupal::logger('search_api')->warning('Processor @id specifies a non-existing @class.', array('@id' => $name, '@class' => $processor_definition['class']));
-      }
-    }
-
-    if ($sortBy == 'weight') {
-      // Sort by weight.
-      uasort($processors_settings, array('Drupal\Component\Utility\SortArray', 'sortByWeightElement'));
-    }
-    else {
-      // Sort by processor ID.
-      ksort($processors_settings);
-    }
-
-    // Filter by status and return.
-    $active_processors = array();
-    // Find out which ones are enabled
-    foreach ($processors_settings as $name => $processor_setting) {
-      // Find out which ones we want
-      if ($all || $processor_setting['status']) {
-        if (!empty($this->processors[$name])) {
-          $active_processors[$name] = $this->processors[$name];
+        elseif (!class_exists($processor_definition['class'])) {
+          \Drupal::logger('search_api')->warning('Processor @id specifies a non-existing @class.', array('@id' => $name, '@class' => $processor_definition['class']));
         }
       }
     }
 
-    return $active_processors;
+    return $this->processors;
   }
 
   /**
    * {@inheritdoc}
    */
   public function preprocessIndexItems(array &$items) {
-    foreach ($this->getProcessors() as $processor) {
+    foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_PREPROCESS_INDEX) as $processor) {
       $processor->preprocessIndexItems($items);
     }
   }
@@ -511,7 +532,7 @@ class Index extends ConfigEntityBase implements IndexInterface {
    * {@inheritdoc}
    */
   public function preprocessSearchQuery(QueryInterface $query) {
-    foreach ($this->getProcessors() as $processor) {
+    foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_PREPROCESS_QUERY) as $processor) {
       $processor->preprocessSearchQuery($query);
     }
   }
@@ -521,7 +542,7 @@ class Index extends ConfigEntityBase implements IndexInterface {
    */
   public function postprocessSearchResults(ResultSetInterface $results) {
     /** @var $processor \Drupal\search_api\Processor\ProcessorInterface */
-    foreach (array_reverse($this->getProcessors()) as $processor) {
+    foreach (array_reverse($this->getProcessorsByStage(ProcessorInterface::STAGE_POSTPROCESS_QUERY)) as $processor) {
       $processor->postprocessSearchResults($results);
     }
   }
@@ -640,8 +661,8 @@ class Index extends ConfigEntityBase implements IndexInterface {
     // All field identifiers should start with the datasource ID.
     if (!$prefix && $datasource_id) {
       $prefix = $datasource_id . self::DATASOURCE_ID_SEPARATOR;
-      $label_prefix = $datasource_id ? $this->getDatasource($datasource_id)->label() . ' » ' : '';
     }
+    $datasource_label = $datasource_id ? $this->getDatasource($datasource_id)->label() . ' » ' : '';
 
     // Loop over all properties and handle them accordingly.
     $recurse = array();
@@ -736,7 +757,7 @@ class Index extends ConfigEntityBase implements IndexInterface {
       $field = Utility::createField($this, $key);
       $field->setType($field_type);
       $field->setLabel($label);
-      $field->setLabelPrefix($label_prefix);
+      $field->setLabelPrefix($datasource_label);
       $field->setDescription($description);
       $field->setIndexed(FALSE);
       $this->fields[0]['fields'][$key] = $field;
@@ -1125,10 +1146,14 @@ class Index extends ConfigEntityBase implements IndexInterface {
     // language" field.
     // @todo Replace this with a cleaner, more flexible approach. See
     //   https://drupal.org/node/2090341
-    $this->options['processors']['language']['status'] = TRUE;
-    $this->options['processors']['language']['weight'] = -50;
-    $this->options['processors']['language']['processorPluginId'] = 'language';
-    $this->options['processors']['language'] += array('settings' => array());
+    $this->options['processors']['language'] = array(
+      'processor_id' => 'language',
+      'weights' =>
+        array(
+          'preprocess_index' => -50,
+        ),
+      'settings' => array(),
+    );
     $this->options['fields']['search_api_language'] = array('type' => 'string');
   }
 
@@ -1303,4 +1328,23 @@ class Index extends ConfigEntityBase implements IndexInterface {
     $this->resetCaches(FALSE);
   }
 
+  /**
+   * Implements the magic __sleep() method.
+   *
+   * Prevents the cached plugins and fields from being serialized.
+   */
+  public function __sleep() {
+    $properties = get_object_vars($this);
+    unset($properties['datasourcePlugins']);
+    unset($properties['trackerPlugin']);
+    unset($properties['serverInstance']);
+    unset($properties['fields']);
+    unset($properties['datasourceFields']);
+    unset($properties['fulltextFields']);
+    unset($properties['processors']);
+    unset($properties['properties']);
+    unset($properties['datasourceAdditionalFields']);
+    return array_keys($properties);
+  }
+
 }
diff --git a/src/Form/IndexFieldsForm.php b/src/Form/IndexFieldsForm.php
index a2737ab..6c246d7 100644
--- a/src/Form/IndexFieldsForm.php
+++ b/src/Form/IndexFieldsForm.php
@@ -163,7 +163,7 @@ class IndexFieldsForm extends EntityForm {
         '#type' => 'checkbox',
         '#default_value' => $field->isIndexed(),
       );
-      $css_key = '#edit-fields-' . Html::getUniqueId($key);
+      $css_key = '#edit-fields-' . Html::getId($key);
       $build['fields'][$key]['type'] = array(
         '#type' => 'select',
         '#options' => $types,
diff --git a/src/Form/IndexFiltersForm.php b/src/Form/IndexFiltersForm.php
index 446b054..61cd065 100644
--- a/src/Form/IndexFiltersForm.php
+++ b/src/Form/IndexFiltersForm.php
@@ -7,10 +7,12 @@
 
 namespace Drupal\search_api\Form;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\String;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\search_api\Processor\ProcessorInterface;
 use Drupal\search_api\Processor\ProcessorPluginManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -75,74 +77,102 @@ class IndexFiltersForm extends EntityForm {
    * {@inheritdoc}
    */
   public function form(array $form, FormStateInterface $form_state) {
-    $processors_by_weight = $this->entity->getProcessors(TRUE, 'weight');
-    $processors_by_name = $this->entity->getProcessors(TRUE, 'name');
-    $processors_settings = $this->entity->getOption('processors');
-
-    // Make sure that we have weights and status for all processors, even new
-    // ones.
-    /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
-    foreach ($processors_by_name as $name => $processor) {
-      $processors_settings[$name]['status'] = (!isset($processors_settings[$name]['status'])) ? 0 : $processors_settings[$name]['status'];
-      $processors_settings[$name]['weight'] = (!isset($processors_settings[$name]['weight'])) ? 0 : $processors_settings[$name]['weight'];
+    // Retrieve lists of all processors, and the stages and weights they have.
+    if (!$form_state->has('processors')) {
+      $all_processors = $this->entity->getProcessors(FALSE);
+      $sort_processors = function (ProcessorInterface $a, ProcessorInterface $b) {
+        return strnatcasecmp($a->label(), $b->label());
+      };
+      uasort($all_processors, $sort_processors);
+      $form_state->set('processors', $all_processors);
+    }
+    else {
+      $all_processors = $form_state->get('processors');
+    }
 
-      $settings = empty($processors_settings[$name]['settings']) ? array() : $processors_settings[$name]['settings'];
-      $settings['index'] = $this->entity;
+    $stages = $this->processorPluginManager->getProcessingStages();
+    $processors_by_stage = array();
+    foreach ($stages as $stage => $definition) {
+      $processors_by_stage[$stage] = $this->entity->getProcessorsByStage($stage, FALSE);
     }
 
+    $processor_settings = $this->entity->getOption('processors');
+
     $form['#tree'] = TRUE;
     $form['#attached']['library'][] = 'search_api/drupal.search_api.index-active-formatters';
     $form['#title'] = $this->t('Manage filters for search index %label', array('%label' => $this->entity->label()));
-    $form['#prefix'] = '<p>' . $this->t('Configure processors which will pre- and post-process data at index and search time.') . '</p>';
+    $form['description']['#markup'] = '<p>' . $this->t('Configure processors which will pre- and post-process data at index and search time.') . '</p>';
 
     // Add the list of processors with checkboxes to enable/disable them.
     $form['status'] = array(
       '#type' => 'fieldset',
-      '#title' => $this->t('Enabled processors'),
-      '#attributes' => array('class' => array('search-api-status-wrapper')),
+      '#title' => $this->t('Enabled'),
+      '#attributes' => array('class' => array(
+        'search-api-status-wrapper',
+      )),
     );
-
-    foreach ($processors_by_name as $name => $processor) {
-      $form['status'][$name] = array(
+    foreach ($all_processors as $processor_id => $processor) {
+      $clean_css_id = Html::cleanCssIdentifier($processor_id);
+      $form['status'][$processor_id] = array(
         '#type' => 'checkbox',
         '#title' => $processor->label(),
-        '#default_value' => $processors_settings[$name]['status'],
-        '#parents' => array('processors', $name, 'status'),
+        '#default_value' => !empty($processor_settings[$processor_id]),
         '#description' => $processor->getDescription(),
-      );
-    }
-
-    // Add a tabledrag-enabled table to re-order the processors. Rows for
-    // disabled processors are hidden with JS magic, but need to be included in
-    // case the processor is enabled.
-    $form['order'] = array(
-      '#type' => 'table',
-      '#header' => array($this->t('Processor'), $this->t('Weight')),
-      '#tabledrag' => array(
-        array(
-          'action' => 'order',
-          'relationship' => 'sibling',
-          'group' => 'search-api-processor-weight'
+        '#attributes' => array(
+          'class' => array(
+            'search-api-processor-status-' . $clean_css_id,
+          ),
+          'data-id' => $clean_css_id,
         ),
-      ),
+      );
+    }
+
+    $form['weights'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Processor order'),
     );
-
-    foreach ($processors_by_weight as $name => $processor) {
-      $form['order'][$name]['#attributes']['class'][] = 'draggable';
-      $form['order'][$name]['label'] = array(
-        '#markup' => String::checkPlain($processor->label()),
+    // Order enabled processors per stage.
+    foreach ($stages as $stage => $description) {
+      $form['weights'][$stage] = array (
+        '#type' => 'fieldset',
+        '#title' => $description['label'],
+        '#attributes' => array('class' => array(
+          'search-api-stage-wrapper',
+          'search-api-stage-wrapper-' . Html::cleanCssIdentifier($stage),
+        )),
       );
-
-      // This column is needed for tabledrag and will normally be hidden with
-      // Javascript (as long as tabledrag is working in the browser).
-      $form['order'][$name]['weight'] = array(
-        '#type' => 'weight',
-        '#title' => $this->t('Weight for processor %title', array('%title' => $processor->label())),
-        '#title_display' => 'invisible',
-        '#default_value' => $processors_settings[$name]['weight'],
-        '#parents' => array('processors', $name, 'weight'),
-        '#attributes' => array('class' => array('search-api-processor-weight')),
+      $form['weights'][$stage]['order'] = array(
+        '#type' => 'table',
       );
+      $form['weights'][$stage]['order']['#tabledrag'][] = array(
+        'action' => 'order',
+        'relationship' => 'sibling',
+        'group' => 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+      );
+    }
+    foreach ($processors_by_stage as $stage => $processors) {
+      /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
+      foreach ($processors as $processor_id => $processor) {
+        $weight = isset($processor_settings[$processor_id]['weights'][$stage])
+          ? $processor_settings[$processor_id]['weights'][$stage]
+          : $processor->getDefaultWeight($stage);
+        $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'draggable';
+        $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'search-api-processor-weight--' . Html::cleanCssIdentifier($processor_id);
+        $form['weights'][$stage]['order'][$processor_id]['#weight'] = $weight;
+        $form['weights'][$stage]['order'][$processor_id]['label'] = array(
+          '#markup' => String::checkPlain($processor->label()),
+        );
+        $form['weights'][$stage]['order'][$processor_id]['weight'] = array(
+          '#type' => 'weight',
+          '#title' => $this->t('Weight for processor %title', array('%title' => $processor->label())),
+          '#title_display' => 'invisible',
+          '#default_value' => $weight,
+          '#parents' => array('processors', $processor_id, 'weights', $stage),
+          '#attributes' => array('class' => array(
+            'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+          )),
+        );
+      }
     }
 
     // Add vertical tabs containing the settings for the processors. Tabs for
@@ -153,17 +183,20 @@ class IndexFiltersForm extends EntityForm {
       '#type' => 'vertical_tabs',
     );
 
-    foreach ($processors_by_weight as $name => $processor) {
-      $settings_form = $processor->buildConfigurationForm($form, $form_state);
-      if (!empty($settings_form)) {
-        $form['settings'][$name] = array(
+    foreach ($all_processors as $processor_id => $processor) {
+      $processor_form_state = new SubFormState($form_state, array('processors', $processor_id, 'settings'));
+      $processor_form = $processor->buildConfigurationForm($form, $processor_form_state);
+      if ($processor_form) {
+        $form['settings'][$processor_id] = array(
           '#type' => 'details',
           '#title' => $processor->label(),
           '#group' => 'processor_settings',
-          '#weight' => $processors_settings[$name]['weight'],
-          '#parents' => array('processors', $name, 'settings'),
+          '#parents' => array('processors', $processor_id, 'settings'),
+          '#attributes' => array('class' => array(
+            'search-api-processor-settings-' . Html::cleanCssIdentifier($processor_id),
+          )),
         );
-        $form['settings'][$name] += $settings_form;
+        $form['settings'][$processor_id] += $processor_form;
       }
     }
 
@@ -175,12 +208,14 @@ class IndexFiltersForm extends EntityForm {
    */
   public function validate(array $form, FormStateInterface $form_state) {
     $values = $form_state->getValues();
-    /** @var $processor \Drupal\search_api\Processor\ProcessorInterface */
-    $processors = $this->entity->getProcessors(TRUE, 'name');
-    foreach ($processors as $name => $processor) {
-      if (!empty($values['processors'][$name]['status']) && isset($values['processors'][$name]['settings'])) {
-        $processor_form_state = new SubFormState($form_state, array('processors', $name, 'settings'));
-        $processor->validateConfigurationForm($form['settings'][$name], $processor_form_state);
+    /** @var \Drupal\search_api\Processor\ProcessorInterface[] $processors */
+    $processors = $form_state->get('processors');
+
+    // Iterate over all processors that have a form and are enabled.
+    foreach ($form['settings'] as $processor_id => $processor_form) {
+      if (!empty($values['status'][$processor_id])) {
+        $processor_form_state = new SubFormState($form_state, array('processors', $processor_id, 'settings'));
+        $processors[$processor_id]->validateConfigurationForm($form['settings'][$processor_id], $processor_form_state);
       }
     }
 
@@ -192,54 +227,39 @@ class IndexFiltersForm extends EntityForm {
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $values = $form_state->getValues();
-    // Due to the "#parents" settings, these are all empty arrays.
-    unset($values['settings']);
-    unset($values['status']);
-    unset($values['order']);
-
-    $options = $this->entity->getOptions();
+    $new_settings = array();
 
     // Store processor settings.
     // @todo Go through all available processors, enable/disable with method on
     //   processor plugin to allow reaction.
     /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
     foreach ($form_state->get('processors') as $processor_id => $processor) {
-      if (!empty($values['processors'][$processor_id]['status'])) {
-        $processor_form = array();
-        if (isset($form['settings'][$processor_id])) {
-          $processor_form = &$form['settings'][$processor_id];
-        }
-        $default_settings = array(
-          'settings' => array(),
-          'processorPluginId' => $processor_id,
-        );
-        $values['processors'][$processor_id] += $default_settings;
-
-        $processor_form_state = new SubFormState($form_state, array(
-          'processors',
-          $processor_id,
-          'settings'
-        ));
-        $processor->submitConfigurationForm($processor_form, $processor_form_state);
-        $values['processors'][$processor_id]['settings'] = $processor->getConfiguration();
+      if (empty($values['status'][$processor_id])) {
+        continue;
       }
-      else {
-        // Settings of a sensor that is not enabled were not validated/
-        // processed, so we do not save them.
-        unset($values['processors'][$processor_id]);
+      $new_settings[$processor_id] = array(
+        'processor_id' => $processor_id,
+        'weights' => array(),
+        'settings' => array(),
+      );
+      $processor_values = $values['processors'][$processor_id];
+      if (!empty($processor_values['weights'])) {
+        $new_settings[$processor_id]['weights'] = $processor_values['weights'];
+      }
+      if (isset($form['settings'][$processor_id])) {
+        $processor_form_state = new SubFormState($form_state, array('processors', $processor_id, 'settings'));
+        $processor->submitConfigurationForm($form['settings'][$processor_id], $processor_form_state);
+        $new_settings[$processor_id]['settings'] = $processor->getConfiguration();
       }
     }
 
-
-    if (!isset($options['processors']) || $options['processors'] !== $values['processors']) {
-      // Save the already sorted arrays to avoid having to sort them at each
-      // use.
-      uasort($values['processors'], array('Drupal\Component\Utility\SortArray', 'sortByWeightElement'));
-      $this->entity->setOption('processors', $values['processors']);
-
+    // Sort the processors so we won't have unnecessary changes.
+    ksort($new_settings);
+    if (!$this->entity->getOption('processors', array()) !== $new_settings) {
+      $this->entity->setOption('processors', $new_settings);
       $this->entity->save();
       $this->entity->reindex();
-      drupal_set_message($this->t("The indexing workflow was successfully edited. All content was scheduled for reindexing so the new settings can take effect."));
+      drupal_set_message($this->t('The indexing workflow was successfully edited. All content was scheduled for reindexing so the new settings can take effect.'));
     }
     else {
       drupal_set_message($this->t('No values were changed.'));
diff --git a/src/Form/IndexForm.php b/src/Form/IndexForm.php
index a1810fe..f0c7005 100644
--- a/src/Form/IndexForm.php
+++ b/src/Form/IndexForm.php
@@ -366,6 +366,8 @@ class IndexForm extends EntityForm {
    */
   public function buildDatasourcesConfigForm(array &$form, FormStateInterface $form_state, IndexInterface $index) {
     foreach ($index->getDatasources() as $datasource_id => $datasource) {
+      // @todo Create, use and save SubFormState already here, not only in
+      //   validate(). Also, use proper subset of $form for first parameter?
       if ($config_form = $datasource->buildConfigurationForm(array(), $form_state)) {
         $form['datasource_configs'][$datasource_id]['#type'] = 'details';
         $form['datasource_configs'][$datasource_id]['#title'] = $this->t('Configure the %datasource datasource', array('%datasource' => $datasource->getPluginDefinition()['label']));
@@ -385,6 +387,8 @@ class IndexForm extends EntityForm {
   public function buildTrackerConfigForm(array &$form, FormStateInterface $form_state, IndexInterface $index) {
     if ($index->hasValidTracker()) {
       $tracker = $index->getTracker();
+      // @todo Create, use and save SubFormState already here, not only in
+      //   validate(). Also, use proper subset of $form for first parameter?
       if ($config_form = $tracker->buildConfigurationForm(array(), $form_state)) {
         $form['tracker_config']['#type'] = 'details';
         $form['tracker_config']['#title'] = $this->t('Configure %plugin', array('%plugin' => $tracker->label()));
diff --git a/src/Form/SubFormState.php b/src/Form/SubFormState.php
index f0cf032..cbe1b4a 100644
--- a/src/Form/SubFormState.php
+++ b/src/Form/SubFormState.php
@@ -89,6 +89,9 @@ class SubFormState implements FormStateInterface {
       $sub_state = array();
     }
     $this->internalStorage = &$this->applySubKeys($sub_state);
+    if (!isset($this->internalStorage)) {
+      $this->internalStorage = array();
+    }
     $this->values = &$this->applySubKeys($main_form_state->getValues());
     if (!is_array($this->values)) {
       $this->values = array();
diff --git a/src/IndexInterface.php b/src/IndexInterface.php
index a9278a5..6069e0e 100644
--- a/src/IndexInterface.php
+++ b/src/IndexInterface.php
@@ -243,18 +243,34 @@ interface IndexInterface extends ConfigEntityInterface {
   public function setServer(ServerInterface $server = NULL);
 
   /**
-   * Loads all enabled processors for this index in proper order.
+   * Loads this index's processors.
    *
-   * @param bool $all
-   *   Also include non-active processors
-   * @param string $sortBy
+   * @param bool $only_enabled
+   *   (optional) If FALSE, also include disabled processors. Otherwise, only
+   *   load enabled ones.
    *
    * @return \Drupal\search_api\Processor\ProcessorInterface[]
-   *   An array of all enabled (or available, if $all is TRUE) processors for
-   *   this index.
+   *   An array of all enabled (or available, if $only_enabled is FALSE)
+   *   processors for this index.
    */
-  // @todo Remove $sortBy.
-  public function getProcessors($all = FALSE, $sortBy = 'weight');
+  public function getProcessors($only_enabled = TRUE);
+
+  /**
+   * Loads this index's processors for a specific stage.
+   *
+   * @param string $stage
+   *   The stage for which to return the processors. One of the
+   *   \Drupal\search_api\Processor\ProcessorInterface::STAGE_* constants.
+   * @param bool $only_enabled
+   *   (optional) If FALSE, also include disabled processors. Otherwise, only
+   *   load enabled ones.
+   *
+   * @return \Drupal\search_api\Processor\ProcessorInterface[]
+   *   An array of all enabled (or available, if if $only_enabled is FALSE)
+   *   processors that support the given stage, ordered by the weight for that
+   *   stage.
+   */
+  public function getProcessorsByStage($stage, $only_enabled = TRUE);
 
   /**
    * Preprocesses data items for indexing.
diff --git a/src/Plugin/search_api/processor/AddURL.php b/src/Plugin/search_api/processor/AddURL.php
index 05756ed..cd6c55c 100644
--- a/src/Plugin/search_api/processor/AddURL.php
+++ b/src/Plugin/search_api/processor/AddURL.php
@@ -15,7 +15,10 @@ use Drupal\search_api\Processor\ProcessorPluginBase;
  * @SearchApiProcessor(
  *   id = "add_url",
  *   label = @Translation("URL field"),
- *   description = @Translation("Adds the item's URL to the indexed data.")
+ *   description = @Translation("Adds the item's URL to the indexed data."),
+ *   stages = {
+ *     "preprocess_index" = 0
+ *   }
  * )
  */
 class AddURL extends ProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/AggregatedFields.php b/src/Plugin/search_api/processor/AggregatedFields.php
index 7a5db5d..6f32323 100644
--- a/src/Plugin/search_api/processor/AggregatedFields.php
+++ b/src/Plugin/search_api/processor/AggregatedFields.php
@@ -17,7 +17,10 @@ use Drupal\search_api\Utility;
  * @SearchApiProcessor(
  *   id = "aggregated_field",
  *   label = @Translation("Aggregated fields"),
- *   description = @Translation("Add customized aggregations of existing fields to the index.")
+ *   description = @Translation("Add customized aggregations of existing fields to the index."),
+ *   stages = {
+ *     "preprocess_index" = 0
+ *   }
  * )
  */
 class AggregatedFields extends ProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/ContentAccess.php b/src/Plugin/search_api/processor/ContentAccess.php
index 5455736..6a2a94d 100644
--- a/src/Plugin/search_api/processor/ContentAccess.php
+++ b/src/Plugin/search_api/processor/ContentAccess.php
@@ -30,7 +30,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  * @SearchApiProcessor(
  *   id = "content_access",
  *   label = @Translation("Content access"),
- *   description = @Translation("Adds content access checks for nodes and comments.")
+ *   description = @Translation("Adds content access checks for nodes and comments."),
+ *   stages = {
+ *     "preprocess_index" = 0,
+ *     "preprocess_query" = 0
+ *   }
  * )
  */
 class ContentAccess extends ProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/Highlight.php b/src/Plugin/search_api/processor/Highlight.php
index 82ff328..017e598 100644
--- a/src/Plugin/search_api/processor/Highlight.php
+++ b/src/Plugin/search_api/processor/Highlight.php
@@ -21,7 +21,10 @@ use Drupal\search_api\Utility;
  * @SearchApiProcessor(
  *   id = "highlight",
  *   label = @Translation("Highlight"),
- *   description = @Translation("Adds a highlighted excerpt to results and highlights returned fields.")
+ *   description = @Translation("Adds a highlighted excerpt to results and highlights returned fields."),
+ *   stages = {
+ *     "postprocess_query" = 0
+ *   }
  * )
  */
 class Highlight extends ProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/HtmlFilter.php b/src/Plugin/search_api/processor/HtmlFilter.php
index 8b18363..2726793 100644
--- a/src/Plugin/search_api/processor/HtmlFilter.php
+++ b/src/Plugin/search_api/processor/HtmlFilter.php
@@ -20,7 +20,11 @@ use Symfony\Component\Yaml\Parser;
  * @SearchApiProcessor(
  *   id = "html_filter",
  *   label = @Translation("HTML filter"),
- *   description = @Translation("Strips HTML tags from fulltext fields and decodes HTML entities. Use this processor when indexing HTML data, e.g., node bodies for certain text formats. The processor also allows to boost (or ignore) the contents of specific elements.")
+ *   description = @Translation("Strips HTML tags from fulltext fields and decodes HTML entities. Use this processor when indexing HTML data, e.g., node bodies for certain text formats. The processor also allows to boost (or ignore) the contents of specific elements."),
+ *   stages = {
+ *     "preprocess_index" = 0,
+ *     "preprocess_query" = 0
+ *   }
  * )
  */
 class HtmlFilter extends FieldsProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/IgnoreCase.php b/src/Plugin/search_api/processor/IgnoreCase.php
index 91608e7..2117ad2 100644
--- a/src/Plugin/search_api/processor/IgnoreCase.php
+++ b/src/Plugin/search_api/processor/IgnoreCase.php
@@ -14,7 +14,11 @@ use Drupal\search_api\Processor\FieldsProcessorPluginBase;
  * @SearchApiProcessor(
  *   id = "ignorecase",
  *   label = @Translation("Ignore case"),
- *   description = @Translation("Makes searches case-insensitive on selected fields.")
+ *   description = @Translation("Makes searches case-insensitive on selected fields."),
+ *   stages = {
+ *     "preprocess_index" = 0,
+ *     "preprocess_query" = 0
+ *   }
  * )
  */
 class IgnoreCase extends FieldsProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/IgnoreCharacters.php b/src/Plugin/search_api/processor/IgnoreCharacters.php
index 44416b0..e1cf572 100644
--- a/src/Plugin/search_api/processor/IgnoreCharacters.php
+++ b/src/Plugin/search_api/processor/IgnoreCharacters.php
@@ -15,7 +15,11 @@ use Drupal\search_api\Processor\FieldsProcessorPluginBase;
  * @SearchApiProcessor(
  *   id = "ignore_character",
  *   label = @Translation("Ignore characters"),
- *   description = @Translation("Configure types of characters which should be ignored for searches.")
+ *   description = @Translation("Configure types of characters which should be ignored for searches."),
+ *   stages = {
+ *     "preprocess_index" = 0,
+ *     "preprocess_query" = 0
+ *   }
  * )
  */
 class IgnoreCharacters extends FieldsProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/Language.php b/src/Plugin/search_api/processor/Language.php
index a7be62b..4435281 100644
--- a/src/Plugin/search_api/processor/Language.php
+++ b/src/Plugin/search_api/processor/Language.php
@@ -18,7 +18,10 @@ use Drupal\search_api\Processor\ProcessorPluginBase;
  * @SearchApiProcessor(
  *   id = "language",
  *   label = @Translation("Language"),
- *   description = @Translation("Adds the item language to indexed items.")
+ *   description = @Translation("Adds the item language to indexed items."),
+ *   stages = {
+ *     "preprocess_index" = -50
+ *   }
  * )
  */
 class Language extends ProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/NodeStatus.php b/src/Plugin/search_api/processor/NodeStatus.php
index 2a6f825..ce1a6a4 100644
--- a/src/Plugin/search_api/processor/NodeStatus.php
+++ b/src/Plugin/search_api/processor/NodeStatus.php
@@ -15,7 +15,10 @@ use Drupal\search_api\Processor\ProcessorPluginBase;
  * @SearchApiProcessor(
  *   id = "node_status",
  *   label = @Translation("Node status"),
- *   description = @Translation("Exclude unpublished nodes from node indexes.")
+ *   description = @Translation("Exclude unpublished nodes from node indexes."),
+ *   stages = {
+ *     "preprocess_index" = 0
+ *   }
  * )
  */
 class NodeStatus extends ProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/RenderedItem.php b/src/Plugin/search_api/processor/RenderedItem.php
index fbbd570..f5c715c 100644
--- a/src/Plugin/search_api/processor/RenderedItem.php
+++ b/src/Plugin/search_api/processor/RenderedItem.php
@@ -19,8 +19,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  * @SearchApiProcessor(
  *   id = "rendered_item",
  *   label = @Translation("Rendered item"),
- *   description = @Translation("Adds an additional field containing the rendered item as it would look when viewed.")
- * );
+ *   description = @Translation("Adds an additional field containing the rendered item as it would look when viewed."),
+ *   stages = {
+ *     "preprocess_index" = 0
+ *   }
+ * )
  */
 class RenderedItem extends ProcessorPluginBase {
 
diff --git a/src/Plugin/search_api/processor/RoleFilter.php b/src/Plugin/search_api/processor/RoleFilter.php
index 33eed7f..71ce6b7 100644
--- a/src/Plugin/search_api/processor/RoleFilter.php
+++ b/src/Plugin/search_api/processor/RoleFilter.php
@@ -18,7 +18,10 @@ use Drupal\user\UserInterface;
  * @SearchApiProcessor(
  *   id = "role_filter",
  *   label = @Translation("Role filter"),
- *   description = @Translation("Filters out users based on their role.")
+ *   description = @Translation("Filters out users based on their role."),
+ *   stages = {
+ *     "preprocess_index" = 0
+ *   }
  * )
  */
 class RoleFilter extends ProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/Stopwords.php b/src/Plugin/search_api/processor/Stopwords.php
index 5272bfb..a9a2ec4 100644
--- a/src/Plugin/search_api/processor/Stopwords.php
+++ b/src/Plugin/search_api/processor/Stopwords.php
@@ -17,7 +17,11 @@ use Drupal\search_api\Utility;
  * @SearchApiProcessor(
  *   id = "stopwords",
  *   label = @Translation("Stopwords"),
- *   description = @Translation("Allows you to define stopwords which will be ignored in searches. <strong>Caution:</strong> Only use after both 'Ignore case' and 'Tokenizer' have run.")
+ *   description = @Translation("Allows you to define stopwords which will be ignored in searches. <strong>Caution:</strong> Only use after both 'Ignore case' and 'Tokenizer' have run."),
+ *   stages = {
+ *     "preprocess_query" = -5,
+ *     "postprocess_query" = 5
+ *   }
  * )
  */
 class Stopwords extends FieldsProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/Tokenizer.php b/src/Plugin/search_api/processor/Tokenizer.php
index fdba8ee..0d56d28 100644
--- a/src/Plugin/search_api/processor/Tokenizer.php
+++ b/src/Plugin/search_api/processor/Tokenizer.php
@@ -17,7 +17,11 @@ use Drupal\search_api\Utility;
  * @SearchApiProcessor(
  *   id = "tokenizer",
  *   label = @Translation("Tokenizer processor"),
- *   description = @Translation("Splits text into individual words for searching.")
+ *   description = @Translation("Splits text into individual words for searching."),
+ *   stages = {
+ *     "preprocess_index" = 0,
+ *     "preprocess_query" = 0
+ *   }
  * )
  */
 class Tokenizer extends FieldsProcessorPluginBase {
diff --git a/src/Plugin/search_api/processor/Transliteration.php b/src/Plugin/search_api/processor/Transliteration.php
index d5abcbd..ef0c356 100644
--- a/src/Plugin/search_api/processor/Transliteration.php
+++ b/src/Plugin/search_api/processor/Transliteration.php
@@ -14,7 +14,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  * @SearchApiProcessor(
  *   id = "transliteration",
  *   label = @Translation("Transliteration"),
- *   description = @Translation("Makes searches insensitive to accents and other non-ASCII characters.")
+ *   description = @Translation("Makes searches insensitive to accents and other non-ASCII characters."),
+ *   stages = {
+ *     "preprocess_index" = 0,
+ *     "preprocess_query" = 0
+ *   }
  * )
  */
 class Transliteration extends FieldsProcessorPluginBase {
diff --git a/src/Processor/FieldsProcessorPluginBase.php b/src/Processor/FieldsProcessorPluginBase.php
index f5371f4..88408d2 100644
--- a/src/Processor/FieldsProcessorPluginBase.php
+++ b/src/Processor/FieldsProcessorPluginBase.php
@@ -78,7 +78,7 @@ abstract class FieldsProcessorPluginBase extends ProcessorPluginBase {
 
     $fields = array_filter($form_state->getValues()['fields']);
     if ($fields) {
-      $fields = array_fill_keys($fields, TRUE);
+      $fields = array_keys($fields);
     }
     $form_state->setValue('fields', $fields);
   }
diff --git a/src/Processor/ProcessorInterface.php b/src/Processor/ProcessorInterface.php
index d805daf..ec65ca4 100644
--- a/src/Processor/ProcessorInterface.php
+++ b/src/Processor/ProcessorInterface.php
@@ -21,10 +21,30 @@ use Drupal\search_api\Query\ResultSetInterface;
  * the other method(s) should simply be left blank. A processor should make it
  * clear in its description or documentation when it will run and what effect it
  * will have.
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiProcessor
+ * @see \Drupal\search_api\Processor\ProcessorPluginManager
+ * @see \Drupal\search_api\Processor\ProcessorPluginBase
+ * @see plugin_api
  */
 interface ProcessorInterface extends IndexPluginInterface {
 
   /**
+   * Processing stage: preprocess index.
+   */
+  const STAGE_PREPROCESS_INDEX = 'preprocess_index';
+
+  /**
+   * Processing stage: preprocess query.
+   */
+  const STAGE_PREPROCESS_QUERY = 'preprocess_query';
+
+  /**
+   * Processing stage: postprocess query.
+   */
+  const STAGE_POSTPROCESS_QUERY = 'postprocess_query';
+
+  /**
    * Checks whether this processor is applicable for a certain index.
    *
    * This can be used for hiding the processor on the index's "Filters" tab. To
@@ -43,6 +63,37 @@ interface ProcessorInterface extends IndexPluginInterface {
   public static function supportsIndex(IndexInterface $index);
 
   /**
+   * Checks whether this processor implements a particular stage.
+   *
+   * @param string $stage_identifier
+   *   The stage to check: self::STAGE_PREPROCESS_INDEX,
+   *   self::STAGE_PREPROCESS_QUERY
+   *   or self::STAGE_POSTPROCESS_QUERY.
+   *
+   * @return bool
+   *   TRUE if the processor runs on a particular stage; FALSE otherwise.
+   */
+  public function supportsStage($stage_identifier);
+
+  /**
+   * Returns the default weight for a specific processing stage.
+   *
+   * Some processors should ensure they run earlier or later in a particular
+   * stage. Processors with lower weights are run earlier. The default value is
+   * used when the processor is first enabled. It can then be changed through
+   * reordering by the user.
+   *
+   * @param string $stage
+   *   The stage whose default weight should be returned. See
+   *   \Drupal\search_api\Processor\ProcessorPluginManager::getProcessingStages()
+   *   for the valid values.
+   *
+   * @return int
+   *   The default weight for the given stage.
+   */
+  public function getDefaultWeight($stage);
+
+  /**
    * Alters the given datasource's property definitions.
    *
    * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
diff --git a/src/Processor/ProcessorPluginBase.php b/src/Processor/ProcessorPluginBase.php
index 59b5737..4a5ea28 100644
--- a/src/Processor/ProcessorPluginBase.php
+++ b/src/Processor/ProcessorPluginBase.php
@@ -30,9 +30,19 @@ use Drupal\search_api\Query\ResultSetInterface;
  * @SearchApiProcessor(
  *   id = "my_processor",
  *   label = @Translation("My Processor"),
- *   description = @Translation("Does … something.")
+ *   description = @Translation("Does … something."),
+ *   stages = {
+ *     "preprocess_index" = 0,
+ *     "preprocess_query" = 0,
+ *     "postprocess_query" = 0
+ *   }
  * )
  * @endcode
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiProcessor
+ * @see \Drupal\search_api\Processor\ProcessorPluginManager
+ * @see \Drupal\search_api\Processor\ProcessorInterface
+ * @see plugin_api
  */
 abstract class ProcessorPluginBase extends IndexPluginBase implements ProcessorInterface {
 
@@ -46,6 +56,22 @@ abstract class ProcessorPluginBase extends IndexPluginBase implements ProcessorI
   /**
    * {@inheritdoc}
    */
+  public function supportsStage($stage_identifier) {
+    $plugin_definition = $this->getPluginDefinition();
+    return isset($plugin_definition['stages'][$stage_identifier]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultWeight($stage) {
+    $plugin_definition = $this->getPluginDefinition();
+    return isset($plugin_definition['stages'][$stage]) ? (int) $plugin_definition['stages'][$stage] : 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function alterPropertyDefinitions(array &$properties, DatasourceInterface $datasource = NULL) {}
 
   /**
diff --git a/src/Processor/ProcessorPluginManager.php b/src/Processor/ProcessorPluginManager.php
index 6d70891..abb16cf 100644
--- a/src/Processor/ProcessorPluginManager.php
+++ b/src/Processor/ProcessorPluginManager.php
@@ -10,6 +10,8 @@ namespace Drupal\search_api\Processor;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
 
 /**
  * Manages processor plugins.
@@ -21,6 +23,8 @@ use Drupal\Core\Plugin\DefaultPluginManager;
  */
 class ProcessorPluginManager extends DefaultPluginManager {
 
+  use StringTranslationTrait;
+
   /**
    * Constructs a ProcessorPluginManager object.
    *
@@ -31,11 +35,40 @@ class ProcessorPluginManager extends DefaultPluginManager {
    *   Cache backend instance to use.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
+   *   The string translation manager.
    */
-  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, TranslationInterface $translation) {
     parent::__construct('Plugin/search_api/processor', $namespaces, $module_handler, 'Drupal\search_api\Processor\ProcessorInterface', 'Drupal\search_api\Annotation\SearchApiProcessor');
     $this->setCacheBackend($cache_backend, 'search_api_processors');
     $this->alterInfo('search_api_processor_info');
+    $this->setStringTranslation($translation);
+  }
+
+  /**
+   * Retrieves information about the available processing stages.
+   *
+   * These are then used by processors in their "stages" definition to specify
+   * in which stages they will run.
+   *
+   * @return array
+   *   An associative array mapping stage identifiers to information about that
+   *   stage. The information itself is an associative array with the following
+   *   keys:
+   *   - label: The translated label for this stage.
+   */
+  public function getProcessingStages() {
+    return array(
+      ProcessorInterface::STAGE_PREPROCESS_INDEX => array(
+        'label' => $this->t('Preprocess index'),
+      ),
+      ProcessorInterface::STAGE_PREPROCESS_QUERY => array(
+        'label' => $this->t('Preprocess query'),
+      ),
+      ProcessorInterface::STAGE_POSTPROCESS_QUERY => array(
+        'label' => $this->t('Postprocess query'),
+      ),
+    );
   }
 
 }
diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php
index 087bd65..9264468 100644
--- a/src/Tests/IntegrationTest.php
+++ b/src/Tests/IntegrationTest.php
@@ -369,7 +369,7 @@ class IntegrationTest extends WebTestBase {
    */
   protected function addFilter() {
     $edit = array(
-      'processors[ignorecase][status]' => 1,
+      'status[ignorecase]' => 1,
     );
     $this->drupalPostForm($this->getIndexPath('filters'), $edit, $this->t('Save'));
     /** @var \Drupal\search_api\IndexInterface $index */
@@ -383,7 +383,7 @@ class IntegrationTest extends WebTestBase {
    */
   protected function configureFilter() {
     $edit = array(
-      'processors[ignorecase][status]' => 1,
+      'status[ignorecase]' => 1,
       'processors[ignorecase][settings][fields][search_api_language]' => FALSE,
       'processors[ignorecase][settings][fields][entity:node/title]' => 'entity:node/title',
     );
diff --git a/src/Tests/Processor/ProcessorIntegrationTest.php b/src/Tests/Processor/ProcessorIntegrationTest.php
index 468cf01..893ddc1 100644
--- a/src/Tests/Processor/ProcessorIntegrationTest.php
+++ b/src/Tests/Processor/ProcessorIntegrationTest.php
@@ -94,7 +94,7 @@ class ProcessorIntegrationTest extends WebTestBase {
     // settings, since we otherwise run into a weird bug only present in the
     // testing environment regarding the YAML in the "tags" setting.
     $edit = array(
-      'processors[html_filter][status]' => 1,
+      'status[html_filter]' => 1,
       'processors[html_filter][settings][fields][search_api_language]' => FALSE,
       'processors[html_filter][settings][title]' => FALSE,
       'processors[html_filter][settings][alt]' => FALSE,
@@ -208,15 +208,14 @@ class ProcessorIntegrationTest extends WebTestBase {
    *   The ID of the processor to enable.
    */
   protected function enableProcessor($processor_id) {
-    // Go to the index's "Filters" tab.
-    $settings_path = 'admin/config/search/search-api/index/' . $this->indexId . '/filters';
+    $this->loadFiltersTab();
 
     $edit = array(
-      "processors[$processor_id][status]" => 1,
+      "status[$processor_id]" => 1,
     );
-    $this->drupalPostForm($settings_path, $edit, $this->t('Save'));
+    $this->drupalPostForm(NULL, $edit, $this->t('Save'));
     $processors = $this->loadIndex()->getProcessors();
-    $this->assertTrue(isset($processors[$processor_id]), "Successfully enabled the '$processor_id' processor.'");
+    $this->assertTrue(!empty($processors[$processor_id]), "Successfully enabled the '$processor_id' processor.'");
   }
 
   /**
@@ -228,9 +227,9 @@ class ProcessorIntegrationTest extends WebTestBase {
    *   The ID of the processor whose settings are edited.
    */
   protected function editSettingsForm($edit, $processor_id) {
-    $settings_path = 'admin/config/search/search-api/index/' . $this->indexId . '/filters';
+    $this->loadFiltersTab();
 
-    $this->drupalPostForm($settings_path, $edit, $this->t('Save'));
+    $this->drupalPostForm(NULL, $edit, $this->t('Save'));
 
     $processors = $this->loadIndex()->getProcessors();
     // @todo Actually test something here. Idea: pass in a $configuration array,
@@ -246,6 +245,16 @@ class ProcessorIntegrationTest extends WebTestBase {
   }
 
   /**
+   * Loads the test index's "Filters" tab in the test browser, if necessary.
+   */
+  protected function loadFiltersTab() {
+    $settings_path = 'admin/config/search/search-api/index/' . $this->indexId . '/filters';
+    if ($this->getAbsoluteUrl($settings_path) != $this->getUrl()) {
+      $this->drupalGet($settings_path);
+    }
+  }
+
+  /**
    * Loads the search index used by this test.
    *
    * @return \Drupal\search_api\IndexInterface
diff --git a/tests/src/Plugin/Processor/FieldsProcessorPluginBaseTest.php b/tests/src/Plugin/Processor/FieldsProcessorPluginBaseTest.php
index b270fc1..17935e4 100644
--- a/tests/src/Plugin/Processor/FieldsProcessorPluginBaseTest.php
+++ b/tests/src/Plugin/Processor/FieldsProcessorPluginBaseTest.php
@@ -270,7 +270,7 @@ class FieldsProcessorPluginBaseTest extends UnitTestCase {
     $this->processor->setMethodOverride('processKey', $override);
 
     $query = Utility::createQuery($this->index);
-    $keys = & $query->getKeys();
+    $keys = &$query->getKeys();
     $keys = array(
       '#conjunction' => 'OR',
       'foo',
