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/Item/FieldsHelper.php b/src/Item/FieldsHelper.php
new file mode 100644
index 0000000..e4966fb
--- /dev/null
+++ b/src/Item/FieldsHelper.php
@@ -0,0 +1,443 @@
+<?php
+
+namespace Drupal\search_api\Item;
+
+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;
+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\DataType\DataTypePluginManager;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Processor\ConfigurablePropertyInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Utility;
+use Symfony\Component\DependencyInjection\Container;
+
+/**
+ * Provides helper methods for dealing with Search API fields and properties.
+ */
+class FieldsHelper implements FieldsHelperInterface {
+
+  /**
+   * The entity field manager.
+   *
+   * @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 $fieldTypeMapping;
+
+  /**
+   * Cache for the fallback data type mapping per index.
+   *
+   * @var array
+   *
+   * @see getDataTypeFallbackMapping()
+   */
+  protected $dataTypeFallbackMapping = array();
+
+  /**
+   * 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 function isTextType($type, array $textTypes = array('text')) {
+    if (in_array($type, $textTypes)) {
+      return TRUE;
+    }
+    $dataType = $this->dataTypeManager->createInstance($type);
+    if ($dataType && !$dataType->isDefault()) {
+      return in_array($dataType->getFallbackType(), $textTypes);
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldTypeMapping() {
+    // Check the cache first.
+    if (!isset($this->fieldTypeMapping)) {
+      // It's easier to write and understand this array in the form of
+      // $searchApiFieldType => array($dataTypes) and flip it below.
+      $defaultMapping = 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 ($defaultMapping as $searchApiType => $dataTypes) {
+        foreach ($dataTypes as $dataType) {
+          $mapping[$dataType] = $searchApiType;
+        }
+      }
+
+      // Allow other modules to intercept and define what default type they want
+      // to use for their data type.
+      $this->moduleHandler->alter('search_api_field_type_mapping', $mapping);
+
+      $this->fieldTypeMapping = $mapping;
+    }
+
+    return $this->fieldTypeMapping;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDataTypeFallbackMapping(IndexInterface $index) {
+    // Check the cache first.
+    $indexId = $index->id();
+    if (empty($this->dataTypeFallbackMapping[$indexId])) {
+      $server = NULL;
+      try {
+        $server = $index->getServerInstance();
+      }
+      catch (SearchApiException $e) {
+        // If the server isn't available, just ignore it here and return all
+        // custom types.
+      }
+      $this->dataTypeFallbackMapping[$indexId] = array();
+      $dataTypes = $this->dataTypeManager->getInstances();
+      foreach ($dataTypes as $typeId => $dataType) {
+        // 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 (!$dataType->isDefault() && (!$server || !$server->supportsDataType($typeId))) {
+          $this->dataTypeFallbackMapping[$indexId][$typeId] = $dataType->getFallbackType();
+        }
+      }
+    }
+
+    return $this->dataTypeFallbackMapping[$indexId];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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.
+    $directFields = array();
+    $nestedFields = array();
+    foreach (array_keys($fields) as $key) {
+      if (strpos($key, ':') !== FALSE) {
+        list($direct, $nested) = explode(':', $key, 2);
+        $nestedFields[$direct][$nested] = $fields[$key];
+      }
+      else {
+        $directFields[] = $key;
+      }
+    }
+    // Extract the direct fields.
+    $properties = $item->getProperties(TRUE);
+    foreach ($directFields as $key) {
+      if (empty($properties[$key])) {
+        continue;
+      }
+      $data = $item->get($key);
+      foreach ($fields[$key] as $field) {
+        $this->extractField($data, $field);
+      }
+    }
+    // Recurse for all nested fields.
+    foreach ($nestedFields as $direct => $fieldsNested) {
+      if (empty($properties[$direct])) {
+        continue;
+      }
+      $itemNested = $item->get($direct);
+      if ($itemNested instanceof DataReferenceInterface) {
+        $itemNested = $itemNested->getTarget();
+      }
+      if ($itemNested instanceof EntityInterface) {
+        $itemNested = $itemNested->getTypedData();
+      }
+      if ($itemNested instanceof ComplexDataInterface && !$itemNested->isEmpty()) {
+        $this->extractFields($itemNested, $fieldsNested);
+      }
+      elseif ($itemNested instanceof ListInterface && !$itemNested->isEmpty()) {
+        foreach ($itemNested as $listItem) {
+          if ($listItem instanceof ComplexDataInterface && !$listItem->isEmpty()) {
+            $this->extractFields($listItem, $fieldsNested);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function extractField(TypedDataInterface $data, FieldInterface $field) {
+    $values = $this->extractFieldValues($data);
+
+    foreach ($values as $i => $value) {
+      $field->addValue($value);
+    }
+    $field->setOriginalType($data->getDataDefinition()->getDataType());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function extractFieldValues(TypedDataInterface $data) {
+    if ($data->getDataDefinition()->isList()) {
+      $values = array();
+      foreach ($data as $piece) {
+        $values[] = $this->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);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNestedProperties(ComplexDataDefinitionInterface $property) {
+    $nestedProperties = $property->getPropertyDefinitions();
+    if ($property instanceof EntityDataDefinitionInterface) {
+      $bundles = $this->entityBundleInfo
+        ->getBundleInfo($property->getEntityTypeId());
+      foreach ($bundles as $bundle => $bundleLabel) {
+        $bundleProperties = $this->entityFieldManager
+          ->getFieldDefinitions($property->getEntityTypeId(), $bundle);
+        $nestedProperties += $bundleProperties;
+      }
+    }
+    return $nestedProperties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function retrieveNestedProperty(array $properties, $propertyPath) {
+    list($key, $nestedPath) = Utility::splitPropertyPath($propertyPath, FALSE);
+    if (!isset($properties[$key])) {
+      return NULL;
+    }
+
+    $property = $this->getInnerProperty($properties[$key]);
+    if (!isset($nestedPath)) {
+      return $property;
+    }
+
+    if (!$property instanceof ComplexDataDefinitionInterface) {
+      return NULL;
+    }
+
+    return $this->retrieveNestedProperty($this->getNestedProperties($property), $nestedPath);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInnerProperty(DataDefinitionInterface $property) {
+    while ($property instanceof ListDataDefinitionInterface) {
+      $property = $property->getItemDefinition();
+    }
+    while ($property instanceof DataReferenceDefinitionInterface) {
+      $property = $property->getTargetDefinition();
+    }
+    return $property;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isFieldIdReserved($fieldId) {
+    return substr($fieldId, 0, 11) == 'search_api_';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
+    return new Item($index, $id, $datasource);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createItemFromObject(IndexInterface $index, ComplexDataInterface $originalObject, $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 = Utility::createCombinedId($datasource->getPluginId(), $datasource->getItemId($originalObject));
+    }
+    $item = $this->createItem($index, $id, $datasource);
+    $item->setOriginalObject($originalObject);
+    return $item;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createField(IndexInterface $index, $fieldIdentifier, $fieldInfo = array()) {
+    $field = new Field($index, $fieldIdentifier);
+
+    foreach ($fieldInfo as $key => $value) {
+      $method = 'set' . Container::camelize($key);
+      if (method_exists($field, $method)) {
+        $field->$method($value);
+      }
+    }
+
+    return $field;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasourceId, $propertyPath, $fieldId = NULL, $type = NULL) {
+    if (!isset($fieldId)) {
+      $fieldId = $this->getNewFieldId($index, $propertyPath);
+    }
+
+    if (!isset($type)) {
+      $typeMapping = $this->getFieldTypeMapping();
+      $propertyType = $property->getDataType();
+      if (isset($typeMapping[$propertyType])) {
+        $type = $typeMapping[$propertyType];
+      }
+      else {
+        $propertyName = $property->getLabel();
+        throw new SearchApiException("No default data type mapping could be found for property '$propertyName' ($propertyPath) of type '$propertyType'.");
+      }
+    }
+
+    $fieldInfo = array(
+      'label' => $property->getLabel(),
+      'datasource_id' => $datasourceId,
+      'property_path' => $propertyPath,
+      'type' => $type,
+    );
+    if ($property instanceof ConfigurablePropertyInterface) {
+      $fieldInfo['configuration'] = $property->defaultConfiguration();
+    }
+    return $this->createField($index, $fieldId, $fieldInfo);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNewFieldId(IndexInterface $index, $propertyPath) {
+    list(, $suggestedId) = Utility::splitPropertyPath($propertyPath);
+
+    // Avoid clashes with reserved IDs by removing the reserved "search_api_"
+    // from our suggested ID.
+    $suggestedId = str_replace('search_api_', '', $suggestedId);
+
+    $fieldId = $suggestedId;
+    $i = 0;
+    while ($index->getField($fieldId)) {
+      $fieldId = $suggestedId . '_' . ++$i;
+    }
+
+    while ($this->isFieldIdReserved($fieldId)) {
+      $fieldId = '_' . $fieldId;
+    }
+
+    return $fieldId;
+  }
+
+}
diff --git a/src/Item/FieldsHelperInterface.php b/src/Item/FieldsHelperInterface.php
new file mode 100644
index 0000000..316335d
--- /dev/null
+++ b/src/Item/FieldsHelperInterface.php
@@ -0,0 +1,242 @@
+<?php
+
+namespace Drupal\search_api\Item;
+
+use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\IndexInterface;
+
+/**
+ *
+ */
+interface FieldsHelperInterface {
+
+  /**
+   * Determines whether fields of the given type contain fulltext data.
+   *
+   * @param string $type
+   *   The type to check.
+   * @param string[] $textTypes
+   *   (optional) An array of types to be considered as text.
+   *
+   * @return bool
+   *   TRUE if $type is one of the specified types, FALSE otherwise.
+   */
+  public function isTextType($type, array $textTypes = 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()
+   */
+  public function getFieldTypeMapping();
+
+  /**
+   * 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.
+   */
+  public function getDataTypeFallbackMapping(IndexInterface $index);
+
+  /**
+   * 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.
+   */
+  public function extractFields(ComplexDataInterface $item, array $fields);
+
+  /**
+   * 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.
+   */
+  public function extractField(TypedDataInterface $data, FieldInterface $field);
+
+  /**
+   * Extracts field values from a typed data object.
+   *
+   * @param \Drupal\Core\TypedData\TypedDataInterface $data
+   *   The typed data object.
+   *
+   * @return array
+   *   An array of values.
+   */
+  public 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.
+   */
+  public function getNestedProperties(ComplexDataDefinitionInterface $property);
+
+  /**
+   * Retrieves a nested property from a list of properties.
+   *
+   * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
+   *   The base properties, keyed by property name.
+   * @param string $propertyPath
+   *   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.
+   */
+  public function retrieveNestedProperty(array $properties, $propertyPath);
+
+  /**
+   * 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.
+   */
+  public function getInnerProperty(DataDefinitionInterface $property);
+
+  /**
+   * 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 $fieldId
+   *   The field ID.
+   *
+   * @return bool
+   *   TRUE if the field ID is reserved, FALSE if it can be used normally.
+   */
+  public function isFieldIdReserved($fieldId);
+
+  /**
+   * 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 function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL);
+
+  /**
+   * 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 $originalObject
+   *   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.
+   */
+  public function createItemFromObject(IndexInterface $index, ComplexDataInterface $originalObject, $id = NULL, DatasourceInterface $datasource = NULL);
+
+  /**
+   * 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 $fieldIdentifier
+   *   The field identifier.
+   * @param array $fieldInfo
+   *   (optional) An array with further configuration for the field.
+   *
+   * @return \Drupal\search_api\Item\FieldInterface
+   *   A new field object.
+   */
+  public function createField(IndexInterface $index, $fieldIdentifier, $fieldInfo = array());
+
+  /**
+   * 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 $datasourceId
+   *   The ID of the index's datasource this property belongs to, or NULL if it
+   *   is a datasource-independent property.
+   * @param string $propertyPath
+   *   The property's property path within the property structure of the
+   *   datasource.
+   * @param string|null $fieldId
+   *   (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.
+   */
+  public function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasourceId, $propertyPath, $fieldId = NULL, $type = NULL);
+
+  /**
+   * Finds a new unique field identifier on the given index.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The search index.
+   * @param string $propertyPath
+   *   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.
+   */
+  public function getNewFieldId(IndexInterface $index, $propertyPath);
+
+}
diff --git a/src/Processor/FieldsProcessorPluginBase.php b/src/Processor/FieldsProcessorPluginBase.php
index 1f31c1e..cd7c261 100644
--- a/src/Processor/FieldsProcessorPluginBase.php
+++ b/src/Processor/FieldsProcessorPluginBase.php
@@ -7,12 +7,13 @@
 use Drupal\Core\Plugin\PluginFormInterface;
 use Drupal\Core\Render\Element;
 use Drupal\search_api\Item\FieldInterface;
+use Drupal\search_api\Item\FieldsHelperInterface;
 use Drupal\search_api\Plugin\PluginFormTrait;
 use Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface;
 use Drupal\search_api\Query\ConditionGroupInterface;
 use Drupal\search_api\Query\ConditionInterface;
 use Drupal\search_api\Query\QueryInterface;
-use Drupal\search_api\Utility;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a base class for processors that work on individual fields.
@@ -38,12 +39,54 @@
 
   use PluginFormTrait;
 
+  /**
+   * The field helper.
+   *
+   * @var \Drupal\search_api\Item\FieldsHelperInterface|null
+   */
+  protected $fieldHelper;
+
   // @todo Add defaultConfiguration() implementation and find a cleaner solution
   //   for all the isset($this->configuration['fields']) checks.
 
   /**
    * {@inheritdoc}
    */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    /** @var static $processor */
+    $processor = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+
+    $processor->setFieldHelper($container->get('search_api.fields_helper'));
+
+    return $processor;
+  }
+
+  /**
+   * Retrieves the field helper.
+   *
+   * @return \Drupal\search_api\Item\FieldsHelperInterface
+   *   The field helper.
+   */
+  public function getFieldHelper() {
+    return $this->fieldHelper ?: \Drupal::service('search_api.fields_helper');
+  }
+
+  /**
+   * Sets the field helper.
+   *
+   * @param \Drupal\search_api\Item\FieldsHelperInterface $field_helper
+   *   The new field helper.
+   *
+   * @return $this
+   */
+  public function setFieldHelper(FieldsHelperInterface $field_helper) {
+    $this->fieldHelper = $field_helper;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
     $fields = $this->index->getFields();
     $field_options = array();
@@ -287,7 +330,7 @@ protected function testField($name, FieldInterface $field) {
    *   TRUE if fields of that type should be processed, FALSE otherwise.
    */
   protected function testType($type) {
-    return Utility::isTextType($type, array('text', 'string'));
+    return $this->getFieldHelper()->isTextType($type, array('text', 'string'));
   }
 
   /**
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);
+  }
+
 }
diff --git a/tests/src/Unit/Plugin/Processor/AddURLTest.php b/tests/src/Unit/Plugin/Processor/AddURLTest.php
index f8a37d5..5db77ec 100644
--- a/tests/src/Unit/Plugin/Processor/AddURLTest.php
+++ b/tests/src/Unit/Plugin/Processor/AddURLTest.php
@@ -38,7 +38,7 @@ class AddURLTest extends UnitTestCase {
   protected function setUp() {
     parent::setUp();
 
-    $this->setUpDataTypePlugin();
+    $this->setUpMockContainer();
 
     // Create a mock for the URL to be returned.
     $url = $this->getMockBuilder('Drupal\Core\Url')
diff --git a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
index df9918d..74c1463 100644
--- a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
+++ b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
@@ -83,7 +83,7 @@ protected function setUp() {
     ), 'search_api_index');
     $this->processor = new AggregatedFields(array('index' => $this->index), 'aggregated_field', array());
     $this->index->addProcessor($this->processor);
-    $this->setUpDataTypePlugin();
+    $this->setUpMockContainer();
   }
 
   /**
@@ -259,6 +259,10 @@ public function testFieldExtraction() {
       ->willReturnMap(array(
         array('foo', $bar_foo_property),
       ));
+    $bar_property->method('getProperties')
+      ->willReturn(array(
+        'foo' => TRUE,
+      ));
     $foobar_property = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
     $foobar_property->method('getValue')
       ->willReturn('wrong_value2');
@@ -269,6 +273,11 @@ public function testFieldExtraction() {
         array('bar', $bar_property),
         array('foobar', $foobar_property),
       ));
+    $object->method('getProperties')
+      ->willReturn(array(
+        'bar' => TRUE,
+        'foobar' => TRUE,
+      ));
 
     /** @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject $index */
     $index = $this->getMock('Drupal\search_api\IndexInterface');
diff --git a/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php b/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php
index e1c3284..d27c99f 100644
--- a/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php
+++ b/tests/src/Unit/Plugin/Processor/FieldsProcessorPluginBaseTest.php
@@ -39,7 +39,7 @@ class FieldsProcessorPluginBaseTest extends UnitTestCase {
   public function setUp() {
     parent::setUp();
 
-    $this->setUpDataTypePlugin();
+    $this->setUpMockContainer();
     $this->index = $this->getMock('Drupal\search_api\IndexInterface');
     $this->index->expects($this->any())
       ->method('status')
diff --git a/tests/src/Unit/Plugin/Processor/HighlightTest.php b/tests/src/Unit/Plugin/Processor/HighlightTest.php
index 6c43393..6de97c3 100644
--- a/tests/src/Unit/Plugin/Processor/HighlightTest.php
+++ b/tests/src/Unit/Plugin/Processor/HighlightTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Tests\search_api\Unit\Plugin\Processor;
 
-use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\TypedData\DataDefinition;
 use Drupal\search_api\Item\Field;
 use Drupal\search_api\Item\ItemInterface;
@@ -45,6 +44,8 @@ class HighlightTest extends UnitTestCase {
   protected function setUp() {
     parent::setUp();
 
+    $this->setUpMockContainer();
+
     $this->processor = new Highlight(array(), 'highlight', array());
 
     $this->index = $this->getMock('Drupal\search_api\IndexInterface');
@@ -154,7 +155,7 @@ public function testPostprocessSearchResultsWithResults() {
       ->will($this->returnValue('foo'));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -200,7 +201,7 @@ public function testPostprocessSearchResultsWithChangedPrefixSuffix() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -243,7 +244,7 @@ public function testPostprocessSearchResultsWithoutHighlight() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -284,7 +285,7 @@ public function testPostprocessSearchResultsWithPreviousHighlighting() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -330,7 +331,7 @@ public function testPostprocessSearchResultsExcerpt() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'congue')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -373,7 +374,7 @@ public function testPostprocessSearchResultsExerptMatchNearEnd() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'diam')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -418,7 +419,7 @@ public function testPostprocessSearchResultsWithChangedExcerptLength() {
       ->will($this->returnValue('congue'));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -463,7 +464,7 @@ public function testPostprocessSearchResultsWithoutExcerpt() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'congue')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = $this->createField('body', 'entity:node/body');
+    $field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -520,7 +521,7 @@ public function testPostprocessSearchResultsWithComplexKeys() {
       ->will($this->returnValue($keys));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = $this->createField('body', 'entity:node/body');
+    $body_field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -565,8 +566,8 @@ public function testPostprocessSearchResultsWithTwoFields() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = $this->createField('body', 'entity:node/body');
-    $title_field = $this->createField('title', 'title');
+    $body_field = $this->createTestField('body', 'entity:node/body');
+    $title_field = $this->createTestField('title', 'title');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -623,7 +624,7 @@ public function testPostprocessSearchResultsWithTwoItems() {
       ->will($this->returnValue(array('#conjunction' => 'OR', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = $this->createField('body', 'entity:node/body');
+    $body_field = $this->createTestField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -674,8 +675,8 @@ public function testExcerptExcludeFields() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = $this->createField('body', 'entity:node/body');
-    $title_field = $this->createField('title', 'title');
+    $body_field = $this->createTestField('body', 'entity:node/body');
+    $title_field = $this->createTestField('title', 'title');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -736,6 +737,10 @@ public function testFieldExtraction() {
       ->willReturnMap(array(
         array('foo', $bar_foo_property),
       ));
+    $bar_property->method('getProperties')
+      ->willReturn(array(
+        'foo' => TRUE,
+      ));
     $foobar_property = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
     $foobar_property->method('getValue')
       ->willReturn('wrong_value2 foo');
@@ -746,14 +751,19 @@ public function testFieldExtraction() {
         array('bar', $bar_property),
         array('foobar', $foobar_property),
       ));
+    $object->method('getProperties')
+      ->willReturn(array(
+        'bar' => TRUE,
+        'foobar' => TRUE,
+      ));
 
     $this->index->method('getFields')
       ->willReturn(array(
-        'field1' => $this->createField('field1', 'entity:test1/bar:foo'),
-        'field2' => $this->createField('field2', 'entity:test2/foobar'),
-        'field3' => $this->createField('field3', 'foo'),
-        'field4' => $this->createField('field4', 'baz', FALSE),
-        'field5' => $this->createField('field5', 'entity:test1/foobar'),
+        'field1' => $this->createTestField('field1', 'entity:test1/bar:foo'),
+        'field2' => $this->createTestField('field2', 'entity:test2/foobar'),
+        'field3' => $this->createTestField('field3', 'foo'),
+        'field4' => $this->createTestField('field4', 'baz', FALSE),
+        'field5' => $this->createTestField('field5', 'entity:test1/foobar'),
       ));
     $this->index->method('getPropertyDefinitions')
       ->willReturnMap(array(
@@ -794,13 +804,6 @@ public function testFieldExtraction() {
       ));
     $this->processor->setIndex($this->index);
 
-    $container = new ContainerBuilder();
-    $data_type_manager = $this->getMockBuilder('Drupal\search_api\DataType\DataTypePluginManager')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $container->set('plugin.manager.search_api.data_type', $data_type_manager);
-    \Drupal::setContainer($container);
-
     /** @var \Drupal\search_api\Datasource\DatasourceInterface|\PHPUnit_Framework_MockObject_MockObject $datasource */
     $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
     $datasource->method('getPluginId')
@@ -808,10 +811,10 @@ public function testFieldExtraction() {
 
     $item = Utility::createItem($this->index, 'id', $datasource);
     $item->setOriginalObject($object);
-    $field = $this->createField('field4', 'baz')
+    $field = $this->createTestField('field4', 'baz')
       ->addValue('wrong_value1 foo');
     $item->setField('field4', $field);
-    $field = $this->createField('field5', 'entity:test1/foobar')
+    $field = $this->createTestField('field5', 'entity:test1/foobar')
       ->addValue('value1 foo')
       ->addValue('value2 foo');
     $item->setField('field5', $field);
@@ -860,7 +863,7 @@ public function testFieldExtraction() {
    * @return \Drupal\search_api\Item\FieldInterface
    *   A field object.
    */
-  protected function createField($id, $combined_property_path, $text = TRUE) {
+  protected function createTestField($id, $combined_property_path, $text = TRUE) {
     $field = new Field($this->index, $id);
     list ($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
     $field->setDatasourceId($datasource_id);
diff --git a/tests/src/Unit/Plugin/Processor/HtmlFilterTest.php b/tests/src/Unit/Plugin/Processor/HtmlFilterTest.php
index d32a629..6dbdbf3 100644
--- a/tests/src/Unit/Plugin/Processor/HtmlFilterTest.php
+++ b/tests/src/Unit/Plugin/Processor/HtmlFilterTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Tests\search_api\Unit\Plugin\Processor;
 
-use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\search_api\Plugin\search_api\processor\HtmlFilter;
 use Drupal\search_api\Utility;
 use Drupal\Tests\UnitTestCase;
@@ -17,12 +16,16 @@
 class HtmlFilterTest extends UnitTestCase {
 
   use ProcessorTestTrait;
+  use TestItemsTrait;
 
   /**
    * Creates a new processor object for use in the tests.
    */
   public function setUp() {
     parent::setUp();
+
+    $this->setUpMockContainer();
+
     $this->processor = new HtmlFilter(array(), 'html_filter', array());
   }
 
@@ -229,13 +232,6 @@ public function tagConfigurationDataProvider() {
    * @dataProvider stringProcessingDataProvider
    */
   public function testStringProcessing(array $config) {
-    $data_type_manager = $this->getMockBuilder('Drupal\search_api\DataType\DataTypePluginManager')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $container = new ContainerBuilder();
-    $container->set('plugin.manager.search_api.data_type', $data_type_manager);
-    \Drupal::setContainer($container);
-
     $this->processor->setConfiguration($config);
 
     $passed_value = '<h2>Foo Bar <em>Baz</em></h2>
diff --git a/tests/src/Unit/Plugin/Processor/NodeStatusTest.php b/tests/src/Unit/Plugin/Processor/NodeStatusTest.php
index ca30bfd..e95e87a 100644
--- a/tests/src/Unit/Plugin/Processor/NodeStatusTest.php
+++ b/tests/src/Unit/Plugin/Processor/NodeStatusTest.php
@@ -16,6 +16,8 @@
  */
 class NodeStatusTest extends UnitTestCase {
 
+  use TestItemsTrait;
+
   /**
    * The processor to be tested.
    *
@@ -36,6 +38,8 @@ class NodeStatusTest extends UnitTestCase {
   protected function setUp() {
     parent::setUp();
 
+    $this->setUpMockContainer();
+
     $this->processor = new NodeStatus(array(), 'node_status', array());
 
     $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
diff --git a/tests/src/Unit/Plugin/Processor/RoleFilterTest.php b/tests/src/Unit/Plugin/Processor/RoleFilterTest.php
index 47b69c5..9f48275 100644
--- a/tests/src/Unit/Plugin/Processor/RoleFilterTest.php
+++ b/tests/src/Unit/Plugin/Processor/RoleFilterTest.php
@@ -16,6 +16,8 @@
  */
 class RoleFilterTest extends UnitTestCase {
 
+  use TestItemsTrait;
+
   /**
    * The processor to be tested.
    *
@@ -36,6 +38,8 @@ class RoleFilterTest extends UnitTestCase {
   protected function setUp() {
     parent::setUp();
 
+    $this->setUpMockContainer();
+
     $this->processor = new RoleFilter(array(), 'role_filter', array());
 
     /** @var \Drupal\search_api\IndexInterface $index */
diff --git a/tests/src/Unit/Plugin/Processor/StopwordsTest.php b/tests/src/Unit/Plugin/Processor/StopwordsTest.php
index 9d93786..70425fa 100644
--- a/tests/src/Unit/Plugin/Processor/StopwordsTest.php
+++ b/tests/src/Unit/Plugin/Processor/StopwordsTest.php
@@ -22,7 +22,7 @@ class StopwordsTest extends UnitTestCase {
    */
   protected function setUp() {
     parent::setUp();
-    $this->setUpDataTypePlugin();
+    $this->setUpMockContainer();
     $this->processor = new Stopwords(array(), 'stopwords', array());;
   }
 
diff --git a/tests/src/Unit/Plugin/Processor/TestItemsTrait.php b/tests/src/Unit/Plugin/Processor/TestItemsTrait.php
index e93c9f1..8cefb6d 100644
--- a/tests/src/Unit/Plugin/Processor/TestItemsTrait.php
+++ b/tests/src/Unit/Plugin/Processor/TestItemsTrait.php
@@ -6,6 +6,9 @@
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\Item\FieldInterface;
+use Drupal\search_api\Item\FieldsHelper;
+use Drupal\search_api\Item\Item;
+use Drupal\search_api\Query\Query;
 use Drupal\search_api\Utility;
 
 /**
@@ -32,29 +35,29 @@
    *
    * @param \Drupal\search_api\IndexInterface $index
    *   The index that should be used for the item.
-   * @param string $field_type
+   * @param string $fieldType
    *   The field type to set for the field.
-   * @param mixed $field_value
+   * @param mixed $fieldValue
    *   A field value to add to the field.
    * @param \Drupal\search_api\Item\FieldInterface $field
    *   (optional) A variable, passed by reference, into which the created field
    *   will be saved.
-   * @param string $field_id
+   * @param string $fieldId
    *   (optional) The field ID to set for the field.
    *
    * @return \Drupal\search_api\Item\ItemInterface[]
    *   An array containing a single item with the specified field.
    */
-  public function createSingleFieldItem(IndexInterface $index, $field_type, $field_value, FieldInterface &$field = NULL, $field_id = 'field_test') {
-    $this->itemIds[0] = $item_id = Utility::createCombinedId('entity:node', '1:en');
-    $item = Utility::createItem($index, $item_id);
-    $field = Utility::createField($index, $field_id);
-    $field->setType($field_type);
-    $field->addValue($field_value);
-    $item->setField($field_id, $field);
+  public function createSingleFieldItem(IndexInterface $index, $fieldType, $fieldValue, FieldInterface &$field = NULL, $fieldId = 'field_test') {
+    $this->itemIds[0] = $itemId = Utility::createCombinedId('entity:node', '1:en');
+    $item = Utility::createItem($index, $itemId);
+    $field = Utility::createField($index, $fieldId);
+    $field->setType($fieldType);
+    $field->addValue($fieldValue);
+    $item->setField($fieldId, $field);
     $item->setFieldsExtracted(TRUE);
 
-    return array($item_id => $item);
+    return array($itemId => $item);
   }
 
   /**
@@ -82,7 +85,7 @@ public function createItems(IndexInterface $index, $count, array $fields, Comple
     for ($i = 0; $i < $count; ++$i) {
       $datasource_id = $datasource_ids[$i % $datasource_count];
       $this->itemIds[$i] = $item_id = Utility::createCombinedId($datasource_id, ($i + 1) . ':en');
-      $item = Utility::createItem($index, $item_id);
+      $item = new Item($index, $item_id);
       if (isset($object)) {
         $item->setOriginalObject($object);
       }
@@ -105,25 +108,39 @@ public function createItems(IndexInterface $index, $count, array $fields, Comple
   /**
    * Adds mock data type plugin manager and results cache services to \Drupal.
    */
-  protected function setUpDataTypePlugin() {
+  protected function setUpMockContainer() {
     /** @var \Drupal\Tests\UnitTestCase|\Drupal\Tests\search_api\Unit\Plugin\Processor\TestItemsTrait $this */
-    $data_type_plugin = $this->getMockBuilder('Drupal\search_api\DataType\DataTypePluginManager')
+    $dataTypeManager = $this->getMockBuilder('Drupal\search_api\DataType\DataTypePluginManager')
       ->disableOriginalConstructor()
       ->getMock();
-    $data_type_plugin->expects($this->any())
-      ->method('getInstances')
+    $dataTypeManager->method('getInstances')
       ->will($this->returnValue(array()));
 
-    $results_static_cache = $this->getMockBuilder('Drupal\search_api\Query\ResultsCache')
+    $entityFieldManager = $this->getMockBuilder('Drupal\Core\Entity\EntityFieldManagerInterface')
       ->disableOriginalConstructor()
       ->getMock();
-    $results_static_cache->expects($this->any())
-      ->method('getResults')
+    $entityBundleInfo = $this->getMockBuilder('Drupal\Core\Entity\EntityTypeBundleInfoInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $moduleHandler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandlerInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $fieldsHelper = new FieldsHelper($entityFieldManager, $entityBundleInfo, $moduleHandler, $dataTypeManager);
+
+    $resultsStaticCache = $this->getMockBuilder('Drupal\search_api\Query\ResultsCache')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $resultsStaticCache->method('createQuery')
+      ->willReturnCallback(function (IndexInterface $index, array $options = array()) use ($resultsStaticCache) {
+        return Query::create($index, $resultsStaticCache, $options);
+      });
+    $resultsStaticCache->method('getResults')
       ->will($this->returnValue(array()));
 
     $this->container = new ContainerBuilder();
-    $this->container->set('plugin.manager.search_api.data_type', $data_type_plugin);
-    $this->container->set('search_api.results_static_cache', $results_static_cache);
+    $this->container->set('plugin.manager.search_api.data_type', $dataTypeManager);
+    $this->container->set('search_api.fields_helper', $fieldsHelper);
+    $this->container->set('search_api.results_static_cache', $resultsStaticCache);
     \Drupal::setContainer($this->container);
   }
 
diff --git a/tests/src/Unit/Plugin/Processor/TransliterationTest.php b/tests/src/Unit/Plugin/Processor/TransliterationTest.php
index bb233dc..555988a 100644
--- a/tests/src/Unit/Plugin/Processor/TransliterationTest.php
+++ b/tests/src/Unit/Plugin/Processor/TransliterationTest.php
@@ -31,7 +31,7 @@ public function setUp() {
 
     $this->index = $this->getMock('Drupal\search_api\IndexInterface');
 
-    $this->setUpDataTypePlugin();
+    $this->setUpMockContainer();
     $this->processor = new Transliteration(array(), 'transliteration', array());
     $this->processor->setLangcode('en');
 
