diff --git a/search_api.module b/search_api.module
index 15a302d..8239469 100644
--- a/search_api.module
+++ b/search_api.module
@@ -141,7 +141,7 @@ function search_api_config_import_steps_alter(&$sync_steps, ConfigImporter $conf
   $prefix_length = strlen($prefix);
   foreach ($new_or_changed as $config_id) {
     if (substr($config_id, 0, $prefix_length) === $prefix) {
-      $sync_steps[] = array('Drupal\search_api\Utility', 'processIndexTasks');
+      $sync_steps[] = array('Drupal\search_api\Task\IndexTaskManager', 'processIndexTasks');
     }
   }
 }
diff --git a/search_api.services.yml b/search_api.services.yml
index bb956e7..f73d0f3 100644
--- a/search_api.services.yml
+++ b/search_api.services.yml
@@ -50,6 +50,10 @@ services:
     tags:
       - { name: event_subscriber }
 
+  search_api.fields_helper:
+    class: \Drupal\search_api\Item\FieldsHelper
+    arguments: ['@entity_field.manager', '@entity_type.bundle.info', '@module_handler', '@plugin.manager.search_api.data_type']
+
   search_api.index_task_manager:
     class: Drupal\search_api\Task\IndexTaskManager
     arguments: ['@search_api.task_manager', '@entity_type.manager']
diff --git a/src/Utility.php b/src/Item/FieldsHelper.php
similarity index 27%
copy from src/Utility.php
copy to src/Item/FieldsHelper.php
index ecbf26d..02effdc 100644
--- a/src/Utility.php
+++ b/src/Item/FieldsHelper.php
@@ -1,10 +1,12 @@
 <?php
 
-namespace Drupal\search_api;
+namespace Drupal\search_api\Item;
 
-use Drupal\Core\Config\ConfigImporter;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\Core\TypedData\DataDefinitionInterface;
@@ -14,59 +16,91 @@
 use Drupal\Core\TypedData\ListInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\search_api\Datasource\DatasourceInterface;
-use Drupal\search_api\Entity\Index;
-use Drupal\search_api\Item\Field;
-use Drupal\search_api\Item\FieldInterface;
-use Drupal\search_api\Item\Item;
-use Drupal\search_api\Plugin\search_api\data_type\value\TextToken;
+use Drupal\search_api\DataType\DataTypePluginManager;
+use Drupal\search_api\IndexInterface;
 use Drupal\search_api\Processor\ConfigurablePropertyInterface;
-use Drupal\search_api\Query\Query;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Utility;
 use Symfony\Component\DependencyInjection\Container;
 
 /**
- * Contains utility methods for the Search API.
- *
- * @todo Maybe move some of these methods to other classes (and/or split this
- *   class into several utility classes).
+ * Provides helper methods for dealing with Search API fields and properties.
  */
