diff --git a/search_api.api.php b/search_api.api.php
index 87e25ce..7f3d43b 100644
--- a/search_api.api.php
+++ b/search_api.api.php
@@ -147,16 +147,44 @@ function hook_search_api_items_indexed(\Drupal\search_api\IndexInterface $index,
  *   The query that will be executed.
  */
 function hook_search_api_query_alter(\Drupal\search_api\Query\QueryInterface &$query) {
-  // Exclude entities with ID 0. (Assume the ID field is always indexed.)
-  $types = $query->getIndex()->getDatasourceIds();
-  foreach ($types as $type) {
-    if (strpos($type, ':')) {
-      list(, $type) = explode(':', $type);
+  // Do not run for queries with a certain tag.
+  if ($query->hasTag('example_tag')) {
+    return;
+  }
+  // Otherwise, exclude all nodes with NID 10.
+  $fields = $query->getIndex()->getFields();
+  foreach ($query->getIndex()->getDatasources() as $datasource_id => $datasource) {
+    if ($datasource->getEntityTypeId() == 'node') {
+      $field = \Drupal\search_api\Utility::createCombinedId($datasource_id, 'nid');
+      if (isset($fields[$field])) {
+        $query->addCondition($field, 10, '<>');
+      }
     }
-    $definition = \Drupal::entityManager()->getDefinition($type, FALSE);
-    if ($definition) {
-      $keys = $definition->getKeys();
-      $query->condition($keys['id'], 0, '<>');
+  }
+}
+
+/**
+ * Alter a search query with a specific tag before it gets executed.
+ *
+ * The hook is invoked after all enabled processors have preprocessed the query.
+ *
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ *   The query that will be executed.
+ */
+function hook_search_api_query_TAG_alter(\Drupal\search_api\Query\QueryInterface &$query) {
+  // Do not run for queries with a certain tag.
+  if ($query->hasTag('example_tag')) {
+    return;
+  }
+  // Otherwise, exclude all nodes with NID 10.
+  $fields = $query->getIndex()->getFields();
+  foreach ($query->getIndex()
+    ->getDatasources() as $datasource_id => $datasource) {
+    if ($datasource->getEntityTypeId() == 'node') {
+      $field = \Drupal\search_api\Utility::createCombinedId($datasource_id, 'nid');
+      if (isset($fields[$field])) {
+        $query->addCondition($field, 10, '<>');
+      }
     }
   }
 }
@@ -175,6 +203,19 @@ function hook_search_api_results_alter(\Drupal\search_api\Query\ResultSetInterfa
 }
 
 /**
+ * Alter the result set of a search query with a specific tag.
+ *
+ * The hook is invoked after all enabled processors have postprocessed the
+ * results.
+ *
+ * @param \Drupal\search_api\Query\ResultSetInterface $results
+ *   The search results to alter.
+ */
+function hook_search_api_results_TAG_alter(\Drupal\search_api\Query\ResultSetInterface &$results) {
+  $results->setExtraData('example_hook_invoked', microtime(TRUE));
+}
+
+/**
  * React when a search index was scheduled for reindexing
  *
  * @param \Drupal\search_api\IndexInterface $index
@@ -193,26 +234,5 @@ function hook_search_api_index_reindex(\Drupal\search_api\IndexInterface $index,
 }
 
 /**
- * Alter the query before executing the query.
- *
- * @param \Drupal\views\ViewExecutable $view
- *   The view object about to be processed.
- * @param \Drupal\search_api\Query\QueryInterface $query
- *   The Search API Views query to be altered.
- *
- * @deprecated Use hook_views_query_alter() instead.
- *
- * @see hook_views_query_alter()
- *
- * @todo Possibly remove this, since hook_views_query_alter() works just as well
- *   (with instanceof check and $query->getSearchApiQuery()).
- */
-function hook_search_api_views_query_alter(\Drupal\views\ViewExecutable $view, \Drupal\search_api\Query\QueryInterface &$query) {
-  if ($view->getPath() === 'search') {
-    $query->setOption('custom_do_magic', TRUE);
-  }
-}
-
-/**
  * @} End of "addtogroup hooks".
  */
diff --git a/search_api_db/src/Plugin/search_api/backend/Database.php b/search_api_db/src/Plugin/search_api/backend/Database.php
index ae79152..1e5d5c0 100644
--- a/search_api_db/src/Plugin/search_api/backend/Database.php
+++ b/search_api_db/src/Plugin/search_api/backend/Database.php
@@ -24,7 +24,7 @@ use Drupal\search_api\Entity\Index;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\Item\FieldInterface;
 use Drupal\search_api\Item\ItemInterface;
-use Drupal\search_api\Query\FilterInterface;
+use Drupal\search_api\Query\ConditionGroupInterface;
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\SearchApiException;
 use Drupal\search_api\Utility;
@@ -1489,9 +1489,9 @@ class Database extends BackendPluginBase {
       $db_query->distinct();
     }
 
-    $filter = $query->getFilter();
-    if ($filter->getFilters()) {
-      $condition = $this->createFilterCondition($filter, $fields, $db_query, $query->getIndex());
+    $condition_group = $query->getConditionGroup();
+    if ($condition_group->getConditions()) {
+      $condition = $this->createDbCondition($condition_group, $fields, $db_query, $query->getIndex());
       if ($condition) {
         $db_query->condition($condition);
       }
@@ -1507,7 +1507,7 @@ class Database extends BackendPluginBase {
   /**
    * Removes nested expressions and phrase groupings from the search keys.
    *
-   * Used as a helper method in createDbQuery() and createFilterCondition().
+   * Used as a helper method in createDbQuery() and createDbCondition().
    *
    * @param array|string|null $keys
    *   The keys which should be preprocessed.
@@ -1626,7 +1626,7 @@ class Database extends BackendPluginBase {
   /**
    * Creates a SELECT query for given search keys.
    *
-   * Used as a helper method in createDbQuery() and createFilterCondition().
+   * Used as a helper method in createDbQuery() and createDbCondition().
    *
    * @param string|array $keys
    *   The search keys, formatted like the return value of
@@ -1793,8 +1793,8 @@ class Database extends BackendPluginBase {
    *
    * Used as a helper method in createDbQuery().
    *
-   * @param \Drupal\search_api\Query\FilterInterface $filter
-   *   The filter for which a condition should be created.
+   * @param \Drupal\search_api\Query\ConditionGroupInterface $conditions
+   *   The conditions for which a condition should be created.
    * @param array $fields
    *   Internal information about the index's fields.
    * @param \Drupal\Core\Database\Query\SelectInterface $db_query
@@ -1808,8 +1808,8 @@ class Database extends BackendPluginBase {
    * @throws \Drupal\search_api\SearchApiException
    *   Thrown if an unknown field was used in the filter.
    */
-  protected function createFilterCondition(FilterInterface $filter, array $fields, SelectInterface $db_query, IndexInterface $index) {
-    $cond =  new Condition($filter->getConjunction());
+  protected function createDbCondition(ConditionGroupInterface $conditions, array $fields, SelectInterface $db_query, IndexInterface $index) {
+    $cond =  new Condition($conditions->getConjunction());
     $db_info = $this->getIndexDbInfo($index);
 
     $empty = TRUE;
@@ -1818,9 +1818,9 @@ class Database extends BackendPluginBase {
     $first_join = array();
     // Store the table aliases for the fields in this condition group.
     $tables = array();
-    foreach ($filter->getFilters() as $f) {
-      if (is_object($f)) {
-        $c = $this->createFilterCondition($f, $fields, $db_query, $index);
+    foreach ($conditions->getConditions() as $condition) {
+      if ($condition instanceof ConditionGroupInterface) {
+        $c = $this->createDbCondition($condition, $fields, $db_query, $index);
         if ($c) {
           $empty = FALSE;
           $cond->condition($c);
@@ -1828,33 +1828,37 @@ class Database extends BackendPluginBase {
       }
       else {
         $empty = FALSE;
+        $field = $condition->getField();
+        $operator = $condition->getOperator();
+        $value = $condition->getValue();
+
         // We don't index the datasource explicitly, so this needs a bit of
         // magic.
         // @todo Index the datasource explicitly so this doesn't need magic.
-        if ($f[0] === 'search_api_datasource') {
+        if ($field === 'search_api_datasource') {
           $alias = $this->getTableAlias(array('table' => $db_info['index_table']), $db_query);
           // @todo Stop recognizing "!=" as an operator.
-          $operator = ($f[2] == '<>' || $f[2] == '!=') ? 'NOT LIKE' : 'LIKE';
-          $prefix = Utility::createCombinedId($f[1], '');
+          $operator = ($operator == '<>' || $operator == '!=') ? 'NOT LIKE' : 'LIKE';
+          $prefix = Utility::createCombinedId($value, '');
           $cond->condition($alias . '.item_id', $this->database->escapeLike($prefix) . '%', $operator);
           continue;
         }
-        if (!isset($fields[$f[0]])) {
-          throw new SearchApiException(new FormattableMarkup('Unknown field in filter clause: @field.', array('@field' => $f[0])));
+        if (!isset($fields[$field])) {
+          throw new SearchApiException(new FormattableMarkup('Unknown field in filter clause: @field.', array('@field' => $field)));
         }
-        $field = $fields[$f[0]];
+        $field_info = $fields[$field];
         // Fields have their own table, so we have to check for NULL values in
         // a special way (i.e., check for missing entries in that table).
         // @todo This can probably always use the denormalized table.
-        if ($f[1] === NULL) {
-          $query = $this->database->select($field['table'], 't')
+        if ($value === NULL) {
+          $query = $this->database->select($field_info['table'], 't')
             ->fields('t', array('item_id'));
-          $cond->condition('t.item_id', $query, $f[2] == '<>' || $f[2] == '!=' ? 'IN' : 'NOT IN');
+          $cond->condition('t.item_id', $query, $operator == '<>' || $operator == '!=' ? 'IN' : 'NOT IN');
           continue;
         }
-        if (Utility::isTextType($field['type'])) {
-          $keys = $this->prepareKeys($f[1]);
-          $query = $this->createKeysQuery($keys, array($f[0] => $field), $fields, $index);
+        if (Utility::isTextType($field_info['type'])) {
+          $keys = $this->prepareKeys($value);
+          $query = $this->createKeysQuery($keys, array($field => $field_info), $fields, $index);
           // We don't need the score, so we remove it. The score might either be
           // an expression or a field.
           $query_expressions = &$query->getExpressions();
@@ -1867,20 +1871,20 @@ class Database extends BackendPluginBase {
             unset($query_fields);
           }
           unset($query_expressions);
-          $cond->condition('t.item_id', $query, $f[2] == '<>' || $f[2] == '!=' ? 'NOT IN' : 'IN');
+          $cond->condition('t.item_id', $query, $operator == '<>' || $operator == '!=' ? 'NOT IN' : 'IN');
         }
         else {
-          $new_join = ($filter->getConjunction() == 'AND' || empty($first_join[$f[0]]));
-          if ($new_join || empty($tables[$f[0]])) {
-            $tables[$f[0]] = $this->getTableAlias($field, $db_query, $new_join);
-            $first_join[$f[0]] = TRUE;
+          $new_join = ($conditions->getConjunction() == 'AND' || empty($first_join[$field]));
+          if ($new_join || empty($tables[$field])) {
+            $tables[$field] = $this->getTableAlias($field_info, $db_query, $new_join);
+            $first_join[$field] = TRUE;
           }
-          $column = $tables[$f[0]] . '.' . 'value';
-          if ($f[1] !== NULL) {
-            $cond->condition($column, $f[1], $f[2]);
+          $column = $tables[$field] . '.' . 'value';
+          if ($value !== NULL) {
+            $cond->condition($column, $value, $operator);
           }
           else {
-            $method = ($f[2] == '=') ? 'isNull' : 'isNotNull';
+            $method = ($operator == '=') ? 'isNull' : 'isNotNull';
             $cond->$method($column);
           }
         }
@@ -1931,6 +1935,9 @@ class Database extends BackendPluginBase {
    *   An array of facets, as specified by the search_api_facets feature.
    */
   protected function getFacets(QueryInterface $query, SelectInterface $db_query) {
+    // @todo This is not actually necessary for OR facets. So, only do this if
+    //   there are actually AND facets, and do not return right away if this
+    //   fails.
     $table = $this->getTemporaryResultsTable($db_query);
     if (!$table) {
       return array();
@@ -1954,11 +1961,11 @@ class Database extends BackendPluginBase {
         // For OR facets, we need to build a different base query that excludes
         // the facet filters applied to the facet.
         $or_query = clone $query;
-        $filters = &$or_query->getFilter()->getFilters();
+        $conditions = &$or_query->getConditionGroup()->getConditions();
         $tag = 'facet:' . $facet['field'];
-        foreach ($filters as $filter_id => $filter) {
-          if ($filter instanceof FilterInterface && $filter->hasTag($tag)) {
-            unset($filters[$filter_id]);
+        foreach ($conditions as $i => $condition) {
+          if ($condition instanceof ConditionGroupInterface && $condition->hasTag($tag)) {
+            unset($conditions[$i]);
           }
         }
         $or_db_query = $this->createDbQuery($or_query, $fields);
diff --git a/search_api_db/src/Tests/BackendTest.php b/search_api_db/src/Tests/BackendTest.php
index 310bb62..c80448d 100644
--- a/search_api_db/src/Tests/BackendTest.php
+++ b/search_api_db/src/Tests/BackendTest.php
@@ -203,7 +203,7 @@ class BackendTest extends EntityUnitTestBase {
    *
    * @param string|array|null $keys
    *   (optional) The search keys to set, if any.
-   * @param array $filters
+   * @param array $conditions
    *   (optional) Filters to set on the query, in the format "field,value".
    * @param array $fields
    *   (optional) Fulltext fields to search for the keys.
@@ -211,7 +211,7 @@ class BackendTest extends EntityUnitTestBase {
    * @return \Drupal\search_api\Query\QueryInterface
    *   A search query on the test index.
    */
-  protected function buildSearch($keys = NULL, array $filters = array(), array $fields = array()) {
+  protected function buildSearch($keys = NULL, array $conditions = array(), array $fields = array()) {
     $query = $this->getIndex()->query();
     if ($keys) {
       $query->keys($keys);
@@ -219,9 +219,9 @@ class BackendTest extends EntityUnitTestBase {
         $query->setFulltextFields($fields);
       }
     }
-    foreach ($filters as $filter) {
-      list($field, $value) = explode(',', $filter, 2);
-      $query->condition($this->getFieldId($field), $value);
+    foreach ($conditions as $condition) {
+      list($field, $value) = explode(',', $condition, 2);
+      $query->addCondition($this->getFieldId($field), $value);
     }
     $query->range(0, 10);
 
@@ -295,9 +295,9 @@ class BackendTest extends EntityUnitTestBase {
    */
   protected function checkFacets() {
     $query = $this->buildSearch();
-    $filter = $query->createFilter('OR', array('facet:' . $this->getFieldId('category')));
-    $filter->condition($this->getFieldId('category'), 'article_category');
-    $query->filter($filter);
+    $conditions = $query->createConditionGroup('OR', array('facet:' . $this->getFieldId('category')));
+    $conditions->addCondition($this->getFieldId('category'), 'article_category');
+    $query->addConditionGroup($conditions);
     $facets['category'] = array(
       'field' => $this->getFieldId('category'),
       'limit' => 0,
@@ -319,12 +319,12 @@ class BackendTest extends EntityUnitTestBase {
     $this->assertEqual($expected, $category_facets, 'Correct OR facets were returned');
 
     $query = $this->buildSearch();
-    $filter = $query->createFilter('OR', array('facet:' . $this->getFieldId('category')));
-    $filter->condition($this->getFieldId('category'), 'article_category');
-    $query->filter($filter);
-    $filter = $query->createFilter('AND');
-    $filter->condition($this->getFieldId('category'), NULL, '<>');
-    $query->filter($filter);
+    $conditions = $query->createConditionGroup('OR', array('facet:' . $this->getFieldId('category')));
+    $conditions->addCondition($this->getFieldId('category'), 'article_category');
+    $query->addConditionGroup($conditions);
+    $conditions = $query->createConditionGroup('AND');
+    $conditions->addCondition($this->getFieldId('category'), NULL, '<>');
+    $query->addConditionGroup($conditions);
     $facets['category'] = array(
       'field' => $this->getFieldId('category'),
       'limit' => 0,
@@ -439,17 +439,17 @@ class BackendTest extends EntityUnitTestBase {
     $this->assertIgnored($results);
     $this->assertWarnings($results);
 
-    $filters = array(
+    $conditions = array(
       'keywords,orange',
       'keywords,apple',
     );
-    $results = $this->buildSearch(NULL, $filters)->execute();
+    $results = $this->buildSearch(NULL, $conditions)->execute();
     $this->assertEqual($results->getResultCount(), 1, 'Filter query 2 on multi-valued field returned correct number of results.');
     $this->assertEqual(array_keys($results->getResultItems()), $this->getItemIds(array(2)), 'Filter query 2 on multi-valued field returned correct result.');
     $this->assertIgnored($results);
     $this->assertWarnings($results);
 
-    $results = $this->buildSearch()->condition($this->getFieldId('keywords'), NULL)->execute();
+    $results = $this->buildSearch()->addCondition($this->getFieldId('keywords'), NULL)->execute();
     $this->assertEqual($results->getResultCount(), 1, 'Query with NULL filter returned correct number of results.');
     $this->assertEqual(array_keys($results->getResultItems()), $this->getItemIds(array(3)), 'Query with NULL filter returned correct result.');
     $this->assertIgnored($results);
@@ -468,10 +468,10 @@ class BackendTest extends EntityUnitTestBase {
     $this->assertWarnings($results);
 
     $query = $this->buildSearch();
-    $filter = $query->createFilter('OR');
-    $filter->condition($this->getFieldId('id'), 3);
-    $filter->condition($this->getFieldId('type'), 'article');
-    $query->filter($filter);
+    $conditions = $query->createConditionGroup('OR');
+    $conditions->addCondition($this->getFieldId('id'), 3);
+    $conditions->addCondition($this->getFieldId('type'), 'article');
+    $query->addConditionGroup($conditions);
     $query->sort($this->getFieldId('id'), QueryInterface::SORT_ASC);
     $results = $query->execute();
     $this->assertEqual($results->getResultCount(), 3, 'OR filter on field with NULLs returned correct number of results.');
@@ -481,10 +481,10 @@ class BackendTest extends EntityUnitTestBase {
 
     // Regression tests for #1863672.
     $query = $this->buildSearch();
-    $filter = $query->createFilter('OR');
-    $filter->condition($this->getFieldId('keywords'), 'orange');
-    $filter->condition($this->getFieldId('keywords'), 'apple');
-    $query->filter($filter);
+    $conditions = $query->createConditionGroup('OR');
+    $conditions->addCondition($this->getFieldId('keywords'), 'orange');
+    $conditions->addCondition($this->getFieldId('keywords'), 'apple');
+    $query->addConditionGroup($conditions);
     $query->sort($this->getFieldId('id'), QueryInterface::SORT_ASC);
     $results = $query->execute();
     $this->assertEqual($results->getResultCount(), 4, 'OR filter on multi-valued field returned correct number of results.');
@@ -493,14 +493,14 @@ class BackendTest extends EntityUnitTestBase {
     $this->assertWarnings($results);
 
     $query = $this->buildSearch();
-    $filter = $query->createFilter('OR');
-    $filter->condition($this->getFieldId('keywords'), 'orange');
-    $filter->condition($this->getFieldId('keywords'), 'strawberry');
-    $query->filter($filter);
-    $filter = $query->createFilter('OR');
-    $filter->condition($this->getFieldId('keywords'), 'apple');
-    $filter->condition($this->getFieldId('keywords'), 'grape');
-    $query->filter($filter);
+    $conditions = $query->createConditionGroup('OR');
+    $conditions->addCondition($this->getFieldId('keywords'), 'orange');
+    $conditions->addCondition($this->getFieldId('keywords'), 'strawberry');
+    $query->addConditionGroup($conditions);
+    $conditions = $query->createConditionGroup('OR');
+    $conditions->addCondition($this->getFieldId('keywords'), 'apple');
+    $conditions->addCondition($this->getFieldId('keywords'), 'grape');
+    $query->addConditionGroup($conditions);
     $query->sort($this->getFieldId('id'), QueryInterface::SORT_ASC);
     $results = $query->execute();
     $this->assertEqual($results->getResultCount(), 3, 'Multiple OR filters on multi-valued field returned correct number of results.');
@@ -509,16 +509,16 @@ class BackendTest extends EntityUnitTestBase {
     $this->assertWarnings($results);
 
     $query = $this->buildSearch();
-    $filter1 = $query->createFilter('OR');
-    $filter = $query->createFilter('AND');
-    $filter->condition($this->getFieldId('keywords'), 'orange');
-    $filter->condition($this->getFieldId('keywords'), 'apple');
-    $filter1->filter($filter);
-    $filter = $query->createFilter('AND');
-    $filter->condition($this->getFieldId('keywords'), 'strawberry');
-    $filter->condition($this->getFieldId('keywords'), 'grape');
-    $filter1->filter($filter);
-    $query->filter($filter1);
+    $conditions1 = $query->createConditionGroup('OR');
+    $conditions = $query->createConditionGroup('AND');
+    $conditions->addCondition($this->getFieldId('keywords'), 'orange');
+    $conditions->addCondition($this->getFieldId('keywords'), 'apple');
+    $conditions1->addConditionGroup($conditions);
+    $conditions = $query->createConditionGroup('AND');
+    $conditions->addCondition($this->getFieldId('keywords'), 'strawberry');
+    $conditions->addCondition($this->getFieldId('keywords'), 'grape');
+    $conditions1->addConditionGroup($conditions);
+    $query->addConditionGroup($conditions1);
     $query->sort($this->getFieldId('id'), QueryInterface::SORT_ASC);
     $results = $query->execute();
     $this->assertEqual($results->getResultCount(), 3, 'Complex nested filters on multi-valued field returned correct number of results.');
@@ -686,7 +686,7 @@ class BackendTest extends EntityUnitTestBase {
       'missing' => TRUE,
     );
     $query->setOption('search_api_facets', $facets);
-    $query->condition($this->getFieldId('type'), 'article');
+    $query->addCondition($this->getFieldId('type'), 'article');
     $query->range(0, 0);
     $results = $query->execute();
     $expected = array(
@@ -708,7 +708,7 @@ class BackendTest extends EntityUnitTestBase {
       'missing' => FALSE,
     );
     $query->setOption('search_api_facets', $facets);
-    $query->condition($this->getFieldId('id'), 5, '<>');
+    $query->addCondition($this->getFieldId('id'), 5, '<>');
     $query->range(0, 0);
     $results = $query->execute();
     $expected = array(
diff --git a/src/Plugin/search_api/processor/ContentAccess.php b/src/Plugin/search_api/processor/ContentAccess.php
index e60849e..23627e0 100644
--- a/src/Plugin/search_api/processor/ContentAccess.php
+++ b/src/Plugin/search_api/processor/ContentAccess.php
@@ -267,16 +267,16 @@ class ContentAccess extends ProcessorPluginBase {
     // however, and can add the "ADD"
     // @todo Add a filter tag, once they are implemented.
     if ($unaffected_datasources) {
-      $outer_filter = $query->createFilter('OR');
-      $query->filter($outer_filter);
+      $outer_conditions = $query->createConditionGroup('OR');
+      $query->addConditionGroup($outer_conditions);
       foreach ($unaffected_datasources as $datasource_id) {
-        $outer_filter->condition('search_api_datasource', $datasource_id);
+        $outer_conditions->addCondition('search_api_datasource', $datasource_id);
       }
-      $access_filter = $query->createFilter('AND');
-      $outer_filter->filter($access_filter);
+      $access_conditions = $query->createConditionGroup('AND');
+      $outer_conditions->addConditionGroup($access_conditions);
     }
     else {
-      $access_filter = $query;
+      $access_conditions = $query;
     }
 
     if (!$account->hasPermission('access content')) {
@@ -294,7 +294,7 @@ class ContentAccess extends ProcessorPluginBase {
       // not return any results at all.
       if (!$unaffected_datasources) {
         // @todo More elegant way to return no results?
-        $query->condition('search_api_language', '');
+        $query->addCondition('search_api_language', '');
       }
       return;
     }
@@ -302,7 +302,7 @@ class ContentAccess extends ProcessorPluginBase {
     // Collect all the required fields that need to be part of the index.
     $unpublished_own = $account->hasPermission('view own unpublished content');
 
-    $enabled_filter = $query->createFilter('OR');
+    $enabled_conditions = $query->createConditionGroup('OR');
     foreach ($affected_datasources as $entity_type => $datasources) {
       $published = ($entity_type == 'node') ? NODE_PUBLISHED : Comment::PUBLISHED;
       foreach ($datasources as $datasource_id) {
@@ -310,27 +310,27 @@ class ContentAccess extends ProcessorPluginBase {
         // unpublished nodes, a simple filter on "status" is enough. Otherwise,
         // it's a bit more complicated.
         $status_field = Utility::createCombinedId($datasource_id, 'status');
-        $enabled_filter->condition($status_field, $published);
+        $enabled_conditions->addCondition($status_field, $published);
         if ($entity_type == 'node' && $unpublished_own) {
           $author_field = Utility::createCombinedId($datasource_id, 'uid');
-          $enabled_filter->condition($author_field, $account->id());
+          $enabled_conditions->addCondition($author_field, $account->id());
         }
       }
     }
-    $access_filter->filter($enabled_filter);
+    $access_conditions->addConditionGroup($enabled_conditions);
 
     // Filter by the user's node access grants.
-    $grants_filter = $query->createFilter('OR');
+    $grants_conditions = $query->createConditionGroup('OR');
     $grants = node_access_grants('view', $account);
     foreach ($grants as $realm => $gids) {
       foreach ($gids as $gid) {
-        $grants_filter->condition('search_api_node_grants', "node_access_$realm:$gid");
+        $grants_conditions->addCondition('search_api_node_grants', "node_access_$realm:$gid");
       }
     }
     // Also add items that are accessible for everyone by checking the "access
     // all" pseudo grant.
-    $grants_filter->condition('search_api_node_grants', 'node_access__all');
-    $access_filter->filter($grants_filter);
+    $grants_conditions->addCondition('search_api_node_grants', 'node_access__all');
+    $access_conditions->addConditionGroup($grants_conditions);
   }
 
 }
diff --git a/src/Plugin/views/argument/SearchApiArgument.php b/src/Plugin/views/argument/SearchApiArgument.php
index 7d7b0b6..f982a56 100644
--- a/src/Plugin/views/argument/SearchApiArgument.php
+++ b/src/Plugin/views/argument/SearchApiArgument.php
@@ -96,17 +96,17 @@ class SearchApiArgument extends ArgumentPluginBase {
     $condition_operator = empty($this->options['not']) ? '=' : '<>';
 
     if (count($this->value) > 1) {
-      $filter = $this->query->createFilter(Unicode::strtoupper($this->operator));
-      // $filter will be NULL if there were errors in the query.
-      if ($filter) {
+      $conditions = $this->query->createConditionGroup(Unicode::strtoupper($this->operator));
+      // $conditions will be NULL if there were errors in the query.
+      if ($conditions) {
         foreach ($this->value as $value) {
-          $filter->condition($this->realField, $value, $condition_operator);
+          $conditions->addCondition($this->realField, $value, $condition_operator);
         }
-        $this->query->filter($filter);
+        $this->query->addConditionGroup($conditions);
       }
     }
     else {
-      $this->query->condition($this->realField, reset($this->value), $condition_operator);
+      $this->query->addCondition($this->realField, reset($this->value), $condition_operator);
     }
   }
 
diff --git a/src/Plugin/views/argument/SearchApiDate.php b/src/Plugin/views/argument/SearchApiDate.php
index c87378b..7613e52 100644
--- a/src/Plugin/views/argument/SearchApiDate.php
+++ b/src/Plugin/views/argument/SearchApiDate.php
@@ -41,12 +41,12 @@ class SearchApiDate extends SearchApiArgument {
     }
 
     if (!empty($this->value)) {
-      $outer_filter = $this->query->createFilter($outer_conjunction);
+      $outer_conditions = $this->query->createConditionGroup($outer_conjunction);
       // @todo Refactor to use only a single nested filter, and only if
-      //   necessary. $value_filter will currently only ever contain a single
+      //   necessary. $value_conditions will currently only ever contain a single
       //   child – a condition or a nested filter with two conditions.
       foreach ($this->value as $value) {
-        $value_filter = $this->query->createFilter($inner_conjunction);
+        $value_conditions = $this->query->createConditionGroup($inner_conjunction);
         $values = explode(';', $value);
         $values = array_map(array($this, 'getTimestamp'), $values);
         if (in_array(FALSE, $values, TRUE)) {
@@ -55,18 +55,18 @@ class SearchApiDate extends SearchApiArgument {
         }
         $is_range = (count($values) > 1);
 
-        $inner_filter = ($is_range ? $this->query->createFilter('AND') : $value_filter);
+        $inner_conditions = ($is_range ? $this->query->createConditionGroup('AND') : $value_conditions);
         $range_op = (empty($this->options['not']) ? '>=' : '<');
-        $inner_filter->condition($this->realField, $values[0], $is_range ? $range_op : $condition_operator);
+        $inner_conditions->addCondition($this->realField, $values[0], $is_range ? $range_op : $condition_operator);
         if ($is_range) {
           $range_op = (empty($this->options['not']) ? '<=' : '>');
-          $inner_filter->condition($this->realField, $values[1], $range_op);
-          $value_filter->filter($inner_filter);
+          $inner_conditions->addCondition($this->realField, $values[1], $range_op);
+          $value_conditions->addConditionGroup($inner_conditions);
         }
-        $outer_filter->filter($value_filter);
+        $outer_conditions->addConditionGroup($value_conditions);
       }
 
-      $this->query->filter($outer_filter);
+      $this->query->addConditionGroup($outer_conditions);
     }
   }
 
diff --git a/src/Plugin/views/argument/SearchApiTaxonomyTerm.php b/src/Plugin/views/argument/SearchApiTaxonomyTerm.php
index 46f2239..e840c29 100644
--- a/src/Plugin/views/argument/SearchApiTaxonomyTerm.php
+++ b/src/Plugin/views/argument/SearchApiTaxonomyTerm.php
@@ -41,31 +41,31 @@ class SearchApiTaxonomyTerm extends SearchApiArgument {
       $terms = Term::load($this->value);
 
       if (!empty($terms)) {
-        $filter = $this->query->createFilter($outer_conjunction);
+        $conditions = $this->query->createConditionGroup($outer_conjunction);
         $vocabulary_fields = $this->definition['vocabulary_fields'];
         $vocabulary_fields += array('' => array());
         foreach ($terms as $term) {
-          $inner_filter = $filter;
+          $inner_conditions = $conditions;
           if ($outer_conjunction != $inner_conjunction) {
-            $inner_filter = $this->query->createFilter($inner_conjunction);
+            $inner_conditions = $this->query->createConditionGroup($inner_conjunction);
           }
           // Set filters for all term reference fields which don't specify a
           // vocabulary, as well as for all fields specifying the term's
           // vocabulary.
           if (!empty($vocabulary_fields[$term->vocabulary_id])) {
             foreach ($vocabulary_fields[$term->vocabulary_id] as $field) {
-              $inner_filter->condition($field, $term->tid, $condition_operator);
+              $inner_conditions->addCondition($field, $term->tid, $condition_operator);
             }
           }
           foreach ($vocabulary_fields[''] as $field) {
-            $inner_filter->condition($field, $term->tid, $condition_operator);
+            $inner_conditions->addCondition($field, $term->tid, $condition_operator);
           }
           if ($outer_conjunction != $inner_conjunction) {
-            $filter->filter($inner_filter);
+            $conditions->addConditionGroup($inner_conditions);
           }
         }
 
-        $this->query->filter($filter);
+        $this->query->addConditionGroup($conditions);
       }
     }
   }
diff --git a/src/Plugin/views/filter/SearchApiFilter.php b/src/Plugin/views/filter/SearchApiFilter.php
index 9f970b9..1bc7f2d 100644
--- a/src/Plugin/views/filter/SearchApiFilter.php
+++ b/src/Plugin/views/filter/SearchApiFilter.php
@@ -98,17 +98,17 @@ class SearchApiFilter extends FilterPluginBase {
    */
   public function query() {
     if ($this->operator === 'empty') {
-      $this->query->condition($this->realField, NULL, '=', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '=', $this->options['group']);
     }
     elseif ($this->operator === 'not empty') {
-      $this->query->condition($this->realField, NULL, '<>', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '<>', $this->options['group']);
     }
     else {
       while (is_array($this->value)) {
         $this->value = $this->value ? reset($this->value) : NULL;
       }
       if (strlen($this->value) > 0) {
-        $this->query->condition($this->realField, $this->value, $this->operator, $this->options['group']);
+        $this->query->addCondition($this->realField, $this->value, $this->operator, $this->options['group']);
       }
     }
   }
diff --git a/src/Plugin/views/filter/SearchApiFilterDate.php b/src/Plugin/views/filter/SearchApiFilterDate.php
index 1566d81..7266835 100644
--- a/src/Plugin/views/filter/SearchApiFilterDate.php
+++ b/src/Plugin/views/filter/SearchApiFilterDate.php
@@ -83,10 +83,10 @@ class SearchApiFilterDate extends SearchApiFilter {
    */
   public function query() {
     if ($this->operator === 'empty') {
-      $this->query->condition($this->realField, NULL, '=', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '=', $this->options['group']);
     }
     elseif ($this->operator === 'not empty') {
-      $this->query->condition($this->realField, NULL, '<>', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '<>', $this->options['group']);
     }
     else {
       while (is_array($this->value)) {
@@ -94,7 +94,7 @@ class SearchApiFilterDate extends SearchApiFilter {
       }
       $v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);
       if ($v !== FALSE) {
-        $this->query->condition($this->realField, $v, $this->operator, $this->options['group']);
+        $this->query->addCondition($this->realField, $v, $this->operator, $this->options['group']);
       }
     }
   }
diff --git a/src/Plugin/views/filter/SearchApiFilterEntityBase.php b/src/Plugin/views/filter/SearchApiFilterEntityBase.php
index f05e486..8829b5b 100644
--- a/src/Plugin/views/filter/SearchApiFilterEntityBase.php
+++ b/src/Plugin/views/filter/SearchApiFilterEntityBase.php
@@ -195,23 +195,23 @@ abstract class SearchApiFilterEntityBase extends SearchApiFilter {
    */
   public function query() {
     if ($this->operator === 'empty') {
-      $this->query->condition($this->realField, NULL, '=', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '=', $this->options['group']);
     }
     elseif ($this->operator === 'not empty') {
-      $this->query->condition($this->realField, NULL, '<>', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '<>', $this->options['group']);
     }
     elseif (is_array($this->value)) {
       $all_of = $this->operator === 'all of';
       $operator = $all_of ? '=' : $this->operator;
       if (count($this->value) == 1) {
-        $this->query->condition($this->realField, reset($this->value), $operator, $this->options['group']);
+        $this->query->addCondition($this->realField, reset($this->value), $operator, $this->options['group']);
       }
       else {
-        $filter = $this->query->createFilter($operator === '<>' || $all_of ? 'AND' : 'OR');
+        $conditions = $this->query->createConditionGroup($operator === '<>' || $all_of ? 'AND' : 'OR');
         foreach ($this->value as $value) {
-          $filter->condition($this->realField, $value, $operator);
+          $conditions->addCondition($this->realField, $value, $operator);
         }
-        $this->query->filter($filter, $this->options['group']);
+        $this->query->addConditionGroup($conditions, $this->options['group']);
       }
     }
   }
diff --git a/src/Plugin/views/filter/SearchApiFilterOptions.php b/src/Plugin/views/filter/SearchApiFilterOptions.php
index b42b173..b5cea54 100644
--- a/src/Plugin/views/filter/SearchApiFilterOptions.php
+++ b/src/Plugin/views/filter/SearchApiFilterOptions.php
@@ -242,11 +242,11 @@ class SearchApiFilterOptions extends SearchApiFilter {
    */
   public function query() {
     if ($this->operator === 'empty') {
-      $this->query->condition($this->realField, NULL, '=', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '=', $this->options['group']);
       return;
     }
     if ($this->operator === 'not empty') {
-      $this->query->condition($this->realField, NULL, '<>', $this->options['group']);
+      $this->query->addCondition($this->realField, NULL, '<>', $this->options['group']);
       return;
     }
 
@@ -273,22 +273,22 @@ class SearchApiFilterOptions extends SearchApiFilter {
     // "is none of"), or want to find only items with no value for the field.
     if ($this->value === array()) {
       if ($operator != '<>') {
-        $this->query->condition($this->realField, NULL, '=', $this->options['group']);
+        $this->query->addCondition($this->realField, NULL, '=', $this->options['group']);
       }
       return;
     }
 
     if (is_scalar($this->value) && $this->value !== '') {
-      $this->query->condition($this->realField, $this->value, $operator, $this->options['group']);
+      $this->query->addCondition($this->realField, $this->value, $operator, $this->options['group']);
     }
     elseif ($this->value) {
-      $filter = $this->query->createFilter($conjunction);
-      // $filter will be NULL if there were errors in the query.
-      if ($filter) {
+      $conditions = $this->query->createConditionGroup($conjunction);
+      // $conditions will be NULL if there were errors in the query.
+      if ($conditions) {
         foreach ($this->value as $v) {
-          $filter->condition($this->realField, $v, $operator);
+          $conditions->addCondition($this->realField, $v, $operator);
         }
-        $this->query->filter($filter, $this->options['group']);
+        $this->query->addConditionGroup($conditions, $this->options['group']);
       }
     }
   }
diff --git a/src/Plugin/views/filter/SearchApiFulltext.php b/src/Plugin/views/filter/SearchApiFulltext.php
index 0726e26..f76c381 100644
--- a/src/Plugin/views/filter/SearchApiFulltext.php
+++ b/src/Plugin/views/filter/SearchApiFulltext.php
@@ -151,15 +151,15 @@ class SearchApiFulltext extends SearchApiFilterText {
     // If something already specifically set different fields, we silently fall
     // back to mere filtering.
     $old = $this->query->getFulltextFields();
-    $filter = $old && (array_diff($old, $fields) || array_diff($fields, $old));
+    $conditions = $old && (array_diff($old, $fields) || array_diff($fields, $old));
 
-    if ($filter) {
-      $filter = $this->query->createFilter('OR');
+    if ($conditions) {
+      $conditions = $this->query->createConditionGroup('OR');
       $op = $this->operator === 'NOT' ? '<>' : '=';
       foreach ($fields as $field) {
-        $filter->condition($field, $this->value, $op);
+        $conditions->addCondition($field, $this->value, $op);
       }
-      $this->query->filter($filter);
+      $this->query->addConditionGroup($conditions);
       return;
     }
 
diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php
index 9165c1f..a695f2d 100644
--- a/src/Plugin/views/query/SearchApiQuery.php
+++ b/src/Plugin/views/query/SearchApiQuery.php
@@ -13,7 +13,7 @@ use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 use Drupal\search_api\Entity\Index;
-use Drupal\search_api\Query\FilterInterface;
+use Drupal\search_api\Query\ConditionGroupInterface;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\Plugin\views\query\QueryPluginBase;
 use Drupal\views\ResultRow;
@@ -90,14 +90,14 @@ class SearchApiQuery extends QueryPluginBase {
    *
    * @var array
    */
-  protected $fields;
+  protected $fields = array();
 
   /**
-   * The query's sub-filters representing the different Views filter groups.
+   * The query's conditions representing the different Views filter groups.
    *
    * @var array
    */
-  protected $filters = array();
+  protected $conditions = array();
 
   /**
    * The conjunction with which multiple filter groups are combined.
@@ -139,9 +139,12 @@ class SearchApiQuery extends QueryPluginBase {
       if (!$this->index) {
         $this->abort(new FormattableMarkup('View %view is not based on Search API but tries to use its query plugin.', array('%view' => $view->storage->label())));
       }
-      $this->query = $this->index->query(array(
-        'parse mode' => $this->options['parse_mode'],
-      ));
+      $this->query = $this->index->query();
+      $this->query->setParseMode($this->options['parse_mode']);
+      $this->query->addTag('views');
+      $this->query->addTag('views_' . $view->id());
+      $this->query->setOption('search_api_view', $view);
+
     }
     catch (\Exception $e) {
       $this->abort($e->getMessage());
@@ -265,8 +268,8 @@ class SearchApiQuery extends QueryPluginBase {
       // add a new OR filter to the query to which the filters for the groups
       // will be added.
       if ($this->group_operator === 'OR') {
-        $base = $this->query->createFilter('OR');
-        $this->query->filter($base);
+        $base = $this->query->createConditionGroup('OR');
+        $this->query->addConditionGroup($base);
       }
       else {
         $base = $this->query;
@@ -277,21 +280,21 @@ class SearchApiQuery extends QueryPluginBase {
           $group += array('type' => 'AND');
           // For filters without a group, we want to always add them directly to
           // the query.
-          $filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']);
+          $conditions = ($group_id === '') ? $this->query : $this->query->createConditionGroup($group['type']);
           if (!empty($group['conditions'])) {
             foreach ($group['conditions'] as $condition) {
               list($field, $value, $operator) = $condition;
-              $filter->condition($field, $value, $operator);
+              $conditions->addCondition($field, $value, $operator);
             }
           }
           if (!empty($group['filters'])) {
-            foreach ($group['filters'] as $nested_filter) {
-              $filter->filter($nested_filter);
+            foreach ($group['filters'] as $nested_conditions) {
+              $conditions->addConditionGroup($nested_conditions);
             }
           }
           // If no group was given, the filters were already set on the query.
           if ($group_id !== '') {
-            $base->filter($filter);
+            $base->addConditionGroup($conditions);
           }
         }
       }
@@ -323,7 +326,6 @@ class SearchApiQuery extends QueryPluginBase {
    */
   public function alter(ViewExecutable $view) {
     \Drupal::moduleHandler()->invokeAll('views_query_alter', array($view, $this));
-    \Drupal::moduleHandler()->alter('search_api_views_query', $view, $this->query);
   }
 
   /**
@@ -540,21 +542,21 @@ class SearchApiQuery extends QueryPluginBase {
   //
 
   /**
-   * Creates a new filter to use with this query object.
+   * Creates a new condition group to use with this query object.
    *
    * @param string $conjunction
-   *   The conjunction to use for the filter - either 'AND' or 'OR'.
+   *   The conjunction to use for the condition group – either 'AND' or 'OR'.
    * @param string[] $tags
-   *   (optional) Tags to set on the filter.
+   *   (optional) Tags to set on the condition group.
    *
-   * @return \Drupal\search_api\Query\FilterInterface
-   *   A filter object that is set to use the specified conjunction.
+   * @return \Drupal\search_api\Query\ConditionGroupInterface
+   *   A condition group object that is set to use the specified conjunction.
    *
-   * @see \Drupal\search_api\Query\QueryInterface::createFilter()
+   * @see \Drupal\search_api\Query\QueryInterface::createConditionGroup()
    */
-  public function createFilter($conjunction = 'AND', array $tags = array()) {
+  public function createConditionGroup($conjunction = 'AND', array $tags = array()) {
     if (!$this->shouldAbort()) {
-      return $this->query->createFilter($conjunction, $tags);
+      return $this->query->createConditionGroup($conjunction, $tags);
     }
     return NULL;
   }
@@ -604,23 +606,23 @@ class SearchApiQuery extends QueryPluginBase {
   }
 
   /**
-   * Adds a subfilter to this query's filter.
+   * Adds a nested condition group.
    *
    * If $group is given, the filter is added to the relevant filter group
    * instead.
    *
-   * @param \Drupal\search_api\Query\FilterInterface $filter
-   *   A filter that should be added as a subfilter.
+   * @param \Drupal\search_api\Query\ConditionGroupInterface $condition_group
+   *   A condition group that should be added.
    * @param string|null $group
    *   (optional) The Views query filter group to add this filter to.
    *
    * @return $this
    *
-   * @see \Drupal\search_api\Query\QueryInterface::filter()
+   * @see \Drupal\search_api\Query\QueryInterface::addConditionGroup()
    */
-  public function filter(FilterInterface $filter, $group = NULL) {
+  public function addConditionGroup(ConditionGroupInterface $condition_group, $group = NULL) {
     if (!$this->shouldAbort()) {
-      $this->filters[$group]['filters'][] = $filter;
+      $this->filters[$group]['filters'][] = $condition_group;
     }
     return $this;
   }
@@ -647,9 +649,9 @@ class SearchApiQuery extends QueryPluginBase {
    *
    * @return $this
    *
-   * @see \Drupal\search_api\Query\QueryInterface::condition()
+   * @see \Drupal\search_api\Query\QueryInterface::addCondition()
    */
-  public function condition($field, $value, $operator = '=', $group = NULL) {
+  public function addCondition($field, $value, $operator = '=', $group = NULL) {
     if (!$this->shouldAbort()) {
       $this->filters[$group]['conditions'][] = array($field, $value, $operator);
     }
@@ -784,14 +786,14 @@ class SearchApiQuery extends QueryPluginBase {
   /**
    * Retrieves the filter object associated with this search query.
    *
-   * @return \Drupal\search_api\Query\FilterInterface
+   * @return \Drupal\search_api\Query\ConditionGroupInterface
    *   This object's associated filter object.
    *
-   * @see \Drupal\search_api\Query\QueryInterface::getFilter()
+   * @see \Drupal\search_api\Query\QueryInterface::getConditionGroup()
    */
   public function getFilter() {
     if (!$this->shouldAbort()) {
-      return $this->query->getFilter();
+      return $this->query->getConditionGroup();
     }
     return NULL;
   }
@@ -852,8 +854,6 @@ class SearchApiQuery extends QueryPluginBase {
    *     the whole result in the index.
    *   - limit: The maximum number of search results to return. -1 means no
    *     limit.
-   *   - 'filter class': Can be used to change the \Drupal\search_api\Query\FilterInterface
-   *     implementation to use.
    *   - 'search id': A string that will be used as the identifier when storing
    *     this search in the Search API's static cache.
    *   - 'skip result count': If present and set to TRUE, the search's result
diff --git a/src/Processor/FieldsProcessorPluginBase.php b/src/Processor/FieldsProcessorPluginBase.php
index 49125e4..2f71564 100644
--- a/src/Processor/FieldsProcessorPluginBase.php
+++ b/src/Processor/FieldsProcessorPluginBase.php
@@ -10,7 +10,8 @@ namespace Drupal\search_api\Processor;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 use Drupal\search_api\Item\FieldInterface;
-use Drupal\search_api\Query\FilterInterface;
+use Drupal\search_api\Query\ConditionGroupInterface;
+use Drupal\search_api\Query\ConditionInterface;
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\Utility;
 
@@ -25,7 +26,7 @@ use Drupal\search_api\Utility;
  * - processFieldValue()
  * - processKeys()
  * - processKey()
- * - processFilters()
+ * - processConditions()
  * - processFilterValue()
  * - process()
  *
@@ -107,9 +108,8 @@ abstract class FieldsProcessorPluginBase extends ProcessorPluginBase {
     if (isset($keys)) {
       $this->processKeys($keys);
     }
-    $filter = $query->getFilter();
-    $filters = &$filter->getFilters();
-    $this->processFilters($filters);
+    $conditions = $query->getConditionGroup();
+    $this->processConditions($conditions->getConditions());
   }
 
   /**
@@ -214,33 +214,39 @@ abstract class FieldsProcessorPluginBase extends ProcessorPluginBase {
   }
 
   /**
-   * Preprocesses the query filters.
+   * Preprocesses the query condition.
    *
-   * @param array $filters
-   *   An array of filters, passed by reference. The contents follow the format
-   *   of \Drupal\search_api\Query\FilterInterface::getFilters().
+   * @param \Drupal\search_api\Query\ConditionInterface[]|\Drupal\search_api\Query\ConditionGroupInterface[] $conditions
+   *   An array of conditions, as returned by
+   *   \Drupal\search_api\Query\ConditionGroupInterface::getConditions(),
+   *   passed by reference.
    */
-  protected function processFilters(array &$filters) {
+  protected function processConditions(array &$conditions) {
     $fields = $this->index->getFields();
-    foreach ($filters as $key => &$f) {
-      if (is_array($f)) {
-        if (isset($fields[$f[0]]) && $this->testField($f[0], $fields[$f[0]])) {
+    foreach ($conditions as $key => &$condition) {
+      if ($condition instanceof ConditionInterface) {
+        $field = $condition->getField();
+        if ($this->testField($field, $fields[$field])) {
           // We want to allow processors also to easily remove complete filters.
           // However, we can't use empty() or the like, as that would sort out
           // filters for 0 or NULL. So we specifically check only for the empty
           // string, and we also make sure the filter value was actually changed
           // by storing whether it was empty before.
-          $empty_string = $f[1] === '';
-          $this->processFilterValue($f[1]);
+          $value = $condition->getValue();
+          $empty_string = $value === '';
+          $this->processFilterValue($value);
 
-          if ($f[1] === '' && !$empty_string) {
-            unset($filters[$key]);
+          if ($value === '' && !$empty_string) {
+            unset($conditions[$key]);
+          }
+          else {
+            $condition->setValue($value);
           }
         }
       }
-      elseif ($f instanceof FilterInterface) {
-        $child_filters = &$f->getFilters();
-        $this->processFilters($child_filters);
+      elseif ($condition instanceof ConditionGroupInterface) {
+        $child_conditions = &$condition->getConditions();
+        $this->processConditions($child_conditions);
       }
     }
   }
diff --git a/src/Query/Condition.php b/src/Query/Condition.php
new file mode 100644
index 0000000..1802559
--- /dev/null
+++ b/src/Query/Condition.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search_api\Query\Condition.
+ */
+
+namespace Drupal\search_api\Query;
+
+/**
+ * Represents a single (field operator value) condition in a search query.
+ */
+class Condition implements ConditionInterface {
+
+  /**
+   * The field this condition checks.
+   *
+   * @var string
+   */
+  protected $field;
+
+  /**
+   * The value being compared to the field.
+   *
+   * @var mixed
+   */
+  protected $value;
+
+  /**
+   * The operator that combined field and value in this condition.
+   *
+   * @var string
+   */
+  protected $operator;
+
+  /**
+   * Constructs a Condition object.
+   *
+   * @param string $field
+   *   The field this condition checks.
+   * @param mixed $value
+   *   The value being compared to the field.
+   * @param string $operator
+   *   (optional) The operator that combined field and value in this condition.
+   */
+  public function __construct($field, $value, $operator = '=') {
+    $this->field = $field;
+    $this->value = $value;
+    $this->operator = $operator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getField() {
+    return $this->field;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setField($field) {
+    $this->field = $field;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    return $this->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($value) {
+    $this->value = $value;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperator() {
+    return $this->operator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOperator($operator) {
+    $this->operator = $operator;
+    return $this;
+  }
+
+  /**
+   * Implements the magic __toString() method to simplify debugging.
+   */
+  function __toString() {
+    return "{$this->field} {$this->operator} " . str_replace("\n", "\n    ", var_export($this->value, TRUE));
+  }
+
+}
diff --git a/src/Query/ConditionGroup.php b/src/Query/ConditionGroup.php
new file mode 100644
index 0000000..61f226a
--- /dev/null
+++ b/src/Query/ConditionGroup.php
@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search_api\Query\ConditionGroup.
+ */
+
+namespace Drupal\search_api\Query;
+
+/**
+ * Provides a standard implementation for a Search API query condition group.
+ */
+class ConditionGroup implements ConditionGroupInterface {
+
+  /**
+   * Array containing sub-conditions.
+   *
+   * Each of these is either an array (field, value, operator), or another
+   * \Drupal\search_api\Query\ConditionGroupInterface object.
+   *
+   * @var array
+   */
+  protected $conditions = array();
+
+  /**
+   * String specifying this condition group's conjunction ('AND' or 'OR').
+   *
+   * @var string
+   */
+  protected $conjunction;
+
+  /**
+   * An array of tags set on this condition group.
+   *
+   * @var string[]
+   */
+  protected $tags;
+
+  /**
+   * Constructs a ConditionGroup object.
+   *
+   * @param string $conjunction
+   *   (optional) The conjunction to use for this condition group - either 'AND'
+   *   or 'OR'.
+   * @param string[] $tags
+   *   (optional) An arbitrary set of tags. Can be used to identify this
+   *   condition group after it's been added to the query. This is
+   *   primarily used by the facet system to support OR facet queries.
+   */
+  public function __construct($conjunction = 'AND', array $tags = array()) {
+    $this->conjunction = strtoupper(trim($conjunction)) == 'OR' ? 'OR' : 'AND';
+    $this->tags = array_combine($tags, $tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConjunction() {
+    return $this->conjunction;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addConditionGroup(ConditionGroupInterface $condition_group) {
+    $this->conditions[] = $condition_group;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addCondition($field, $value, $operator = '=') {
+    $this->conditions[] = new Condition($field, $value, $operator);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getConditions() {
+    return $this->conditions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTag($tag) {
+    return isset($this->tags[$tag]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getTags() {
+    return $this->tags;
+  }
+
+  /**
+   * Implements the magic __clone() method.
+   *
+   * Takes care to clone nested condition groups, too.
+   */
+  public function __clone() {
+    foreach ($this->conditions as $i => $condition) {
+      $this->conditions[$i] = clone $condition;
+    }
+  }
+
+  /**
+   * Implements the magic __toString() method to simplify debugging.
+   */
+  public function __toString() {
+    // Special case for a single, nested condition group:
+    if (count($this->conditions) == 1) {
+      return (string) reset($this->conditions);
+    }
+    $ret = array();
+    foreach ($this->conditions as $condition) {
+      $ret[] = str_replace("\n", "\n  ", (string) $condition);
+    }
+    return $ret ? "[\n  " . implode("\n{$this->conjunction}\n  ", $ret) . "\n]" : '';
+  }
+
+}
diff --git a/src/Query/ConditionGroupInterface.php b/src/Query/ConditionGroupInterface.php
new file mode 100644
index 0000000..79f2ab8
--- /dev/null
+++ b/src/Query/ConditionGroupInterface.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search_api\Query\ConditionGroupInterface.
+ */
+
+namespace Drupal\search_api\Query;
+
+/**
+ * Represents a condition group on a search query.
+ *
+ * Condition groups can contain both basic (field operator value) conditions
+ * and nested condition groups.
+ */
+interface ConditionGroupInterface extends ConditionSetInterface {
+
+  /**
+   * Retrieves the conjunction used by this condition group.
+   *
+   * @return string
+   *   The conjunction used by this condition group – either 'AND' or 'OR'.
+   */
+  public function getConjunction();
+
+  /**
+   * Retrieves all conditions and nested condition groups of this object.
+   *
+   * @return \Drupal\search_api\Query\ConditionInterface[]|\Drupal\search_api\Query\ConditionGroupInterface[]
+   *   An array containing this object's conditions. Each of these is either a
+   *   simple condition, represented as an object of type
+   *   \Drupal\search_api\Query\ConditionInterface, or a nested condition group,
+   *   represented by a \Drupal\search_api\Query\ConditionGroupInterface object.
+   */
+  public function &getConditions();
+
+  /**
+   * Checks whether a certain tag was set on this condition group.
+   *
+   * @param string $tag
+   *   A tag to check for.
+   *
+   * @return bool
+   *   TRUE if the tag was set for this condition group, FALSE otherwise.
+   */
+  public function hasTag($tag);
+
+  /**
+   * Retrieves the tags set on this condition group.
+   *
+   * @return string[]
+   *   The tags associated with this condition group, as both the array keys and
+   *   values.
+   */
+  public function &getTags();
+
+}
diff --git a/src/Query/ConditionInterface.php b/src/Query/ConditionInterface.php
new file mode 100644
index 0000000..97fd6f8
--- /dev/null
+++ b/src/Query/ConditionInterface.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search_api\Query\ConditionInterface.
+ */
+
+namespace Drupal\search_api\Query;
+
+/**
+ * Represents a single (field operator value) condition in a search query.
+ */
+interface ConditionInterface {
+
+  /**
+   * Retrieves the field.
+   *
+   * @return string
+   *   The field this condition checks.
+   */
+  public function getField();
+
+  /**
+   * Sets the field.
+   *
+   * @param string $field
+   *   The new field.
+   *
+   * @return $this
+   */
+  public function setField($field);
+
+  /**
+   * Retrieves the value.
+   *
+   * @return mixed
+   *   The value being compared to the field.
+   */
+  public function getValue();
+
+  /**
+   * Sets the value.
+   *
+   * @param mixed $value
+   *   The new value.
+   *
+   * @return $this
+   */
+  public function setValue($value);
+
+  /**
+   * Retrieves the operator.
+   *
+   * @return string
+   *   The operator that combined field and value in this condition.
+   */
+  public function getOperator();
+
+  /**
+   * Sets the operator.
+   *
+   * @param string $operator
+   *   The new operator.
+   *
+   * @return $this
+   */
+  public function setOperator($operator);
+
+}
diff --git a/src/Query/ConditionSetInterface.php b/src/Query/ConditionSetInterface.php
new file mode 100644
index 0000000..01fa23b
--- /dev/null
+++ b/src/Query/ConditionSetInterface.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search_api\Query\ConditionSetInterface.
+ */
+
+namespace Drupal\search_api\Query;
+
+/**
+ * Defines a common interface for objects which can hold conditions.
+ */
+interface ConditionSetInterface {
+
+  /**
+   * Adds a new ($field $operator $value) condition.
+   *
+   * @param string $field
+   *   The field to filter on, e.g. "title". The special field
+   *   "search_api_datasource" can be used to filter by datasource ID.
+   * @param mixed $value
+   *   The value the field should have (or be related to by the operator).
+   * @param string $operator
+   *   The operator to use for checking the constraint. The following operators
+   *   are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
+   *   have the same semantics as the corresponding SQL operators.
+   *   If $field is a fulltext field, $operator can only be "=" or "<>", which
+   *   are in this case interpreted as "contains" or "doesn't contain",
+   *   respectively.
+   *   If $value is NULL, $operator also can only be "=" or "<>", meaning the
+   *   field must have no or some value, respectively.
+   *
+   * @return $this
+   */
+  public function addCondition($field, $value, $operator = '=');
+
+  /**
+   * Adds a nested condition group.
+   *
+   * @param \Drupal\search_api\Query\ConditionGroupInterface $condition_group
+   *   A condition group that should be added.
+   *
+   * @return $this
+   */
+  public function addConditionGroup(ConditionGroupInterface $condition_group);
+
+}
diff --git a/src/Query/Filter.php b/src/Query/Filter.php
deleted file mode 100644
index 5621c28..0000000
--- a/src/Query/Filter.php
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\search_api\Query\Filter.
- */
-
-namespace Drupal\search_api\Query;
-
-/**
- * Provides a standard implementation for a Search API query filter.
- */
-class Filter implements FilterInterface {
-
-  /**
-   * Array containing subfilters.
-   *
-   * Each of these is either an array (field, value, operator), or another
-   * \Drupal\search_api\Query\FilterInterface object.
-   *
-   * @var array
-   */
-  protected $filters = array();
-
-  /**
-   * String specifying this filter's conjunction ('AND' or 'OR').
-   *
-   * @var string
-   */
-  protected $conjunction;
-
-  /**
-   * An array of tags set on this filter.
-   *
-   * @var string[]
-   */
-  protected $tags;
-
-  /**
-   * Constructs a Filter object.
-   *
-   * @param string $conjunction
-   *   (optional) The conjunction to use for this filter - either 'AND' or 'OR'.
-   * @param string[] $tags
-   *   (optional) An arbitrary set of tags. Can be used to identify this filter
-   *   after it's been added to the query. This is primarily used by the facet
-   *   system to support OR facet queries.
-   */
-  public function __construct($conjunction = 'AND', array $tags = array()) {
-    $this->conjunction = strtoupper(trim($conjunction)) == 'OR' ? 'OR' : 'AND';
-    $this->tags = array_combine($tags, $tags);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getConjunction() {
-    return $this->conjunction;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function filter(FilterInterface $filter) {
-    $this->filters[] = $filter;
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function condition($field, $value, $operator = '=') {
-    $this->filters[] = array($field, $value, $operator);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function &getFilters() {
-    return $this->filters;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function hasTag($tag) {
-    return isset($this->tags[$tag]);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function &getTags() {
-    return $this->tags;
-  }
-
-  /**
-   * Implements the magic __clone() method to clone nested filters, too.
-   */
-  public function __clone() {
-    foreach ($this->filters as $i => $filter) {
-      if (is_object($filter)) {
-        $this->filters[$i] = clone $filter;
-      }
-    }
-  }
-
-  /**
-   * Implements the magic __toString() method to simplify debugging.
-   */
-  public function __toString() {
-    // Special case for a single, nested filter:
-    if (count($this->filters) == 1 && is_object($this->filters[0])) {
-      return (string) $this->filters[0];
-    }
-    $ret = array();
-    foreach ($this->filters as $filter) {
-      if (is_object($filter)) {
-        $ret[] = "[\n  " . str_replace("\n", "\n    ", (string) $filter) . "\n  ]";
-      }
-      else {
-        $ret[] = "$filter[0] $filter[2] " . str_replace("\n", "\n    ", var_export($filter[1], TRUE));
-      }
-    }
-    return $ret ? '  ' . implode("\n{$this->conjunction}\n  ", $ret) : '';
-  }
-
-}
diff --git a/src/Query/FilterInterface.php b/src/Query/FilterInterface.php
deleted file mode 100644
index 61d7fc4..0000000
--- a/src/Query/FilterInterface.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\search_api\Query\FilterInterface.
- */
-
-namespace Drupal\search_api\Query;
-
-/**
- * Represents a filter on a search query.
- *
- * Filters apply conditions on one or more fields with a specific conjunction
- * (AND or OR) and may contain nested filters.
- */
-interface FilterInterface {
-
-  /**
-   * Adds a subfilter.
-   *
-   * @param \Drupal\search_api\Query\FilterInterface $filter
-   *   A filter object that should be added as a subfilter.
-   *
-   * @return $this
-   */
-  public function filter(FilterInterface $filter);
-
-  /**
-   * Adds a new ($field $operator $value) condition.
-   *
-   * @param string $field
-   *   The field to filter on, e.g. "title". The special field
-   *   "search_api_datasource" can be used to filter by datasource ID.
-   * @param mixed $value
-   *   The value the field should have (or be related to by the operator).
-   * @param string $operator
-   *   The operator to use for checking the constraint. The following operators
-   *   are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
-   *   have the same semantics as the corresponding SQL operators.
-   *   If $field is a fulltext field, $operator can only be "=" or "<>", which
-   *   are in this case interpreted as "contains" or "doesn't contain",
-   *   respectively.
-   *   If $value is NULL, $operator also can only be "=" or "<>", meaning the
-   *   field must have no or some value, respectively.
-   *
-   * @return $this
-   */
-  public function condition($field, $value, $operator = '=');
-
-  /**
-   * Retrieves the conjunction used by this filter.
-   *
-   * @return string
-   *   The conjunction used by this filter – either 'AND' or 'OR'.
-   */
-  public function getConjunction();
-
-  /**
-   * Retrieves all conditions and nested filters contained in this filter.
-   *
-   * @return array
-   *   An array containing this filter's subfilters. Each of these is either a
-   *   condition, represented as a numerically indexed array with the arguments
-   *   of a previous \Drupal\search_api\Query\FilterInterface::condition() call
-   *   (field, value, operator); or a nested filter, represented by a
-   *   \Drupal\search_api\Query\FilterInterface filter object.
-   */
-  public function &getFilters();
-
-  /**
-   * Checks whether a certain tag was set on this filter.
-   *
-   * @param string $tag
-   *   A tag to check for.
-   *
-   * @return bool
-   *   TRUE if the tag was set for this filter, FALSE otherwise.
-   */
-  public function hasTag($tag);
-
-  /**
-   * Retrieves the tags set on this filter.
-   *
-   * @return string[]
-   *   The tags associated with this filter, as both the array keys and values.
-   */
-  public function &getTags();
-
-}
diff --git a/src/Query/Query.php b/src/Query/Query.php
index eb4013b..75e93b9 100644
--- a/src/Query/Query.php
+++ b/src/Query/Query.php
@@ -43,6 +43,15 @@ class Query implements QueryInterface {
   protected $resultsCache;
 
   /**
+   * The parse mode to use for fulltext search keys.
+   *
+   * @var string
+   *
+   * @see \Drupal\search_api\Query\QueryInterface::parseModes()
+   */
+  protected $parseMode = 'terms';
+
+  /**
    * The search keys.
    *
    * If NULL, this will be a filter-only search.
@@ -66,11 +75,11 @@ class Query implements QueryInterface {
   protected $fields;
 
   /**
-   * The search filter associated with this query.
+   * The root condition group associated with this query.
    *
-   * @var \Drupal\search_api\Query\FilterInterface
+   * @var \Drupal\search_api\Query\ConditionGroupInterface
    */
-  protected $filter;
+  protected $conditionGroup;
 
   /**
    * The sorts associated with this query.
@@ -87,6 +96,13 @@ class Query implements QueryInterface {
   protected $options;
 
   /**
+   * The tags set on this query.
+   *
+   * @var string[]
+   */
+  protected $tags = array();
+
+  /**
    * Flag for whether preExecute() was already called for this query.
    *
    * @var bool
@@ -113,21 +129,13 @@ class Query implements QueryInterface {
     if (!$index->status()) {
       throw new SearchApiException(new FormattableMarkup("Can't search on index %index which is disabled.", array('%index' => $index->label())));
     }
-    if (isset($options['parse mode'])) {
-      $modes = $this->parseModes();
-      if (!isset($modes[$options['parse mode']])) {
-        throw new SearchApiException(new FormattableMarkup('Unknown parse mode: @mode.', array('@mode' => $options['parse mode'])));
-      }
-    }
     $this->index = $index;
     $this->resultsCache = $results_cache;
     $this->options = $options + array(
       'conjunction' => 'AND',
-      'parse mode' => 'terms',
-      'filter class' => '\Drupal\search_api\Query\Filter',
       'search id' => __CLASS__,
     );
-    $this->filter = $this->createFilter('AND');
+    $this->conditionGroup = $this->createConditionGroup('AND');
   }
 
   /**
@@ -162,12 +170,36 @@ class Query implements QueryInterface {
   /**
    * {@inheritdoc}
    */
-  protected function parseKeys($keys, $mode) {
+  public function getParseMode() {
+    return $this->parseMode;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setParseMode($parse_mode) {
+    $this->parseMode = $parse_mode;
+    return $this;
+  }
+
+  /**
+   * Parses search keys input by the user according to the given parse mode.
+   *
+   * @param string|array|null $keys
+   *   The keywords to parse.
+   *
+   * @return array|null|string
+   *   The parsed keywords, in the format defined by
+   *   \Drupal\search_api\Query\QueryInterface::getKeys().
+   *
+   * @see \Drupal\search_api\Query\QueryInterface::parseModes()
+   */
+  protected function parseKeys($keys) {
     if ($keys === NULL || is_array($keys)) {
       return $keys;
     }
     $keys = '' . $keys;
-    switch ($mode) {
+    switch ($this->parseMode) {
       case 'direct':
         return $keys;
 
@@ -218,9 +250,8 @@ class Query implements QueryInterface {
   /**
    * {@inheritdoc}
    */
-  public function createFilter($conjunction = 'AND', array $tags = array()) {
-    $filter_class = $this->options['filter class'];
-    return new $filter_class($conjunction, $tags);
+  public function createConditionGroup($conjunction = 'AND', array $tags = array()) {
+    return new ConditionGroup($conjunction, $tags);
   }
 
   /**
@@ -229,7 +260,7 @@ class Query implements QueryInterface {
   public function keys($keys = NULL) {
     $this->origKeys = $keys;
     if (isset($keys)) {
-      $this->keys = $this->parseKeys($keys, $this->options['parse mode']);
+      $this->keys = $this->parseKeys($keys);
     }
     else {
       $this->keys = NULL;
@@ -247,16 +278,16 @@ class Query implements QueryInterface {
   /**
    * {@inheritdoc}
    */
-  public function filter(FilterInterface $filter) {
-    $this->filter->filter($filter);
+  public function addConditionGroup(ConditionGroupInterface $condition_group) {
+    $this->conditionGroup->addConditionGroup($condition_group);
     return $this;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function condition($field, $value, $operator = '=') {
-    $this->filter->condition($field, $value, $operator);
+  public function addCondition($field, $value, $operator = '=') {
+    $this->conditionGroup->addCondition($field, $value, $operator);
     return $this;
   }
 
@@ -311,7 +342,11 @@ class Query implements QueryInterface {
       $this->index->preprocessSearchQuery($this);
 
       // Let modules alter the query.
-      \Drupal::moduleHandler()->alter('search_api_query', $this);
+      $hooks = array('search_api_query');
+      foreach ($this->tags as $tag) {
+        $hooks[] = "search_api_query_$tag";
+      }
+      \Drupal::moduleHandler()->alter($hooks, $this);
     }
   }
 
@@ -323,7 +358,11 @@ class Query implements QueryInterface {
     $this->index->postprocessSearchResults($results);
 
     // Let modules alter the results.
-    \Drupal::moduleHandler()->alter('search_api_results', $results);
+    $hooks = array('search_api_results');
+    foreach ($this->tags as $tag) {
+      $hooks[] = "search_api_results_$tag";
+    }
+    \Drupal::moduleHandler()->alter($hooks, $results);
 
     // Store the results in the static cache.
     $this->resultsCache->addResults($results);
@@ -360,8 +399,8 @@ class Query implements QueryInterface {
   /**
    * {@inheritdoc}
    */
-  public function getFilter() {
-    return $this->filter;
+  public function getConditionGroup() {
+    return $this->conditionGroup;
   }
 
   /**
@@ -424,9 +463,9 @@ class Query implements QueryInterface {
       $ret .= 'Parsed keys: ' . str_replace("\n", "\n  ", var_export($this->keys, TRUE)) . "\n";
       $ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n";
     }
-    if ($filter = (string) $this->filter) {
-      $filter = str_replace("\n", "\n  ", $filter);
-      $ret .= "Filters:\n  $filter\n";
+    if ($conditions = (string) $this->conditionGroup) {
+      $conditions = str_replace("\n", "\n  ", $conditions);
+      $ret .= "Conditions:\n  $conditions\n";
     }
     if ($this->sorts) {
       $sorts = array();
@@ -441,4 +480,40 @@ class Query implements QueryInterface {
     return $ret;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function addTag($tag) {
+    $this->tags[$tag] = $tag;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTag($tag) {
+    return isset($this->tags[$tag]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasAllTags() {
+    return !array_diff_key(array_flip(func_get_args()), $this->tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasAnyTag() {
+    return (bool) array_intersect_key(array_flip(func_get_args()), $this->tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getTags() {
+    return $this->tags;
+  }
+
 }
diff --git a/src/Query/QueryInterface.php b/src/Query/QueryInterface.php
index af262f3..f25228e 100644
--- a/src/Query/QueryInterface.php
+++ b/src/Query/QueryInterface.php
@@ -15,7 +15,7 @@ use Drupal\search_api\IndexInterface;
  * Methods not returning something else will return the object itself, so calls
  * can be chained.
  */
-interface QueryInterface {
+interface QueryInterface extends ConditionSetInterface {
 
   /**
    * Constant representing ascending sorting.
@@ -59,23 +59,41 @@ interface QueryInterface {
   public function parseModes();
 
   /**
-   * Creates a new filter to use with this query object.
+   * Retrieves the parse mode.
+   *
+   * @return string
+   *   The parse mode.
+   */
+  public function getParseMode();
+
+  /**
+   * Sets the parse mode.
+   *
+   * @param string $parse_mode
+   *   The parse mode.
+   *
+   * @return $this
+   */
+  public function setParseMode($parse_mode);
+
+  /**
+   * Creates a new condition group to use with this query object.
    *
    * @param string $conjunction
-   *   The conjunction to use for the filter – either 'AND' or 'OR'.
+   *   The conjunction to use for the condition group – either 'AND' or 'OR'.
    * @param string[] $tags
-   *   (optional) Tags to set on the filter.
+   *   (optional) Tags to set on the condition group.
    *
-   * @return \Drupal\search_api\Query\FilterInterface
-   *   A filter object that is set to use the specified conjunction.
+   * @return \Drupal\search_api\Query\ConditionGroupInterface
+   *   A condition group object that is set to use the specified conjunction.
    */
-  public function createFilter($conjunction = 'AND', array $tags = array());
+  public function createConditionGroup($conjunction = 'AND', array $tags = array());
 
   /**
    * Sets the keys to search for.
    *
    * If this method is not called on the query before execution, this will be a
-   * filter-only query.
+   * filters-only query.
    *
    * @param string|array|null $keys
    *   A string with the search keys, in one of the formats specified by
@@ -99,38 +117,6 @@ interface QueryInterface {
   public function setFulltextFields(array $fields = NULL);
 
   /**
-   * Adds a subfilter to this query's filter.
-   *
-   * @param \Drupal\search_api\Query\FilterInterface $filter
-   *   A filter that should be added as a subfilter.
-   *
-   * @return $this
-   */
-  public function filter(FilterInterface $filter);
-
-  /**
-   * Adds a new ($field $operator $value) condition filter.
-   *
-   * @param string $field
-   *   The field to filter on, e.g. "title". The special field
-   *   "search_api_datasource" can be used to filter by datasource ID.
-   * @param mixed $value
-   *   The value the field should have (or be related to by the operator).
-   * @param string $operator
-   *   The operator to use for checking the constraint. The following operators
-   *   are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
-   *   have the same semantics as the corresponding SQL operators.
-   *   If $field is a fulltext field, $operator can only be "=" or "<>", which
-   *   are in this case interpreted as "contains" or "doesn't contain",
-   *   respectively.
-   *   If $value is NULL, $operator also can only be "=" or "<>", meaning the
-   *   field must have no or some value, respectively.
-   *
-   * @return $this
-   */
-  public function condition($field, $value, $operator = '=');
-
-  /**
    * Adds a sort directive to this search query.
    *
    * If no sort is manually set, the results will be sorted descending by
@@ -250,12 +236,12 @@ interface QueryInterface {
   public function &getFulltextFields();
 
   /**
-   * Retrieves the filter object associated with this search query.
+   * Retrieves the condition group object associated with this search query.
    *
-   * @return \Drupal\search_api\Query\FilterInterface
-   *   This object's associated filter object.
+   * @return \Drupal\search_api\Query\ConditionGroupInterface
+   *   This object's associated condition group object.
    */
-  public function getFilter();
+  public function getConditionGroup();
 
   /**
    * Retrieves the sorts set for this query.
@@ -290,17 +276,11 @@ interface QueryInterface {
    *   The name of an option. The following options are recognized by default:
    *   - conjunction: The type of conjunction to use for this query – either
    *     'AND' or 'OR'. 'AND' by default. This only influences the search keys,
-   *     filters will always use AND by default.
-   *   - 'parse mode': The mode with which to parse the $keys variable, if it
-   *     is set and not already an array. See
-   *     \Drupal\search_api\Query\Query::parseModes() for recognized parse
-   *     modes.
+   *     condition groups will always use AND by default.
    *   - offset: The position of the first returned search results relative to
    *     the whole result in the index.
    *   - limit: The maximum number of search results to return. -1 means no
    *     limit.
-   *   - 'filter class': Can be used to change the
-   *     \Drupal\search_api\Query\FilterInterface implementation to use.
    *   - 'search id': A string that will be used as the identifier when storing
    *     this search in the Search API's static cache.
    *   - 'skip result count': If present and set to TRUE, the search's result
@@ -333,4 +313,68 @@ interface QueryInterface {
    */
   public function &getOptions();
 
+  /**
+   * Sets the given tag on this query.
+   *
+   * Tags are strings that categorize a query. A query may have any number of
+   * tags. Tags are used to mark a query so that alter hooks may decide if they
+   * wish to take action. Tags should be all lower-case and contain only
+   * letters, numbers, and underscore, and start with a letter. That is, they
+   * should follow the same rules as PHP identifiers in general.
+   *
+   * The call will be ignored if the tag is already set on this query.
+   *
+   * @param string $tag
+   *   The tag to set.
+   *
+   * @return $this
+   *
+   * @see hook_search_api_query_TAG_alter()
+   */
+  public function addTag($tag);
+
+  /**
+   * Checks whether a certain tag was set on this search query.
+   *
+   * @param string $tag
+   *   The tag to check for.
+   *
+   * @return bool
+   *   TRUE if the tag was set for this search query, FALSE otherwise.
+   */
+  public function hasTag($tag);
+
+  /**
+   * Determines whether this query has all the given tags set on it.
+   *
+   * @param string ...
+   *   The tags to check for.
+   *
+   * @return bool
+   *   TRUE if all the method parameters were set as tags on this query; FALSE
+   *   otherwise.
+   */
+  public function hasAllTags();
+
+  /**
+   * Determines whether this query has any of the given tags set on it.
+   *
+   * @param string ...
+   *   The tags to check for.
+   *
+   * @return bool
+   *   TRUE if any of the method parameters was set as a tag on this query;
+   *   FALSE otherwise.
+   */
+  public function hasAnyTag();
+
+  /**
+   * Retrieves the tags set on this query.
+   *
+   * @return string[]
+   *   The tags associated with this search query, as both the array keys and
+   *   values.
+   */
+  public function &getTags();
+
 }
diff --git a/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php b/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php
index 84fc945..98c2184 100644
--- a/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php
+++ b/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\search_api\Unit\Plugin\Processor;
 
+use Drupal\search_api\Query\Condition;
 use Drupal\search_api\Tests\Processor\TestItemsTrait;
 use Drupal\search_api\Utility;
 use Drupal\Tests\UnitTestCase;
@@ -32,7 +33,7 @@ class FieldsProcessorPluginBaseTest extends UnitTestCase {
   /**
    * The class under test.
    *
-   * @var \Drupal\Tests\search_api\Plugin\Processor\TestFieldsProcessorPlugin
+   * @var \Drupal\Tests\search_api\Unit\Plugin\Processor\TestFieldsProcessorPlugin
    */
   protected $processor;
 
@@ -302,18 +303,18 @@ class FieldsProcessorPluginBaseTest extends UnitTestCase {
    */
   public function testProcessFilters() {
     $query = Utility::createQuery($this->index);
-    $query->condition('text_field', 'foo');
-    $query->condition('string_field', NULL, '<>');
-    $query->condition('integer_field', 'bar');
+    $query->addCondition('text_field', 'foo');
+    $query->addCondition('string_field', NULL, '<>');
+    $query->addCondition('integer_field', 'bar');
 
     $this->processor->preprocessSearchQuery($query);
 
     $expected = array(
-      array('text_field', '*foo', '='),
-      array('string_field', 'undefined', '<>'),
-      array('integer_field', 'bar', '='),
+      new Condition('text_field', '*foo'),
+      new Condition('string_field', 'undefined', '<>'),
+      new Condition('integer_field', 'bar'),
     );
-    $this->assertEquals($expected, $query->getFilter()->getFilters(), 'Filters were preprocessed correctly.');
+    $this->assertEquals($expected, $query->getConditionGroup()->getConditions(), 'Filters were preprocessed correctly.');
   }
 
   /**
@@ -321,20 +322,20 @@ class FieldsProcessorPluginBaseTest extends UnitTestCase {
    */
   public function testProcessFiltersNestedFilter() {
     $query = Utility::createQuery($this->index);
-    $filter = $query->createFilter();
-    $filter->condition('text_field', 'foo');
-    $filter->condition('string_field', NULL, '<>');
-    $filter->condition('integer_field', 'bar');
-    $query->filter($filter);
+    $conditions = $query->createConditionGroup();
+    $conditions->addCondition('text_field', 'foo');
+    $conditions->addCondition('string_field', NULL, '<>');
+    $conditions->addCondition('integer_field', 'bar');
+    $query->addConditionGroup($conditions);
 
     $this->processor->preprocessSearchQuery($query);
 
     $expected = array(
-      array('text_field', '*foo', '='),
-      array('string_field', 'undefined', '<>'),
-      array('integer_field', 'bar', '='),
+      new Condition('text_field', '*foo'),
+      new Condition('string_field', 'undefined', '<>'),
+      new Condition('integer_field', 'bar'),
     );
-    $this->assertEquals($expected, $query->getFilter()->getFilters()[0]->getFilters(), 'Filters were preprocessed correctly.');
+    $this->assertEquals($expected, $query->getConditionGroup()->getConditions()[0]->getConditions(), 'Filters were preprocessed correctly.');
   }
 
   /**
@@ -349,17 +350,17 @@ class FieldsProcessorPluginBaseTest extends UnitTestCase {
     $this->processor->setMethodOverride('processFilterValue', $override);
 
     $query = Utility::createQuery($this->index);
-    $query->condition('text_field', 'foo');
-    $query->condition('string_field', NULL, '<>');
-    $query->condition('integer_field', 'bar');
+    $query->addCondition('text_field', 'foo');
+    $query->addCondition('string_field', NULL, '<>');
+    $query->addCondition('integer_field', 'bar');
 
     $this->processor->preprocessSearchQuery($query);
 
     $expected = array(
-      array('string_field', NULL, '<>'),
-      array('integer_field', 'bar', '='),
+      new Condition('string_field', NULL, '<>'),
+      new Condition('integer_field', 'bar'),
     );
-    $this->assertEquals($expected, array_merge($query->getFilter()->getFilters()), 'Filters were preprocessed correctly.');
+    $this->assertEquals($expected, array_merge($query->getConditionGroup()->getConditions()), 'Filters were preprocessed correctly.');
   }
 
   /**