-class Utility {
+class FieldsHelper implements FieldsHelperInterface {
 
   /**
-   * Static cache for the field type mapping.
+   * The entity field manager.
    *
-   * @var array
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The entity type bundle info service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $entityBundleInfo;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The data type plugin manager.
+   *
+   * @var \Drupal\search_api\DataType\DataTypePluginManager
+   */
+  protected $dataTypeManager;
+
+  /**
+   * Cache for the field type mapping.
+   *
+   * @var array|null
    *
    * @see getFieldTypeMapping()
    */
-  protected static $fieldTypeMapping = array();
+  protected $fieldTypeMapping;
 
   /**
-   * Static cache for the fallback data type mapping per index.
+   * Cache for the fallback data type mapping per index.
    *
    * @var array
    *
    * @see getDataTypeFallbackMapping()
    */
-  protected static $dataTypeFallbackMapping = array();
+  protected $dataTypeFallbackMapping = array();
 
   /**
-   * Determines whether fields of the given type contain fulltext data.
-   *
-   * @param string $type
-   *   The type to check.
-   * @param string[] $text_types
-   *   (optional) An array of types to be considered as text.
-   *
-   * @return bool
-   *   TRUE if $type is one of the specified types, FALSE otherwise.
+   * Constructs a FieldsHelper object.
+   *
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
+   *   The entity field manager.
+   * @param EntityTypeBundleInfoInterface $entityBundleInfo
+   *   The entity type bundle info service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   The module handler.
+   * @param \Drupal\search_api\DataType\DataTypePluginManager $dataTypeManager
+   *   The data type plugin manager.
+   */
+  public function __construct(EntityFieldManagerInterface $entityFieldManager, EntityTypeBundleInfoInterface $entityBundleInfo, ModuleHandlerInterface $moduleHandler, DataTypePluginManager $dataTypeManager) {
+    $this->entityFieldManager = $entityFieldManager;
+    $this->entityBundleInfo = $entityBundleInfo;
+    $this->moduleHandler = $moduleHandler;
+    $this->dataTypeManager = $dataTypeManager;
+  }
+
+  /**
+   * {@inheritdoc}
    */
-  public static function isTextType($type, array $text_types = array('text')) {
+  public function isTextType($type, array $text_types = array('text')) {
     if (in_array($type, $text_types)) {
       return TRUE;
     }
-    $data_type = \Drupal::getContainer()
-      ->get('plugin.manager.search_api.data_type')
-      ->createInstance($type);
+    $data_type = $this->dataTypeManager->createInstance($type);
     if ($data_type && !$data_type->isDefault()) {
       return in_array($data_type->getFallbackType(), $text_types);
     }
@@ -74,18 +108,11 @@ public static function isTextType($type, array $text_types = array('text')) {
   }
 
   /**
-   * Retrieves the mapping for known data types to Search API's internal types.
-   *
-   * @return string[]
-   *   An array mapping all known (and supported) Drupal data types to their
-   *   corresponding Search API data types. Empty values mean that fields of
-   *   that type should be ignored by the Search API.
-   *
-   * @see hook_search_api_field_type_mapping_alter()
+   * {@inheritdoc}
    */
-  public static function getFieldTypeMapping() {
-    // Check the static cache first.
-    if (empty(static::$fieldTypeMapping)) {
+  public function getFieldTypeMapping() {
+    // Check the cache first.
+    if (!isset($this->fieldTypeMapping)) {
       // It's easier to write and understand this array in the form of
       // $search_api_field_type => array($data_types) and flip it below.
       $default_mapping = array(
@@ -133,28 +160,21 @@ public static function getFieldTypeMapping() {
 
       // Allow other modules to intercept and define what default type they want
       // to use for their data type.
-      \Drupal::moduleHandler()->alter('search_api_field_type_mapping', $mapping);
+      $this->moduleHandler->alter('search_api_field_type_mapping', $mapping);
 
-      static::$fieldTypeMapping = $mapping;
+      $this->fieldTypeMapping = $mapping;
     }
 
-    return static::$fieldTypeMapping;
+    return $this->fieldTypeMapping;
   }
 
   /**
-   * Retrieves the necessary type fallbacks for an index.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The index for which to return the type fallbacks.
-   *
-   * @return string[]
-   *   An array containing the IDs of all custom data types that are not
-   *   supported by the index's current server, mapped to their fallback types.
+   * {@inheritdoc}
    */
-  public static function getDataTypeFallbackMapping(IndexInterface $index) {
-    // Check the static cache first.
+  public function getDataTypeFallbackMapping(IndexInterface $index) {
+    // Check the cache first.
     $index_id = $index->id();
-    if (empty(static::$dataTypeFallbackMapping[$index_id])) {
+    if (empty($this->dataTypeFallbackMapping[$index_id])) {
       $server = NULL;
       try {
         $server = $index->getServerInstance();
@@ -163,36 +183,25 @@ public static function getDataTypeFallbackMapping(IndexInterface $index) {
         // If the server isn't available, just ignore it here and return all
         // custom types.
       }
-      static::$dataTypeFallbackMapping[$index_id] = array();
-      $data_types = \Drupal::getContainer()
-        ->get('plugin.manager.search_api.data_type')
-        ->getInstances();
+      $this->dataTypeFallbackMapping[$index_id] = array();
+      $data_types = $this->dataTypeManager->getInstances();
       foreach ($data_types as $type_id => $data_type) {
         // We know for sure that we do not need to fall back for the default
         // data types as they are always present and are required to be
         // supported by all backends.
         if (!$data_type->isDefault() && (!$server || !$server->supportsDataType($type_id))) {
-          static::$dataTypeFallbackMapping[$index_id][$type_id] = $data_type->getFallbackType();
+          $this->dataTypeFallbackMapping[$index_id][$type_id] = $data_type->getFallbackType();
         }
       }
     }
 
-    return static::$dataTypeFallbackMapping[$index_id];
+    return $this->dataTypeFallbackMapping[$index_id];
   }
 
   /**
-   * Extracts specific field values from a complex data object.
-   *
-   * The values will be set directly on the given field objects, nothing is
-   * returned.
-   *
-   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
-   *   The item from which fields should be extracted.
-   * @param \Drupal\search_api\Item\FieldInterface[][] $fields
-   *   An associative array, keyed by property paths, mapped to field objects
-   *   with that property path.
+   * {@inheritdoc}
    */
-  public static function extractFields(ComplexDataInterface $item, array $fields) {
+  public function extractFields(ComplexDataInterface $item, array $fields) {
     // Figure out which fields are directly on the item and which need to be
     // extracted from nested items.
     $direct_fields = array();
@@ -207,56 +216,46 @@ public static function extractFields(ComplexDataInterface $item, array $fields)
       }
     }
     // Extract the direct fields.
+    $properties = $item->getProperties(TRUE);
     foreach ($direct_fields as $key) {
-      try {
-        $data = $item->get($key);
-        foreach ($fields[$key] as $field) {
-          self::extractField($data, $field);
-        }
+      if (empty($properties[$key])) {
+        continue;
       }
-      catch (\InvalidArgumentException $e) {
-        // This can happen with properties added by processors.
-        // @todo Find a cleaner solution for this.
+      $data = $item->get($key);
+      foreach ($fields[$key] as $field) {
+        $this->extractField($data, $field);
       }
     }
     // Recurse for all nested fields.
     foreach ($nested_fields as $direct => $fields_nested) {
-      try {
-        $item_nested = $item->get($direct);
-        if ($item_nested instanceof DataReferenceInterface) {
-          $item_nested = $item_nested->getTarget();
-        }
-        if ($item_nested instanceof EntityInterface) {
-          $item_nested = $item_nested->getTypedData();
-        }
-        if ($item_nested instanceof ComplexDataInterface && !$item_nested->isEmpty()) {
-          self::extractFields($item_nested, $fields_nested);
-        }
-        elseif ($item_nested instanceof ListInterface && !$item_nested->isEmpty()) {
-          foreach ($item_nested as $list_item) {
-            if ($list_item instanceof ComplexDataInterface && !$list_item->isEmpty()) {
-              self::extractFields($list_item, $fields_nested);
-            }
+      if (empty($properties[$direct])) {
+        continue;
+      }
+      $item_nested = $item->get($direct);
+      if ($item_nested instanceof DataReferenceInterface) {
+        $item_nested = $item_nested->getTarget();
+      }
+      if ($item_nested instanceof EntityInterface) {
+        $item_nested = $item_nested->getTypedData();
+      }
+      if ($item_nested instanceof ComplexDataInterface && !$item_nested->isEmpty()) {
+        $this->extractFields($item_nested, $fields_nested);
+      }
+      elseif ($item_nested instanceof ListInterface && !$item_nested->isEmpty()) {
+        foreach ($item_nested as $list_item) {
+          if ($list_item instanceof ComplexDataInterface && !$list_item->isEmpty()) {
+            $this->extractFields($list_item, $fields_nested);
           }
         }
       }
-      catch (\InvalidArgumentException $e) {
-        // This can happen with properties added by processors.
-        // @todo Find a cleaner solution for this.
-      }
     }
   }
 
   /**
-   * Extracts value and original type from a single piece of data.
-   *
-   * @param \Drupal\Core\TypedData\TypedDataInterface $data
-   *   The piece of data from which to extract information.
-   * @param \Drupal\search_api\Item\FieldInterface $field
-   *   The field into which to put the extracted data.
+   * {@inheritdoc}
    */
-  public static function extractField(TypedDataInterface $data, FieldInterface $field) {
-    $values = static::extractFieldValues($data);
+  public function extractField(TypedDataInterface $data, FieldInterface $field) {
+    $values = $this->extractFieldValues($data);
 
     foreach ($values as $i => $value) {
       $field->addValue($value);
@@ -265,19 +264,13 @@ public static function extractField(TypedDataInterface $data, FieldInterface $fi
   }
 
   /**
-   * Extracts field values from a typed data object.
-   *
-   * @param \Drupal\Core\TypedData\TypedDataInterface $data
-   *   The typed data object.
-   *
-   * @return array
-   *   An array of values.
+   * {@inheritdoc}
    */
-  public static function extractFieldValues(TypedDataInterface $data) {
+  public function extractFieldValues(TypedDataInterface $data) {
     if ($data->getDataDefinition()->isList()) {
       $values = array();
       foreach ($data as $piece) {
-        $values[] = self::extractFieldValues($piece);
+        $values[] = $this->extractFieldValues($piece);
       }
       return $values ? call_user_func_array('array_merge', $values) : array();
     }
@@ -297,26 +290,16 @@ public static function extractFieldValues(TypedDataInterface $data) {
   }
 
   /**
-   * Retrieves a list of nested properties from a complex property.
-   *
-   * Takes care of including bundle-specific properties for entity reference
-   * properties.
-   *
-   * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface $property
-   *   The base definition.
-   *
-   * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
-   *   The nested properties, keyed by property name.
+   * {@inheritdoc}
    */
-  public static function getNestedProperties(ComplexDataDefinitionInterface $property) {
+  public function getNestedProperties(ComplexDataDefinitionInterface $property) {
     $nested_properties = $property->getPropertyDefinitions();
     if ($property instanceof EntityDataDefinitionInterface) {
-      $container = \Drupal::getContainer();
-      $bundles = $container->get('entity_type.bundle.info')
+      $bundles = $this->entityBundleInfo
         ->getBundleInfo($property->getEntityTypeId());
-      $field_manager = $container->get('entity_field.manager');
       foreach ($bundles as $bundle => $bundle_label) {
-        $bundle_properties = $field_manager->getFieldDefinitions($property->getEntityTypeId(), $bundle);
+        $bundle_properties = $this->entityFieldManager
+          ->getFieldDefinitions($property->getEntityTypeId(), $bundle);
         $nested_properties += $bundle_properties;
       }
     }
@@ -324,23 +307,15 @@ public static function getNestedProperties(ComplexDataDefinitionInterface $prope
   }
 
   /**
-   * Retrieves a nested property from a list of properties.
-   *
-   * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
-   *   The base properties, keyed by property name.
-   * @param string $property_path
-   *   The property path of the property to retrieve.
-   *
-   * @return \Drupal\Core\TypedData\DataDefinitionInterface|null
-   *   The requested property, or NULL if it couldn't be found.
+   * {@inheritdoc}
    */
-  public static function retrieveNestedProperty(array $properties, $property_path) {
-    list($key, $nested_path) = static::splitPropertyPath($property_path, FALSE);
+  public function retrieveNestedProperty(array $properties, $property_path) {
+    list($key, $nested_path) = Utility::splitPropertyPath($property_path, FALSE);
     if (!isset($properties[$key])) {
       return NULL;
     }
 
-    $property = static::getInnerProperty($properties[$key]);
+    $property = $this->getInnerProperty($properties[$key]);
     if (!isset($nested_path)) {
       return $property;
     }
@@ -349,22 +324,13 @@ public static function retrieveNestedProperty(array $properties, $property_path)
       return NULL;
     }
 
-    return static::retrieveNestedProperty(static::getNestedProperties($property), $nested_path);
+    return $this->retrieveNestedProperty($this->getNestedProperties($property), $nested_path);
   }
 
   /**
-   * Retrieves the inner property definition of a compound property definition.
-   *
-   * This will retrieve the list item type from a list data definition or the
-   * definition of the referenced data from a reference data definition.
-   *
-   * @param \Drupal\Core\TypedData\DataDefinitionInterface $property
-   *   The original property definition.
-   *
-   * @return \Drupal\Core\TypedData\DataDefinitionInterface
-   *   The inner property definition.
+   * {@inheritdoc}
    */
-  public static function getInnerProperty(DataDefinitionInterface $property) {
+  public function getInnerProperty(DataDefinitionInterface $property) {
     while ($property instanceof ListDataDefinitionInterface) {
       $property = $property->getItemDefinition();
     }
@@ -375,204 +341,38 @@ public static function getInnerProperty(DataDefinitionInterface $property) {
   }
 
   /**
-   * Splits a property path into two parts along a path separator (:).
-   *
-   * The path is split into one part with a single property name, and one part
-   * with the complete rest of the property path (which might be empty).
-   * Depending on $separate_last the returned single property key will be the
-   * first (FALSE) or last (TRUE) property of the path.
-   *
-   * @param string $property_path
-   *   The property path to split.
-   * @param bool $separate_last
-   *   (optional) If FALSE, separate the first property of the path. By default,
-   *   the last property is separated from the rest.
-   * @param string $separator
-   *   (optional) The separator to use.
-   *
-   * @return string[]
-   *   An array with indexes 0 and 1, 0 containing the first part of the
-   *   property path and 1 the second. If $separate_last is FALSE, index 0 will
-   *   always contain a single property name (without any colons) and index 1
-   *   might be NULL. If $separate_last is TRUE it's the exact other way round.
-   */
-  public static function splitPropertyPath($property_path, $separate_last = TRUE, $separator = ':') {
-    $function = $separate_last ? 'strrpos' : 'strpos';
-    $pos = $function($property_path, $separator);
-    if ($pos !== FALSE) {
-      return array(
-        substr($property_path, 0, $pos),
-        substr($property_path, $pos + 1),
-      );
-    }
-
-    return $separate_last ? array(NULL, $property_path) : array($property_path, NULL);
-  }
-
-  /**
-   * Determines whether a field ID is reserved for special use.
-   *
-   * We define all field IDs starting with "search_api_" as reserved, to be safe
-   * for future additions (and from clashing with backend-defined fields).
-   *
-   * @param string $field_id
-   *   The field ID.
-   *
-   * @return bool
-   *   TRUE if the field ID is reserved, FALSE if it can be used normally.
+   * {@inheritdoc}
    */
-  public static function isFieldIdReserved($field_id) {
+  public function isFieldIdReserved($field_id) {
     return substr($field_id, 0, 11) == 'search_api_';
   }
 
   /**
-   * Processes all pending index tasks inside a batch run.
-   *
-   * @param array $context
-   *   The current batch context.
-   * @param \Drupal\Core\Config\ConfigImporter $config_importer
-   *   The config importer.
-   *
-   * @throws \Drupal\search_api\SearchApiException
-   *   Thrown if any error occurred while tracking items.
+   * {@inheritdoc}
    */
-  public static function processIndexTasks(array &$context, ConfigImporter $config_importer) {
-    $index_task_manager = \Drupal::getContainer()->get('search_api.index_task_manager');
-
-    if (!isset($context['sandbox']['indexes'])) {
-      $context['sandbox']['indexes'] = array();
-
-      $indexes = \Drupal::entityTypeManager()
-        ->getStorage('search_api_index')
-        ->loadByProperties(array(
-          'status' => TRUE,
-        ));
-      $deleted = $config_importer->getUnprocessedConfiguration('delete');
-
-      /** @var \Drupal\search_api\IndexInterface $index */
-      foreach ($indexes as $index_id => $index) {
-        if (!$index_task_manager->isTrackingComplete($index) && !in_array($index->getConfigDependencyName(), $deleted)) {
-          $context['sandbox']['indexes'][] = $index_id;
-        }
-      }
-      $context['sandbox']['total'] = count($context['sandbox']['indexes']);
-      if (!$context['sandbox']['total']) {
-        $context['finished'] = 1;
-        return;
-      }
-    }
-
-    try {
-      $index_id = array_shift($context['sandbox']['indexes']);
-      $index = Index::load($index_id);
-      if (!($index_task_manager->addItemsOnce($index))) {
-        array_unshift($context['sandbox']['indexes'], $index_id);
-      }
-    }
-    catch (SearchApiException $e) {
-      watchdog_exception('search_api', $e);
-    }
-
-    if (empty($context['sandbox']['indexes'])) {
-      $context['finished'] = 1;
-    }
-    else {
-      $finished = $context['sandbox']['total'] - count($context['sandbox']['indexes']);
-      $context['finished'] = $finished / $context['sandbox']['total'];
-      $args = array(
-        '%index' => $index->label(),
-        '@num' => $finished + 1,
-        '@total' => $context['sandbox']['total'],
-      );
-      $context['message'] = \Drupal::translation()->translate('Tracking items for search index %index (@num of @total)', $args);
-    }
-  }
-
-  /**
-   * Creates a new search query object.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The index on which to search.
-   * @param array $options
-   *   (optional) The options to set for the query. See
-   *   \Drupal\search_api\Query\QueryInterface::setOption() for a list of
-   *   options that are recognized by default.
-   *
-   * @return \Drupal\search_api\Query\QueryInterface
-   *   A search query object to use.
-   *
-   * @see \Drupal\search_api\Query\QueryInterface::create()
-   */
-  public static function createQuery(IndexInterface $index, array $options = array()) {
-    $search_results_cache = \Drupal::service('search_api.results_static_cache');
-    return Query::create($index, $search_results_cache, $options);
-  }
-
-  /**
-   * Creates a search item object.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The item's search index.
-   * @param string $id
-   *   The item's (combined) ID.
-   * @param \Drupal\search_api\Datasource\DatasourceInterface|null $datasource
-   *   (optional) The datasource of the item. If not set, it will be determined
-   *   from the ID and loaded from the index if needed.
-   *
-   * @return \Drupal\search_api\Item\ItemInterface
-   *   A search item with the given values.
-   */
-  public static function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
+  public function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
     return new Item($index, $id, $datasource);
   }
 
   /**
-   * Creates a search item object by wrapping an existing complex data object.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The item's search index.
-   * @param \Drupal\Core\TypedData\ComplexDataInterface $original_object
-   *   The original object to wrap.
-   * @param string $id
-   *   (optional) The item's (combined) ID. If not set, it will be determined
-   *   with the \Drupal\search_api\Datasource\DatasourceInterface::getItemId()
-   *   method of $datasource. In this case, $datasource must not be NULL.
-   * @param \Drupal\search_api\Datasource\DatasourceInterface|null $datasource
-   *   (optional) The datasource of the item. If not set, it will be determined
-   *   from the ID and loaded from the index if needed.
-   *
-   * @return \Drupal\search_api\Item\ItemInterface
-   *   A search item with the given values.
-   *
-   * @throws \InvalidArgumentException
-   *   Thrown if both $datasource and $id are NULL.
+   * {@inheritdoc}
    */
-  public static function createItemFromObject(IndexInterface $index, ComplexDataInterface $original_object, $id = NULL, DatasourceInterface $datasource = NULL) {
+  public function createItemFromObject(IndexInterface $index, ComplexDataInterface $original_object, $id = NULL, DatasourceInterface $datasource = NULL) {
     if (!isset($id)) {
       if (!isset($datasource)) {
         throw new \InvalidArgumentException('Need either an item ID or the datasource to create a search item from an object.');
       }
-      $id = self::createCombinedId($datasource->getPluginId(), $datasource->getItemId($original_object));
+      $id = Utility::createCombinedId($datasource->getPluginId(), $datasource->getItemId($original_object));
     }
-    $item = static::createItem($index, $id, $datasource);
+    $item = $this->createItem($index, $id, $datasource);
     $item->setOriginalObject($original_object);
     return $item;
   }
 
   /**
-   * Creates a new field object wrapping a field of the given index.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The index to which this field should be attached.
-   * @param string $field_identifier
-   *   The field identifier.
-   * @param array $field_info
-   *   (optional) An array with further configuration for the field.
-   *
-   * @return \Drupal\search_api\Item\FieldInterface
-   *   A new field object.
+   * {@inheritdoc}
    */
-  public static function createField(IndexInterface $index, $field_identifier, $field_info = array()) {
+  public function createField(IndexInterface $index, $field_identifier, $field_info = array()) {
     $field = new Field($index, $field_identifier);
 
     foreach ($field_info as $key => $value) {
@@ -586,40 +386,15 @@ public static function createField(IndexInterface $index, $field_identifier, $fi
   }
 
   /**
-   * Creates a new field on an index based on a property.
-   *
-   * Will find and set a new unique field identifier for the field on the index.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The search index.
-   * @param \Drupal\Core\TypedData\DataDefinitionInterface $property
-   *   The data definition of the property.
-   * @param string|null $datasource_id
-   *   The ID of the index's datasource this property belongs to, or NULL if it
-   *   is a datasource-independent property.
-   * @param string $property_path
-   *   The property's property path within the property structure of the
-   *   datasource.
-   * @param string|null $field_id
-   *   (optional) The identifier to use for the field. If not set, a new unique
-   *   field identifier on the index will be chosen automatically.
-   * @param string|null $type
-   *   (optional) The type to set for the field, or NULL to determine a default
-   *   type automatically.
-   *
-   * @return \Drupal\search_api\Item\FieldInterface
-   *   A new field object for the index, based on the given property.
-   *
-   * @throws \Drupal\search_api\SearchApiException
-   *   Thrown if no type was given and no default could be determined.
+   * {@inheritdoc}
    */
-  public static function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasource_id, $property_path, $field_id = NULL, $type = NULL) {
+  public function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasource_id, $property_path, $field_id = NULL, $type = NULL) {
     if (!isset($field_id)) {
-      $field_id = static::getNewFieldId($index, $property_path);
+      $field_id = $this->getNewFieldId($index, $property_path);
     }
 
     if (!isset($type)) {
-      $type_mapping = static::getFieldTypeMapping();
+      $type_mapping = $this->getFieldTypeMapping();
       $property_type = $property->getDataType();
       if (isset($type_mapping[$property_type])) {
         $type = $type_mapping[$property_type];
@@ -639,23 +414,14 @@ public static function createFieldFromProperty(IndexInterface $index, DataDefini
     if ($property instanceof ConfigurablePropertyInterface) {
       $field_info['configuration'] = $property->defaultConfiguration();
     }
-    return self::createField($index, $field_id, $field_info);
+    return $this->createField($index, $field_id, $field_info);
   }
 
   /**
-   * Finds a new unique field identifier on the given index.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The search index.
-   * @param string $property_path
-   *   The property path on which the field identifier should be based. Only the
-   *   last component of the property path will be considered.
-   *
-   * @return string
-   *   A new unique field identifier on the given index.
+   * {@inheritdoc}
    */
-  public static function getNewFieldId(IndexInterface $index, $property_path) {
-    list(, $suggested_id) = static::splitPropertyPath($property_path);
+  public function getNewFieldId(IndexInterface $index, $property_path) {
+    list(, $suggested_id) = Utility::splitPropertyPath($property_path);
 
     // Avoid clashes with reserved IDs by removing the reserved "search_api_"
     // from our suggested ID.
@@ -667,109 +433,11 @@ public static function getNewFieldId(IndexInterface $index, $property_path) {
       $field_id = $suggested_id . '_' . ++$i;
     }
 
-    while (static::isFieldIdReserved($field_id)) {
+    while ($this->isFieldIdReserved($field_id)) {
       $field_id = '_' . $field_id;
     }
 
     return $field_id;
   }
 
-  /**
-   * Creates a single text token.
-   *
-   * @param string $value
-   *   The word or other token value.
-   * @param float $score
-   *   (optional) The token's score.
-   *
-   * @return \Drupal\search_api\Plugin\search_api\data_type\value\TextTokenInterface
-   *   A text token object.
-   */
-  public static function createTextToken($value, $score = 1.0) {
-    return new TextToken($value, (float) $score);
-  }
-
-  /**
-   * Returns a deep copy of the input array.
-   *
-   * The behavior of PHP regarding arrays with references pointing to it is
-   * rather weird. Therefore, this method should be used when making a copy of
-   * such an array, or of an array containing references.
-   *
-   * This method will also omit empty array elements (i.e., elements that
-   * evaluate to FALSE according to PHP's native rules).
-   *
-   * @param array $array
-   *   The array to copy.
-   *
-   * @return array
-   *   A deep copy of the array.
-   */
-  public static function deepCopy(array $array) {
-    $copy = array();
-    foreach ($array as $k => $v) {
-      if (is_array($v)) {
-        if ($v = static::deepCopy($v)) {
-          $copy[$k] = $v;
-        }
-      }
-      elseif (is_object($v)) {
-        $copy[$k] = clone $v;
-      }
-      elseif ($v) {
-        $copy[$k] = $v;
-      }
-    }
-    return $copy;
-  }
-
-  /**
-   * Creates a combined ID from a raw ID and an optional datasource prefix.
-   *
-   * This can be used to created an internal item ID from a datasource ID and a
-   * datasource-specific raw item ID, or a combined property path from a
-   * datasource ID and a property path to identify properties index-wide.
-   *
-   * @param string|null $datasource_id
-   *   The ID of the datasource to which the item belongs. Or NULL to return the
-   *   raw ID unchanged (option included for compatibility purposes).
-   * @param string $raw_id
-   *   The datasource-specific raw item ID of the item (or property).
-   *
-   * @return string
-   *   The combined ID, with the datasource prefix separated by
-   *   \Drupal\search_api\IndexInterface::DATASOURCE_ID_SEPARATOR.
-   */
-  public static function createCombinedId($datasource_id, $raw_id) {
-    if (!isset($datasource_id)) {
-      return $raw_id;
-    }
-    return $datasource_id . IndexInterface::DATASOURCE_ID_SEPARATOR . $raw_id;
-  }
-
-  /**
-   * Splits an internal ID into its two parts.
-   *
-   * Both internal item IDs and combined property paths are prefixed with the
-   * corresponding datasource ID. This method will split these IDs up again into
-   * their two parts.
-   *
-   * @param string $combined_id
-   *   The internal ID, with an optional datasource prefix separated with
-   *   \Drupal\search_api\IndexInterface::DATASOURCE_ID_SEPARATOR from the
-   *   raw item ID or property path.
-   *
-   * @return array
-   *   A numeric array, containing the datasource ID in element 0 and the raw
-   *   item ID or property path in element 1. In the case of
-   *   datasource-independent properties (i.e., when there is no prefix),
-   *   element 0 will be NULL.
-   */
-  public static function splitCombinedId($combined_id) {
-    if (strpos($combined_id, IndexInterface::DATASOURCE_ID_SEPARATOR) !== FALSE) {
-      return explode(IndexInterface::DATASOURCE_ID_SEPARATOR, $combined_id, 2);
-    }
-    return array(NULL, $combined_id);
-  }
-
 }
diff --git a/src/Utility.php b/src/Item/FieldsHelperInterface.php
similarity index 27%
copy from src/Utility.php
copy to src/Item/FieldsHelperInterface.php
index ecbf26d..84450f9 100644
--- a/src/Utility.php
+++ b/src/Item/FieldsHelperInterface.php
@@ -1,53 +1,18 @@
 <?php
 
-namespace Drupal\search_api;
+namespace Drupal\search_api\Item;
 
-use Drupal\Core\Config\ConfigImporter;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
 use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\Core\TypedData\DataDefinitionInterface;
-use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
-use Drupal\Core\TypedData\DataReferenceInterface;
-use Drupal\Core\TypedData\ListDataDefinitionInterface;
-use Drupal\Core\TypedData\ListInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\search_api\Datasource\DatasourceInterface;
-use Drupal\search_api\Entity\Index;
-use Drupal\search_api\Item\Field;
-use Drupal\search_api\Item\FieldInterface;
-use Drupal\search_api\Item\Item;
-use Drupal\search_api\Plugin\search_api\data_type\value\TextToken;
-use Drupal\search_api\Processor\ConfigurablePropertyInterface;
-use Drupal\search_api\Query\Query;
-use Symfony\Component\DependencyInjection\Container;
+use Drupal\search_api\IndexInterface;
 
 /**
- * Contains utility methods for the Search API.
  *
- * @todo Maybe move some of these methods to other classes (and/or split this
- *   class into several utility classes).
  */
-class Utility {
-
-  /**
-   * Static cache for the field type mapping.
-   *
-   * @var array
-   *
-   * @see getFieldTypeMapping()
-   */
-  protected static $fieldTypeMapping = array();
-
-  /**
-   * Static cache for the fallback data type mapping per index.
-   *
-   * @var array
-   *
-   * @see getDataTypeFallbackMapping()
-   */
-  protected static $dataTypeFallbackMapping = array();
+interface FieldsHelperInterface {
 
   /**
    * Determines whether fields of the given type contain fulltext data.
@@ -60,18 +25,7 @@ class Utility {
    * @return bool
    *   TRUE if $type is one of the specified types, FALSE otherwise.
    */
-  public static function isTextType($type, array $text_types = array('text')) {
-    if (in_array($type, $text_types)) {
-      return TRUE;
-    }
-    $data_type = \Drupal::getContainer()
-      ->get('plugin.manager.search_api.data_type')
-      ->createInstance($type);
-    if ($data_type && !$data_type->isDefault()) {
-      return in_array($data_type->getFallbackType(), $text_types);
-    }
-    return FALSE;
-  }
+  public function isTextType($type, array $text_types = array('text'));
 
   /**
    * Retrieves the mapping for known data types to Search API's internal types.
@@ -83,63 +37,7 @@ public static function isTextType($type, array $text_types = array('text')) {
    *
    * @see hook_search_api_field_type_mapping_alter()
    */
-  public static function getFieldTypeMapping() {
-    // Check the static cache first.
-    if (empty(static::$fieldTypeMapping)) {
-      // It's easier to write and understand this array in the form of
-      // $search_api_field_type => array($data_types) and flip it below.
-      $default_mapping = array(
-        'text' => array(
-          'field_item:string_long.string',
-          'field_item:text_long.string',
-          'field_item:text_with_summary.string',
-          'text',
-        ),
-        'string' => array(
-          'string',
-          'email',
-          'uri',
-          'filter_format',
-          'duration_iso8601',
-          'field_item:path',
-        ),
-        'integer' => array(
-          'integer',
-          'timespan',
-        ),
-        'decimal' => array(
-          'decimal',
-          'float',
-        ),
-        'date' => array(
-          'date',
-          'datetime_iso8601',
-          'timestamp',
-        ),
-        'boolean' => array(
-          'boolean',
-        ),
-        // Types we know about but want/have to ignore.
-        NULL => array(
-          'language',
-        ),
-      );
-
-      foreach ($default_mapping as $search_api_type => $data_types) {
-        foreach ($data_types as $data_type) {
-          $mapping[$data_type] = $search_api_type;
-        }
-      }
-
-      // Allow other modules to intercept and define what default type they want
-      // to use for their data type.
-      \Drupal::moduleHandler()->alter('search_api_field_type_mapping', $mapping);
-
-      static::$fieldTypeMapping = $mapping;
-    }
-
-    return static::$fieldTypeMapping;
-  }
+  public function getFieldTypeMapping();
 
   /**
    * Retrieves the necessary type fallbacks for an index.
@@ -151,34 +49,7 @@ public static function getFieldTypeMapping() {
    *   An array containing the IDs of all custom data types that are not
    *   supported by the index's current server, mapped to their fallback types.
    */
-  public static function getDataTypeFallbackMapping(IndexInterface $index) {
-    // Check the static cache first.
-    $index_id = $index->id();
-    if (empty(static::$dataTypeFallbackMapping[$index_id])) {
-      $server = NULL;
-      try {
-        $server = $index->getServerInstance();
-      }
-      catch (SearchApiException $e) {
-        // If the server isn't available, just ignore it here and return all
-        // custom types.
-      }
-      static::$dataTypeFallbackMapping[$index_id] = array();
-      $data_types = \Drupal::getContainer()
-        ->get('plugin.manager.search_api.data_type')
-        ->getInstances();
-      foreach ($data_types as $type_id => $data_type) {
-        // We know for sure that we do not need to fall back for the default
-        // data types as they are always present and are required to be
-        // supported by all backends.
-        if (!$data_type->isDefault() && (!$server || !$server->supportsDataType($type_id))) {
-          static::$dataTypeFallbackMapping[$index_id][$type_id] = $data_type->getFallbackType();
-        }
-      }
-    }
-
-    return static::$dataTypeFallbackMapping[$index_id];
-  }
+  public function getDataTypeFallbackMapping(IndexInterface $index);
 
   /**
    * Extracts specific field values from a complex data object.
@@ -192,60 +63,7 @@ public static function getDataTypeFallbackMapping(IndexInterface $index) {
    *   An associative array, keyed by property paths, mapped to field objects
    *   with that property path.
    */
-  public static function extractFields(ComplexDataInterface $item, array $fields) {
-    // Figure out which fields are directly on the item and which need to be
-    // extracted from nested items.
-    $direct_fields = array();
-    $nested_fields = array();
-    foreach (array_keys($fields) as $key) {
-      if (strpos($key, ':') !== FALSE) {
-        list($direct, $nested) = explode(':', $key, 2);
-        $nested_fields[$direct][$nested] = $fields[$key];
-      }
-      else {
-        $direct_fields[] = $key;
-      }
-    }
-    // Extract the direct fields.
-    foreach ($direct_fields as $key) {
-      try {
-        $data = $item->get($key);
-        foreach ($fields[$key] as $field) {
-          self::extractField($data, $field);
-        }
-      }
-      catch (\InvalidArgumentException $e) {
-        // This can happen with properties added by processors.
-        // @todo Find a cleaner solution for this.
-      }
-    }
-    // Recurse for all nested fields.
-    foreach ($nested_fields as $direct => $fields_nested) {
-      try {
-        $item_nested = $item->get($direct);
-        if ($item_nested instanceof DataReferenceInterface) {
-          $item_nested = $item_nested->getTarget();
-        }
-        if ($item_nested instanceof EntityInterface) {
-          $item_nested = $item_nested->getTypedData();
-        }
-        if ($item_nested instanceof ComplexDataInterface && !$item_nested->isEmpty()) {
-          self::extractFields($item_nested, $fields_nested);
-        }
-        elseif ($item_nested instanceof ListInterface && !$item_nested->isEmpty()) {
-          foreach ($item_nested as $list_item) {
-            if ($list_item instanceof ComplexDataInterface && !$list_item->isEmpty()) {
-              self::extractFields($list_item, $fields_nested);
-            }
-          }
-        }
-      }
-      catch (\InvalidArgumentException $e) {
-        // This can happen with properties added by processors.
-        // @todo Find a cleaner solution for this.
-      }
-    }
-  }
+  public function extractFields(ComplexDataInterface $item, array $fields);
 
   /**
    * Extracts value and original type from a single piece of data.
@@ -255,14 +73,7 @@ public static function extractFields(ComplexDataInterface $item, array $fields)
    * @param \Drupal\search_api\Item\FieldInterface $field
    *   The field into which to put the extracted data.
    */
-  public static function extractField(TypedDataInterface $data, FieldInterface $field) {
-    $values = static::extractFieldValues($data);
-
-    foreach ($values as $i => $value) {
-      $field->addValue($value);
-    }
-    $field->setOriginalType($data->getDataDefinition()->getDataType());
-  }
+  public function extractField(TypedDataInterface $data, FieldInterface $field);
 
   /**
    * Extracts field values from a typed data object.
@@ -273,28 +84,7 @@ public static function extractField(TypedDataInterface $data, FieldInterface $fi
    * @return array
    *   An array of values.
    */
-  public static function extractFieldValues(TypedDataInterface $data) {
-    if ($data->getDataDefinition()->isList()) {
-      $values = array();
-      foreach ($data as $piece) {
-        $values[] = self::extractFieldValues($piece);
-      }
-      return $values ? call_user_func_array('array_merge', $values) : array();
-    }
-
-    $value = $data->getValue();
-    $definition = $data->getDataDefinition();
-    if ($definition instanceof ComplexDataDefinitionInterface) {
-      $property = $definition->getMainPropertyName();
-      if (isset($value[$property])) {
-        return array($value[$property]);
-      }
-    }
-    elseif (is_array($value)) {
-      return array_values($value);
-    }
-    return array($value);
-  }
+  public function extractFieldValues(TypedDataInterface $data);
 
   /**
    * Retrieves a list of nested properties from a complex property.
@@ -308,20 +98,7 @@ public static function extractFieldValues(TypedDataInterface $data) {
    * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
    *   The nested properties, keyed by property name.
    */
-  public static function getNestedProperties(ComplexDataDefinitionInterface $property) {
-    $nested_properties = $property->getPropertyDefinitions();
-    if ($property instanceof EntityDataDefinitionInterface) {
-      $container = \Drupal::getContainer();
-      $bundles = $container->get('entity_type.bundle.info')
-        ->getBundleInfo($property->getEntityTypeId());
-      $field_manager = $container->get('entity_field.manager');
-      foreach ($bundles as $bundle => $bundle_label) {
-        $bundle_properties = $field_manager->getFieldDefinitions($property->getEntityTypeId(), $bundle);
-        $nested_properties += $bundle_properties;
-      }
-    }
-    return $nested_properties;
-  }
+  public function getNestedProperties(ComplexDataDefinitionInterface $property);
 
   /**
    * Retrieves a nested property from a list of properties.
@@ -334,23 +111,7 @@ public static function getNestedProperties(ComplexDataDefinitionInterface $prope
    * @return \Drupal\Core\TypedData\DataDefinitionInterface|null
    *   The requested property, or NULL if it couldn't be found.
    */
-  public static function retrieveNestedProperty(array $properties, $property_path) {
-    list($key, $nested_path) = static::splitPropertyPath($property_path, FALSE);
-    if (!isset($properties[$key])) {
-      return NULL;
-    }
-
-    $property = static::getInnerProperty($properties[$key]);
-    if (!isset($nested_path)) {
-      return $property;
-    }
-
-    if (!$property instanceof ComplexDataDefinitionInterface) {
-      return NULL;
-    }
-
-    return static::retrieveNestedProperty(static::getNestedProperties($property), $nested_path);
-  }
+  public function retrieveNestedProperty(array $properties, $property_path);
 
   /**
    * Retrieves the inner property definition of a compound property definition.
@@ -364,50 +125,7 @@ public static function retrieveNestedProperty(array $properties, $property_path)
    * @return \Drupal\Core\TypedData\DataDefinitionInterface
    *   The inner property definition.
    */
-  public static function getInnerProperty(DataDefinitionInterface $property) {
-    while ($property instanceof ListDataDefinitionInterface) {
-      $property = $property->getItemDefinition();
-    }
-    while ($property instanceof DataReferenceDefinitionInterface) {
-      $property = $property->getTargetDefinition();
-    }
-    return $property;
-  }
-
-  /**
-   * Splits a property path into two parts along a path separator (:).
-   *
-   * The path is split into one part with a single property name, and one part
-   * with the complete rest of the property path (which might be empty).
-   * Depending on $separate_last the returned single property key will be the
-   * first (FALSE) or last (TRUE) property of the path.
-   *
-   * @param string $property_path
-   *   The property path to split.
-   * @param bool $separate_last
-   *   (optional) If FALSE, separate the first property of the path. By default,
-   *   the last property is separated from the rest.
-   * @param string $separator
-   *   (optional) The separator to use.
-   *
-   * @return string[]
-   *   An array with indexes 0 and 1, 0 containing the first part of the
-   *   property path and 1 the second. If $separate_last is FALSE, index 0 will
-   *   always contain a single property name (without any colons) and index 1
-   *   might be NULL. If $separate_last is TRUE it's the exact other way round.
-   */
-  public static function splitPropertyPath($property_path, $separate_last = TRUE, $separator = ':') {
-    $function = $separate_last ? 'strrpos' : 'strpos';
-    $pos = $function($property_path, $separator);
-    if ($pos !== FALSE) {
-      return array(
-        substr($property_path, 0, $pos),
-        substr($property_path, $pos + 1),
-      );
-    }
-
-    return $separate_last ? array(NULL, $property_path) : array($property_path, NULL);
-  }
+  public function getInnerProperty(DataDefinitionInterface $property);
 
   /**
    * Determines whether a field ID is reserved for special use.
@@ -421,92 +139,7 @@ public static function splitPropertyPath($property_path, $separate_last = TRUE,
    * @return bool
    *   TRUE if the field ID is reserved, FALSE if it can be used normally.
    */
-  public static function isFieldIdReserved($field_id) {
-    return substr($field_id, 0, 11) == 'search_api_';
-  }
-
-  /**
-   * Processes all pending index tasks inside a batch run.
-   *
-   * @param array $context
-   *   The current batch context.
-   * @param \Drupal\Core\Config\ConfigImporter $config_importer
-   *   The config importer.
-   *
-   * @throws \Drupal\search_api\SearchApiException
-   *   Thrown if any error occurred while tracking items.
-   */
-  public static function processIndexTasks(array &$context, ConfigImporter $config_importer) {
-    $index_task_manager = \Drupal::getContainer()->get('search_api.index_task_manager');
-
-    if (!isset($context['sandbox']['indexes'])) {
-      $context['sandbox']['indexes'] = array();
-
-      $indexes = \Drupal::entityTypeManager()
-        ->getStorage('search_api_index')
-        ->loadByProperties(array(
-          'status' => TRUE,
-        ));
-      $deleted = $config_importer->getUnprocessedConfiguration('delete');
-
-      /** @var \Drupal\search_api\IndexInterface $index */
-      foreach ($indexes as $index_id => $index) {
-        if (!$index_task_manager->isTrackingComplete($index) && !in_array($index->getConfigDependencyName(), $deleted)) {
-          $context['sandbox']['indexes'][] = $index_id;
-        }
-      }
-      $context['sandbox']['total'] = count($context['sandbox']['indexes']);
-      if (!$context['sandbox']['total']) {
-        $context['finished'] = 1;
-        return;
-      }
-    }
-
-    try {
-      $index_id = array_shift($context['sandbox']['indexes']);
-      $index = Index::load($index_id);
-      if (!($index_task_manager->addItemsOnce($index))) {
-        array_unshift($context['sandbox']['indexes'], $index_id);
-      }
-    }
-    catch (SearchApiException $e) {
-      watchdog_exception('search_api', $e);
-    }
-
-    if (empty($context['sandbox']['indexes'])) {
-      $context['finished'] = 1;
-    }
-    else {
-      $finished = $context['sandbox']['total'] - count($context['sandbox']['indexes']);
-      $context['finished'] = $finished / $context['sandbox']['total'];
-      $args = array(
-        '%index' => $index->label(),
-        '@num' => $finished + 1,
-        '@total' => $context['sandbox']['total'],
-      );
-      $context['message'] = \Drupal::translation()->translate('Tracking items for search index %index (@num of @total)', $args);
-    }
-  }
-
-  /**
-   * Creates a new search query object.
-   *
-   * @param \Drupal\search_api\IndexInterface $index
-   *   The index on which to search.
-   * @param array $options
-   *   (optional) The options to set for the query. See
-   *   \Drupal\search_api\Query\QueryInterface::setOption() for a list of
-   *   options that are recognized by default.
-   *
-   * @return \Drupal\search_api\Query\QueryInterface
-   *   A search query object to use.
-   *
-   * @see \Drupal\search_api\Query\QueryInterface::create()
-   */
-  public static function createQuery(IndexInterface $index, array $options = array()) {
-    $search_results_cache = \Drupal::service('search_api.results_static_cache');
-    return Query::create($index, $search_results_cache, $options);
-  }
+  public function isFieldIdReserved($field_id);
 
   /**
    * Creates a search item object.
@@ -522,9 +155,7 @@ public static function createQuery(IndexInterface $index, array $options = array
    * @return \Drupal\search_api\Item\ItemInterface
    *   A search item with the given values.
    */
-  public static function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
-    return new Item($index, $id, $datasource);
-  }
+  public function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL);
 
   /**
    * Creates a search item object by wrapping an existing complex data object.
@@ -547,17 +178,7 @@ public static function createItem(IndexInterface $index, $id, DatasourceInterfac
    * @throws \InvalidArgumentException
    *   Thrown if both $datasource and $id are NULL.
    */
-  public static function createItemFromObject(IndexInterface $index, ComplexDataInterface $original_object, $id = NULL, DatasourceInterface $datasource = NULL) {
-    if (!isset($id)) {
-      if (!isset($datasource)) {
-        throw new \InvalidArgumentException('Need either an item ID or the datasource to create a search item from an object.');
-      }
-      $id = self::createCombinedId($datasource->getPluginId(), $datasource->getItemId($original_object));
-    }
-    $item = static::createItem($index, $id, $datasource);
-    $item->setOriginalObject($original_object);
-    return $item;
-  }
+  public function createItemFromObject(IndexInterface $index, ComplexDataInterface $original_object, $id = NULL, DatasourceInterface $datasource = NULL);
 
   /**
    * Creates a new field object wrapping a field of the given index.
@@ -572,18 +193,7 @@ public static function createItemFromObject(IndexInterface $index, ComplexDataIn
    * @return \Drupal\search_api\Item\FieldInterface
    *   A new field object.
    */
-  public static function createField(IndexInterface $index, $field_identifier, $field_info = array()) {
-    $field = new Field($index, $field_identifier);
-
-    foreach ($field_info as $key => $value) {
-      $method = 'set' . Container::camelize($key);
-      if (method_exists($field, $method)) {
-        $field->$method($value);
-      }
-    }
-
-    return $field;
-  }
+  public function createField(IndexInterface $index, $field_identifier, $field_info = array());
 
   /**
    * Creates a new field on an index based on a property.
@@ -613,34 +223,7 @@ public static function createField(IndexInterface $index, $field_identifier, $fi
    * @throws \Drupal\search_api\SearchApiException
    *   Thrown if no type was given and no default could be determined.
    */
-  public static function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasource_id, $property_path, $field_id = NULL, $type = NULL) {
-    if (!isset($field_id)) {
-      $field_id = static::getNewFieldId($index, $property_path);
-    }
-
-    if (!isset($type)) {
-      $type_mapping = static::getFieldTypeMapping();
-      $property_type = $property->getDataType();
-      if (isset($type_mapping[$property_type])) {
-        $type = $type_mapping[$property_type];
-      }
-      else {
-        $property_name = $property->getLabel();
-        throw new SearchApiException("No default data type mapping could be found for property '$property_name' ($property_path) of type '$property_type'.");
-      }
-    }
-
-    $field_info = array(
-      'label' => $property->getLabel(),
-      'datasource_id' => $datasource_id,
-      'property_path' => $property_path,
-      'type' => $type,
-    );
-    if ($property instanceof ConfigurablePropertyInterface) {
-      $field_info['configuration'] = $property->defaultConfiguration();
-    }
-    return self::createField($index, $field_id, $field_info);
-  }
+  public function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasource_id, $property_path, $field_id = NULL, $type = NULL);
 
   /**
    * Finds a new unique field identifier on the given index.
@@ -654,122 +237,6 @@ public static function createFieldFromProperty(IndexInterface $index, DataDefini
    * @return string
    *   A new unique field identifier on the given index.
    */
-  public static function getNewFieldId(IndexInterface $index, $property_path) {
-    list(, $suggested_id) = static::splitPropertyPath($property_path);
-
-    // Avoid clashes with reserved IDs by removing the reserved "search_api_"
-    // from our suggested ID.
-    $suggested_id = str_replace('search_api_', '', $suggested_id);
-
-    $field_id = $suggested_id;
-    $i = 0;
-    while ($index->getField($field_id)) {
-      $field_id = $suggested_id . '_' . ++$i;
-    }
-
-    while (static::isFieldIdReserved($field_id)) {
-      $field_id = '_' . $field_id;
-    }
-
-    return $field_id;
-  }
-
-  /**
-   * Creates a single text token.
-   *
-   * @param string $value
-   *   The word or other token value.
-   * @param float $score
-   *   (optional) The token's score.
-   *
-   * @return \Drupal\search_api\Plugin\search_api\data_type\value\TextTokenInterface
-   *   A text token object.
-   */
-  public static function createTextToken($value, $score = 1.0) {
-    return new TextToken($value, (float) $score);
-  }
-
-  /**
-   * Returns a deep copy of the input array.
-   *
-   * The behavior of PHP regarding arrays with references pointing to it is
-   * rather weird. Therefore, this method should be used when making a copy of
-   * such an array, or of an array containing references.
-   *
-   * This method will also omit empty array elements (i.e., elements that
-   * evaluate to FALSE according to PHP's native rules).
-   *
-   * @param array $array
-   *   The array to copy.
-   *
-   * @return array
-   *   A deep copy of the array.
-   */
-  public static function deepCopy(array $array) {
-    $copy = array();
-    foreach ($array as $k => $v) {
-      if (is_array($v)) {
-        if ($v = static::deepCopy($v)) {
-          $copy[$k] = $v;
-        }
-      }
-      elseif (is_object($v)) {
-        $copy[$k] = clone $v;
-      }
-      elseif ($v) {
-        $copy[$k] = $v;
-      }
-    }
-    return $copy;
-  }
-
-  /**
-   * Creates a combined ID from a raw ID and an optional datasource prefix.
-   *
-   * This can be used to created an internal item ID from a datasource ID and a
-   * datasource-specific raw item ID, or a combined property path from a
-   * datasource ID and a property path to identify properties index-wide.
-   *
-   * @param string|null $datasource_id
-   *   The ID of the datasource to which the item belongs. Or NULL to return the
-   *   raw ID unchanged (option included for compatibility purposes).
-   * @param string $raw_id
-   *   The datasource-specific raw item ID of the item (or property).
-   *
-   * @return string
-   *   The combined ID, with the datasource prefix separated by
-   *   \Drupal\search_api\IndexInterface::DATASOURCE_ID_SEPARATOR.
-   */
-  public static function createCombinedId($datasource_id, $raw_id) {
-    if (!isset($datasource_id)) {
-      return $raw_id;
-    }
-    return $datasource_id . IndexInterface::DATASOURCE_ID_SEPARATOR . $raw_id;
-  }
-
-  /**
-   * Splits an internal ID into its two parts.
-   *
-   * Both internal item IDs and combined property paths are prefixed with the
-   * corresponding datasource ID. This method will split these IDs up again into
-   * their two parts.
-   *
-   * @param string $combined_id
-   *   The internal ID, with an optional datasource prefix separated with
-   *   \Drupal\search_api\IndexInterface::DATASOURCE_ID_SEPARATOR from the
-   *   raw item ID or property path.
-   *
-   * @return array
-   *   A numeric array, containing the datasource ID in element 0 and the raw
-   *   item ID or property path in element 1. In the case of
-   *   datasource-independent properties (i.e., when there is no prefix),
-   *   element 0 will be NULL.
-   */
-  public static function splitCombinedId($combined_id) {
-    if (strpos($combined_id, IndexInterface::DATASOURCE_ID_SEPARATOR) !== FALSE) {
-      return explode(IndexInterface::DATASOURCE_ID_SEPARATOR, $combined_id, 2);
-    }
-    return array(NULL, $combined_id);
-  }
+  public function getNewFieldId(IndexInterface $index, $property_path);
 
 }
diff --git a/src/Query/ResultsCache.php b/src/Query/ResultsCache.php
index 6ef369d..be92bf5 100644
--- a/src/Query/ResultsCache.php
+++ b/src/Query/ResultsCache.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\search_api\Query;
 
+use Drupal\search_api\IndexInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
@@ -45,6 +46,13 @@ public function __construct(RequestStack $request_stack) {
   /**
    * {@inheritdoc}
    */
+  public function createQuery(IndexInterface $index, array $options = array()) {
+    return Query::create($index, $this, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function addResults(ResultSetInterface $results) {
     // @todo Create getter and setter methods for the search ID.
     $search_id = $results->getQuery()->getOption('search id', '');
@@ -101,7 +109,7 @@ public function removeResults($search_id) {
    *   NULL key.
    */
   protected function getCurrentRequest() {
-    return $this->requestStack->getCurrentRequest();
+    return $this->requestStack->getCurrentRequest() ?: $this->null;
   }
 
 }
diff --git a/src/Query/ResultsCacheInterface.php b/src/Query/ResultsCacheInterface.php
index 76c72e1..6782235 100644
--- a/src/Query/ResultsCacheInterface.php
+++ b/src/Query/ResultsCacheInterface.php
@@ -2,12 +2,29 @@
 
 namespace Drupal\search_api\Query;
 
+use Drupal\search_api\IndexInterface;
+
 /**
  * Represents a search results cache.
  */
 interface ResultsCacheInterface {
 
   /**
+   * Creates a new search query object.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index on which to search.
+   * @param array $options
+   *   (optional) The options to set for the query. See
+   *   \Drupal\search_api\Query\QueryInterface::setOption() for a list of
+   *   options that are recognized by default.
+   *
+   * @return \Drupal\search_api\Query\QueryInterface
+   *   A search query object to use.
+   */
+  public function createQuery(IndexInterface $index, array $options = array());
+
+  /**
    * Adds a result set to the cache.
    *
    * @param \Drupal\search_api\Query\ResultSetInterface $results
diff --git a/src/Task/IndexTaskManager.php b/src/Task/IndexTaskManager.php
index 3af2d43..1d488c6 100644
--- a/src/Task/IndexTaskManager.php
+++ b/src/Task/IndexTaskManager.php
@@ -3,8 +3,10 @@
 namespace Drupal\search_api\Task;
 
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Config\ConfigImporter;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\search_api\Entity\Index;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\SearchApiException;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -58,6 +60,71 @@ public static function getSubscribedEvents() {
   }
 
   /**
+   * Processes all pending index tasks inside a batch run.
+   *
+   * @param array $context
+   *   The current batch context.
+   * @param \Drupal\Core\Config\ConfigImporter $config_importer
+   *   The config importer.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if any error occurred while tracking items.
+   */
+  public static function processIndexTasks(array &$context, ConfigImporter $config_importer) {
+    $index_task_manager = \Drupal::getContainer()
+      ->get('search_api.index_task_manager');
+
+    if (!isset($context['sandbox']['indexes'])) {
+      $context['sandbox']['indexes'] = array();
+
+      $indexes = \Drupal::entityTypeManager()
+        ->getStorage('search_api_index')
+        ->loadByProperties(array(
+          'status' => TRUE,
+        ));
+      $deleted = $config_importer->getUnprocessedConfiguration('delete');
+
+      /** @var \Drupal\search_api\IndexInterface $index */
+      foreach ($indexes as $index_id => $index) {
+        if (!$index_task_manager->isTrackingComplete($index) && !in_array($index->getConfigDependencyName(), $deleted)) {
+          $context['sandbox']['indexes'][] = $index_id;
+        }
+      }
+      $context['sandbox']['total'] = count($context['sandbox']['indexes']);
+      if (!$context['sandbox']['total']) {
+        $context['finished'] = 1;
+        return;
+      }
+    }
+
+    $index_id = array_shift($context['sandbox']['indexes']);
+    $index = Index::load($index_id);
+    try {
+      if (!($index_task_manager->addItemsOnce($index))) {
+        array_unshift($context['sandbox']['indexes'], $index_id);
+      }
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api', $e);
+    }
+
+    if (empty($context['sandbox']['indexes'])) {
+      $context['finished'] = 1;
+    }
+    else {
+      $finished = $context['sandbox']['total'] - count($context['sandbox']['indexes']);
+      $context['finished'] = $finished / $context['sandbox']['total'];
+      $args = array(
+        '%index' => $index->label(),
+        '@num' => $finished + 1,
+        '@total' => $context['sandbox']['total'],
+      );
+      $context['message'] = \Drupal::translation()
+        ->translate('Tracking items for search index %index (@num of @total)', $args);
+    }
+  }
+
+  /**
    * Tracks items according to the given event.
    *
    * @param \Drupal\search_api\Task\TaskEvent $event
diff --git a/src/Utility.php b/src/Utility.php
index ecbf26d..2a780a4 100644
--- a/src/Utility.php
+++ b/src/Utility.php
@@ -2,54 +2,20 @@
 
 namespace Drupal\search_api;
 
-use Drupal\Core\Config\ConfigImporter;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
 use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\Core\TypedData\DataDefinitionInterface;
-use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
-use Drupal\Core\TypedData\DataReferenceInterface;
-use Drupal\Core\TypedData\ListDataDefinitionInterface;
-use Drupal\Core\TypedData\ListInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\search_api\Datasource\DatasourceInterface;
-use Drupal\search_api\Entity\Index;
-use Drupal\search_api\Item\Field;
 use Drupal\search_api\Item\FieldInterface;
-use Drupal\search_api\Item\Item;
 use Drupal\search_api\Plugin\search_api\data_type\value\TextToken;
-use Drupal\search_api\Processor\ConfigurablePropertyInterface;
-use Drupal\search_api\Query\Query;
-use Symfony\Component\DependencyInjection\Container;
 
 /**
  * Contains utility methods for the Search API.
- *
- * @todo Maybe move some of these methods to other classes (and/or split this
- *   class into several utility classes).
  */
 class Utility {
 
   /**
-   * Static cache for the field type mapping.
-   *
-   * @var array
-   *
-   * @see getFieldTypeMapping()
-   */
-  protected static $fieldTypeMapping = array();
-
-  /**
-   * Static cache for the fallback data type mapping per index.
-   *
-   * @var array
-   *
-   * @see getDataTypeFallbackMapping()
-   */
-  protected static $dataTypeFallbackMapping = array();
-
-  /**
    * Determines whether fields of the given type contain fulltext data.
    *
    * @param string $type
@@ -59,18 +25,13 @@ class Utility {
    *
    * @return bool
    *   TRUE if $type is one of the specified types, FALSE otherwise.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function isTextType($type, array $text_types = array('text')) {
-    if (in_array($type, $text_types)) {
-      return TRUE;
-    }
-    $data_type = \Drupal::getContainer()
-      ->get('plugin.manager.search_api.data_type')
-      ->createInstance($type);
-    if ($data_type && !$data_type->isDefault()) {
-      return in_array($data_type->getFallbackType(), $text_types);
-    }
-    return FALSE;
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->isTextType($type, $text_types);
   }
 
   /**
@@ -81,64 +42,14 @@ public static function isTextType($type, array $text_types = array('text')) {
    *   corresponding Search API data types. Empty values mean that fields of
    *   that type should be ignored by the Search API.
    *
+   * @deprecated Will be removed during Beta phase.
+   *
    * @see hook_search_api_field_type_mapping_alter()
    */
   public static function getFieldTypeMapping() {
-    // Check the static cache first.
-    if (empty(static::$fieldTypeMapping)) {
-      // It's easier to write and understand this array in the form of
-      // $search_api_field_type => array($data_types) and flip it below.
-      $default_mapping = array(
-        'text' => array(
-          'field_item:string_long.string',
-          'field_item:text_long.string',
-          'field_item:text_with_summary.string',
-          'text',
-        ),
-        'string' => array(
-          'string',
-          'email',
-          'uri',
-          'filter_format',
-          'duration_iso8601',
-          'field_item:path',
-        ),
-        'integer' => array(
-          'integer',
-          'timespan',
-        ),
-        'decimal' => array(
-          'decimal',
-          'float',
-        ),
-        'date' => array(
-          'date',
-          'datetime_iso8601',
-          'timestamp',
-        ),
-        'boolean' => array(
-          'boolean',
-        ),
-        // Types we know about but want/have to ignore.
-        NULL => array(
-          'language',
-        ),
-      );
-
-      foreach ($default_mapping as $search_api_type => $data_types) {
-        foreach ($data_types as $data_type) {
-          $mapping[$data_type] = $search_api_type;
-        }
-      }
-
-      // Allow other modules to intercept and define what default type they want
-      // to use for their data type.
-      \Drupal::moduleHandler()->alter('search_api_field_type_mapping', $mapping);
-
-      static::$fieldTypeMapping = $mapping;
-    }
-
-    return static::$fieldTypeMapping;
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->getFieldTypeMapping();
   }
 
   /**
@@ -150,34 +61,13 @@ public static function getFieldTypeMapping() {
    * @return string[]
    *   An array containing the IDs of all custom data types that are not
    *   supported by the index's current server, mapped to their fallback types.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function getDataTypeFallbackMapping(IndexInterface $index) {
-    // Check the static cache first.
-    $index_id = $index->id();
-    if (empty(static::$dataTypeFallbackMapping[$index_id])) {
-      $server = NULL;
-      try {
-        $server = $index->getServerInstance();
-      }
-      catch (SearchApiException $e) {
-        // If the server isn't available, just ignore it here and return all
-        // custom types.
-      }
-      static::$dataTypeFallbackMapping[$index_id] = array();
-      $data_types = \Drupal::getContainer()
-        ->get('plugin.manager.search_api.data_type')
-        ->getInstances();
-      foreach ($data_types as $type_id => $data_type) {
-        // We know for sure that we do not need to fall back for the default
-        // data types as they are always present and are required to be
-        // supported by all backends.
-        if (!$data_type->isDefault() && (!$server || !$server->supportsDataType($type_id))) {
-          static::$dataTypeFallbackMapping[$index_id][$type_id] = $data_type->getFallbackType();
-        }
-      }
-    }
-
-    return static::$dataTypeFallbackMapping[$index_id];
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->getDataTypeFallbackMapping($index);
   }
 
   /**
@@ -191,60 +81,13 @@ public static function getDataTypeFallbackMapping(IndexInterface $index) {
    * @param \Drupal\search_api\Item\FieldInterface[][] $fields
    *   An associative array, keyed by property paths, mapped to field objects
    *   with that property path.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function extractFields(ComplexDataInterface $item, array $fields) {
-    // Figure out which fields are directly on the item and which need to be
-    // extracted from nested items.
-    $direct_fields = array();
-    $nested_fields = array();
-    foreach (array_keys($fields) as $key) {
-      if (strpos($key, ':') !== FALSE) {
-        list($direct, $nested) = explode(':', $key, 2);
-        $nested_fields[$direct][$nested] = $fields[$key];
-      }
-      else {
-        $direct_fields[] = $key;
-      }
-    }
-    // Extract the direct fields.
-    foreach ($direct_fields as $key) {
-      try {
-        $data = $item->get($key);
-        foreach ($fields[$key] as $field) {
-          self::extractField($data, $field);
-        }
-      }
-      catch (\InvalidArgumentException $e) {
-        // This can happen with properties added by processors.
-        // @todo Find a cleaner solution for this.
-      }
-    }
-    // Recurse for all nested fields.
-    foreach ($nested_fields as $direct => $fields_nested) {
-      try {
-        $item_nested = $item->get($direct);
-        if ($item_nested instanceof DataReferenceInterface) {
-          $item_nested = $item_nested->getTarget();
-        }
-        if ($item_nested instanceof EntityInterface) {
-          $item_nested = $item_nested->getTypedData();
-        }
-        if ($item_nested instanceof ComplexDataInterface && !$item_nested->isEmpty()) {
-          self::extractFields($item_nested, $fields_nested);
-        }
-        elseif ($item_nested instanceof ListInterface && !$item_nested->isEmpty()) {
-          foreach ($item_nested as $list_item) {
-            if ($list_item instanceof ComplexDataInterface && !$list_item->isEmpty()) {
-              self::extractFields($list_item, $fields_nested);
-            }
-          }
-        }
-      }
-      catch (\InvalidArgumentException $e) {
-        // This can happen with properties added by processors.
-        // @todo Find a cleaner solution for this.
-      }
-    }
+    \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->extractFields($item, $fields);
   }
 
   /**
@@ -254,14 +97,13 @@ public static function extractFields(ComplexDataInterface $item, array $fields)
    *   The piece of data from which to extract information.
    * @param \Drupal\search_api\Item\FieldInterface $field
    *   The field into which to put the extracted data.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function extractField(TypedDataInterface $data, FieldInterface $field) {
-    $values = static::extractFieldValues($data);
-
-    foreach ($values as $i => $value) {
-      $field->addValue($value);
-    }
-    $field->setOriginalType($data->getDataDefinition()->getDataType());
+    \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->extractField($data, $field);
   }
 
   /**
@@ -272,28 +114,13 @@ public static function extractField(TypedDataInterface $data, FieldInterface $fi
    *
    * @return array
    *   An array of values.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function extractFieldValues(TypedDataInterface $data) {
-    if ($data->getDataDefinition()->isList()) {
-      $values = array();
-      foreach ($data as $piece) {
-        $values[] = self::extractFieldValues($piece);
-      }
-      return $values ? call_user_func_array('array_merge', $values) : array();
-    }
-
-    $value = $data->getValue();
-    $definition = $data->getDataDefinition();
-    if ($definition instanceof ComplexDataDefinitionInterface) {
-      $property = $definition->getMainPropertyName();
-      if (isset($value[$property])) {
-        return array($value[$property]);
-      }
-    }
-    elseif (is_array($value)) {
-      return array_values($value);
-    }
-    return array($value);
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->extractFieldValues($data);
   }
 
   /**
@@ -307,20 +134,13 @@ public static function extractFieldValues(TypedDataInterface $data) {
    *
    * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
    *   The nested properties, keyed by property name.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function getNestedProperties(ComplexDataDefinitionInterface $property) {
-    $nested_properties = $property->getPropertyDefinitions();
-    if ($property instanceof EntityDataDefinitionInterface) {
-      $container = \Drupal::getContainer();
-      $bundles = $container->get('entity_type.bundle.info')
-        ->getBundleInfo($property->getEntityTypeId());
-      $field_manager = $container->get('entity_field.manager');
-      foreach ($bundles as $bundle => $bundle_label) {
-        $bundle_properties = $field_manager->getFieldDefinitions($property->getEntityTypeId(), $bundle);
-        $nested_properties += $bundle_properties;
-      }
-    }
-    return $nested_properties;
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->getNestedProperties($property);
   }
 
   /**
@@ -333,23 +153,13 @@ public static function getNestedProperties(ComplexDataDefinitionInterface $prope
    *
    * @return \Drupal\Core\TypedData\DataDefinitionInterface|null
    *   The requested property, or NULL if it couldn't be found.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function retrieveNestedProperty(array $properties, $property_path) {
-    list($key, $nested_path) = static::splitPropertyPath($property_path, FALSE);
-    if (!isset($properties[$key])) {
-      return NULL;
-    }
-
-    $property = static::getInnerProperty($properties[$key]);
-    if (!isset($nested_path)) {
-      return $property;
-    }
-
-    if (!$property instanceof ComplexDataDefinitionInterface) {
-      return NULL;
-    }
-
-    return static::retrieveNestedProperty(static::getNestedProperties($property), $nested_path);
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->retrieveNestedProperty($properties, $property_path);
   }
 
   /**
@@ -363,50 +173,13 @@ public static function retrieveNestedProperty(array $properties, $property_path)
    *
    * @return \Drupal\Core\TypedData\DataDefinitionInterface
    *   The inner property definition.
-   */
-  public static function getInnerProperty(DataDefinitionInterface $property) {
-    while ($property instanceof ListDataDefinitionInterface) {
-      $property = $property->getItemDefinition();
-    }
-    while ($property instanceof DataReferenceDefinitionInterface) {
-      $property = $property->getTargetDefinition();
-    }
-    return $property;
-  }
-
-  /**
-   * Splits a property path into two parts along a path separator (:).
-   *
-   * The path is split into one part with a single property name, and one part
-   * with the complete rest of the property path (which might be empty).
-   * Depending on $separate_last the returned single property key will be the
-   * first (FALSE) or last (TRUE) property of the path.
    *
-   * @param string $property_path
-   *   The property path to split.
-   * @param bool $separate_last
-   *   (optional) If FALSE, separate the first property of the path. By default,
-   *   the last property is separated from the rest.
-   * @param string $separator
-   *   (optional) The separator to use.
-   *
-   * @return string[]
-   *   An array with indexes 0 and 1, 0 containing the first part of the
-   *   property path and 1 the second. If $separate_last is FALSE, index 0 will
-   *   always contain a single property name (without any colons) and index 1
-   *   might be NULL. If $separate_last is TRUE it's the exact other way round.
+   * @deprecated Will be removed during Beta phase.
    */
-  public static function splitPropertyPath($property_path, $separate_last = TRUE, $separator = ':') {
-    $function = $separate_last ? 'strrpos' : 'strpos';
-    $pos = $function($property_path, $separator);
-    if ($pos !== FALSE) {
-      return array(
-        substr($property_path, 0, $pos),
-        substr($property_path, $pos + 1),
-      );
-    }
-
-    return $separate_last ? array(NULL, $property_path) : array($property_path, NULL);
+  public static function getInnerProperty(DataDefinitionInterface $property) {
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->getInnerProperty($property);
   }
 
   /**
@@ -420,72 +193,13 @@ public static function splitPropertyPath($property_path, $separate_last = TRUE,
    *
    * @return bool
    *   TRUE if the field ID is reserved, FALSE if it can be used normally.
-   */
-  public static function isFieldIdReserved($field_id) {
-    return substr($field_id, 0, 11) == 'search_api_';
-  }
-
-  /**
-   * Processes all pending index tasks inside a batch run.
-   *
-   * @param array $context
-   *   The current batch context.
-   * @param \Drupal\Core\Config\ConfigImporter $config_importer
-   *   The config importer.
    *
-   * @throws \Drupal\search_api\SearchApiException
-   *   Thrown if any error occurred while tracking items.
+   * @deprecated Will be removed during Beta phase.
    */
-  public static function processIndexTasks(array &$context, ConfigImporter $config_importer) {
-    $index_task_manager = \Drupal::getContainer()->get('search_api.index_task_manager');
-
-    if (!isset($context['sandbox']['indexes'])) {
-      $context['sandbox']['indexes'] = array();
-
-      $indexes = \Drupal::entityTypeManager()
-        ->getStorage('search_api_index')
-        ->loadByProperties(array(
-          'status' => TRUE,
-        ));
-      $deleted = $config_importer->getUnprocessedConfiguration('delete');
-
-      /** @var \Drupal\search_api\IndexInterface $index */
-      foreach ($indexes as $index_id => $index) {
-        if (!$index_task_manager->isTrackingComplete($index) && !in_array($index->getConfigDependencyName(), $deleted)) {
-          $context['sandbox']['indexes'][] = $index_id;
-        }
-      }
-      $context['sandbox']['total'] = count($context['sandbox']['indexes']);
-      if (!$context['sandbox']['total']) {
-        $context['finished'] = 1;
-        return;
-      }
-    }
-
-    try {
-      $index_id = array_shift($context['sandbox']['indexes']);
-      $index = Index::load($index_id);
-      if (!($index_task_manager->addItemsOnce($index))) {
-        array_unshift($context['sandbox']['indexes'], $index_id);
-      }
-    }
-    catch (SearchApiException $e) {
-      watchdog_exception('search_api', $e);
-    }
-
-    if (empty($context['sandbox']['indexes'])) {
-      $context['finished'] = 1;
-    }
-    else {
-      $finished = $context['sandbox']['total'] - count($context['sandbox']['indexes']);
-      $context['finished'] = $finished / $context['sandbox']['total'];
-      $args = array(
-        '%index' => $index->label(),
-        '@num' => $finished + 1,
-        '@total' => $context['sandbox']['total'],
-      );
-      $context['message'] = \Drupal::translation()->translate('Tracking items for search index %index (@num of @total)', $args);
-    }
+  public static function isFieldIdReserved($field_id) {
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->isFieldIdReserved($field_id);
   }
 
   /**
@@ -501,11 +215,14 @@ public static function processIndexTasks(array &$context, ConfigImporter $config
    * @return \Drupal\search_api\Query\QueryInterface
    *   A search query object to use.
    *
+   * @deprecated Will be removed during Beta phase.
+   *
    * @see \Drupal\search_api\Query\QueryInterface::create()
    */
   public static function createQuery(IndexInterface $index, array $options = array()) {
-    $search_results_cache = \Drupal::service('search_api.results_static_cache');
-    return Query::create($index, $search_results_cache, $options);
+    return \Drupal::getContainer()
+      ->get('search_api.results_static_cache')
+      ->createQuery($index, $options);
   }
 
   /**
@@ -521,9 +238,13 @@ public static function createQuery(IndexInterface $index, array $options = array
    *
    * @return \Drupal\search_api\Item\ItemInterface
    *   A search item with the given values.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
-    return new Item($index, $id, $datasource);
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->createItem($index, $id, $datasource);
   }
 
   /**
@@ -546,17 +267,13 @@ public static function createItem(IndexInterface $index, $id, DatasourceInterfac
    *
    * @throws \InvalidArgumentException
    *   Thrown if both $datasource and $id are NULL.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function createItemFromObject(IndexInterface $index, ComplexDataInterface $original_object, $id = NULL, DatasourceInterface $datasource = NULL) {
-    if (!isset($id)) {
-      if (!isset($datasource)) {
-        throw new \InvalidArgumentException('Need either an item ID or the datasource to create a search item from an object.');
-      }
-      $id = self::createCombinedId($datasource->getPluginId(), $datasource->getItemId($original_object));
-    }
-    $item = static::createItem($index, $id, $datasource);
-    $item->setOriginalObject($original_object);
-    return $item;
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->createItemFromObject($index, $original_object, $id, $datasource);
   }
 
   /**
@@ -571,18 +288,13 @@ public static function createItemFromObject(IndexInterface $index, ComplexDataIn
    *
    * @return \Drupal\search_api\Item\FieldInterface
    *   A new field object.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function createField(IndexInterface $index, $field_identifier, $field_info = array()) {
-    $field = new Field($index, $field_identifier);
-
-    foreach ($field_info as $key => $value) {
-      $method = 'set' . Container::camelize($key);
-      if (method_exists($field, $method)) {
-        $field->$method($value);
-      }
-    }
-
-    return $field;
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->createField($index, $field_identifier, $field_info);
   }
 
   /**
@@ -612,34 +324,13 @@ public static function createField(IndexInterface $index, $field_identifier, $fi
    *
    * @throws \Drupal\search_api\SearchApiException
    *   Thrown if no type was given and no default could be determined.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasource_id, $property_path, $field_id = NULL, $type = NULL) {
-    if (!isset($field_id)) {
-      $field_id = static::getNewFieldId($index, $property_path);
-    }
-
-    if (!isset($type)) {
-      $type_mapping = static::getFieldTypeMapping();
-      $property_type = $property->getDataType();
-      if (isset($type_mapping[$property_type])) {
-        $type = $type_mapping[$property_type];
-      }
-      else {
-        $property_name = $property->getLabel();
-        throw new SearchApiException("No default data type mapping could be found for property '$property_name' ($property_path) of type '$property_type'.");
-      }
-    }
-
-    $field_info = array(
-      'label' => $property->getLabel(),
-      'datasource_id' => $datasource_id,
-      'property_path' => $property_path,
-      'type' => $type,
-    );
-    if ($property instanceof ConfigurablePropertyInterface) {
-      $field_info['configuration'] = $property->defaultConfiguration();
-    }
-    return self::createField($index, $field_id, $field_info);
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->createFieldFromProperty($index, $property, $datasource_id, $property_path, $field_id, $type);
   }
 
   /**
@@ -653,25 +344,13 @@ public static function createFieldFromProperty(IndexInterface $index, DataDefini
    *
    * @return string
    *   A new unique field identifier on the given index.
+   *
+   * @deprecated Will be removed during Beta phase.
    */
   public static function getNewFieldId(IndexInterface $index, $property_path) {
-    list(, $suggested_id) = static::splitPropertyPath($property_path);
-
-    // Avoid clashes with reserved IDs by removing the reserved "search_api_"
-    // from our suggested ID.
-    $suggested_id = str_replace('search_api_', '', $suggested_id);
-
-    $field_id = $suggested_id;
-    $i = 0;
-    while ($index->getField($field_id)) {
-      $field_id = $suggested_id . '_' . ++$i;
-    }
-
-    while (static::isFieldIdReserved($field_id)) {
-      $field_id = '_' . $field_id;
-    }
-
-    return $field_id;
+    return \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->getNewFieldId($index, $property_path);
   }
 
   /**
@@ -772,4 +451,42 @@ public static function splitCombinedId($combined_id) {
     return array(NULL, $combined_id);
   }
 
+  /**
+   * Splits a property path into two parts along a path separator (:).
+   *
+   * The path is split into one part with a single property name, and one part
+   * with the complete rest of the property path (which might be empty).
+   * Depending on $separate_last the returned single property key will be the
+   * first (FALSE) or last (TRUE) property of the path.
+   *
+   * @param string $property_path
+   *   The property path to split.
+   * @param bool $separate_last
+   *   (optional) If FALSE, separate the first property of the path. By default,
+   *   the last property is separated from the rest.
+   * @param string $separator
+   *   (optional) The separator to use.
+   *
+   * @return string[]
+   *   An array with indexes 0 and 1, 0 containing the first part of the
+   *   property path and 1 the second. If $separate_last is FALSE, index 0 will
+   *   always contain a single property name (without any colons) and index 1
+   *   might be NULL. If $separate_last is TRUE it's the exact other way round.
+   */
+  public static function splitPropertyPath($property_path, $separate_last = TRUE, $separator = ':') {
+    $function = $separate_last ? 'strrpos' : 'strpos';
+    $pos = $function($property_path, $separator);
+    if ($pos !== FALSE) {
+      return array(
+        substr($property_path, 0, $pos),
+        substr($property_path, $pos + 1),
+      );
+    }
+
+    if ($separate_last) {
+      return array(NULL, $property_path);
+    }
+    return array($property_path, NULL);
+  }
+
 }
