diff --git a/src/Entity/Index.php b/src/Entity/Index.php
index 1b710d8..7970bac 100644
--- a/src/Entity/Index.php
+++ b/src/Entity/Index.php
@@ -778,13 +778,14 @@ public function getField($field_id) {
    * {@inheritdoc}
    */
   public function getFieldsByDatasource($datasource_id) {
-    $datasource_fields = array_fill_keys(array_keys($this->getDatasources()), array());
-    $datasource_fields[NULL] = array();
+    $datasource_fields = array();
     foreach ($this->getFields() as $field_id => $field) {
-      $datasource_fields[$field->getDatasourceId()][$field_id] = $field;
+      if ($field->getDatasourceId() === $datasource_id) {
+        $datasource_fields[$field_id] = $field;
+      }
     }
 
-    return $datasource_fields[$datasource_id];
+    return $datasource_fields;
   }
 
   /**
diff --git a/src/Plugin/search_api/processor/AddURL.php b/src/Plugin/search_api/processor/AddURL.php
index fe42a9a..bf23af7 100644
--- a/src/Plugin/search_api/processor/AddURL.php
+++ b/src/Plugin/search_api/processor/AddURL.php
@@ -48,7 +48,9 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
   public function addFieldValues(ItemInterface $item) {
     $url = $item->getDatasource()->getItemUrl($item->getOriginalObject());
     if ($url) {
-      foreach ($this->filterForPropertyPath($item->getFields(), 'search_api_url') as $field) {
+      $fields = $this->getFieldsHelper()
+        ->filterForPropertyPath($item->getFields(), NULL, 'search_api_url');
+      foreach ($fields as $field) {
         if (!$field->getDatasourceId()) {
           $field->addValue($url->toString());
         }
diff --git a/src/Plugin/search_api/processor/AggregatedFields.php b/src/Plugin/search_api/processor/AggregatedFields.php
index 4b2d4c4..13c4c4c 100644
--- a/src/Plugin/search_api/processor/AggregatedFields.php
+++ b/src/Plugin/search_api/processor/AggregatedFields.php
@@ -47,10 +47,9 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
    * {@inheritdoc}
    */
   public function addFieldValues(ItemInterface $item) {
-    $aggregated_fields = $this->filterForPropertyPath(
-      $this->index->getFieldsByDatasource(NULL),
-      'aggregated_field'
-    );
+    $fields = $this->index->getFields();
+    $aggregated_fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($fields, NULL, 'aggregated_field');
     $required_properties_by_datasource = array(
       NULL => array(),
       $item->getDatasourceId() => array(),
@@ -62,9 +61,11 @@ public function addFieldValues(ItemInterface $item) {
       }
     }
 
-    $property_values = $this->extractItemValues(array($item), $required_properties_by_datasource)[0];
+    $property_values = $this->getFieldsHelper()
+      ->extractItemValues(array($item), $required_properties_by_datasource)[0];
 
-    $aggregated_fields = $this->filterForPropertyPath($item->getFields(), 'aggregated_field');
+    $aggregated_fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($item->getFields(), NULL, 'aggregated_field');
     foreach ($aggregated_fields as $aggregated_field) {
       $values = array();
       $configuration = $aggregated_field->getConfiguration();
diff --git a/src/Plugin/search_api/processor/ContentAccess.php b/src/Plugin/search_api/processor/ContentAccess.php
index c7108b9..6397329 100644
--- a/src/Plugin/search_api/processor/ContentAccess.php
+++ b/src/Plugin/search_api/processor/ContentAccess.php
@@ -161,7 +161,10 @@ public function addFieldValues(ItemInterface $item) {
       return;
     }
 
-    foreach ($this->filterForPropertyPath($item->getFields(), 'search_api_node_grants') as $field) {
+    $fields = $item->getFields();
+    $fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($fields, NULL, 'search_api_node_grants');
+    foreach ($fields as $field) {
       // Collect grant information for the node.
       if (!$node->access('view', $anonymous_user)) {
         // If anonymous user has no permission we collect all grants with
diff --git a/src/Plugin/search_api/processor/Highlight.php b/src/Plugin/search_api/processor/Highlight.php
index 70812e5..74447f3 100644
--- a/src/Plugin/search_api/processor/Highlight.php
+++ b/src/Plugin/search_api/processor/Highlight.php
@@ -312,7 +312,8 @@ protected function getFulltextFields(array $result_items, array $fulltext_fields
       }
     }
 
-    return $this->extractItemValues($result_items, $fields_by_datasource, $load);
+    return $this->getFieldsHelper()
+      ->extractItemValues($result_items, $fields_by_datasource, $load);
   }
 
   /**
diff --git a/src/Plugin/search_api/processor/RenderedItem.php b/src/Plugin/search_api/processor/RenderedItem.php
index a5a972c..57e07d6 100644
--- a/src/Plugin/search_api/processor/RenderedItem.php
+++ b/src/Plugin/search_api/processor/RenderedItem.php
@@ -270,7 +270,9 @@ public function addFieldValues(ItemInterface $item) {
     // Count of items that don't have a view mode.
     $unset_view_modes = 0;
 
-    foreach ($this->filterForPropertyPath($item->getFields(), 'rendered_item') as $field) {
+    $fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($item->getFields(), NULL, 'rendered_item');
+    foreach ($fields as $field) {
       $configuration = $field->getConfiguration();
 
       // Change the current user to our dummy implementation to ensure we are
@@ -321,8 +323,8 @@ public function addFieldValues(ItemInterface $item) {
   public function calculateDependencies() {
     $this->dependencies = parent::calculateDependencies();
 
-    $fields = $this->index->getFieldsByDatasource(NULL);
-    $fields = $this->filterForPropertyPath($fields, 'rendered_item');
+    $fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($this->index->getFields(), NULL, 'rendered_item');
     foreach ($fields as $field) {
       $view_modes = $field->getConfiguration()['view_mode'];
       foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
@@ -354,8 +356,8 @@ public function onDependencyRemoval(array $dependencies) {
     // dependencies.
     // The code is highly similar to calculateDependencies(), only that we
     // remove the setting (if necessary) instead of adding a dependency.
-    $fields = $this->index->getFieldsByDatasource(NULL);
-    $fields = $this->filterForPropertyPath($fields, 'rendered_item');
+    $fields = $this->getFieldsHelper()
+      ->filterForPropertyPath($this->index->getFields(), NULL, 'rendered_item');
     foreach ($fields as $field) {
       $field_config = $field->getConfiguration();
       $view_modes = $field_config['view_mode'];
diff --git a/src/Processor/ProcessorPluginBase.php b/src/Processor/ProcessorPluginBase.php
index 8f00c62..947ae27 100644
--- a/src/Processor/ProcessorPluginBase.php
+++ b/src/Processor/ProcessorPluginBase.php
@@ -9,7 +9,9 @@
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\Query\ResultSetInterface;
 use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Utility\FieldsHelperInterface;
 use Drupal\search_api\Utility\Utility;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a base class from which other processors may extend.
@@ -45,6 +47,48 @@
 abstract class ProcessorPluginBase extends IndexPluginBase implements ProcessorInterface {
 
   /**
+   * The fields helper.
+   *
+   * @var \Drupal\search_api\Utility\FieldsHelperInterface|null
+   */
+  protected $fieldsHelper;
+
+  /**
+   * {@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->setFieldsHelper($container->get('search_api.fields_helper'));
+
+    return $processor;
+  }
+
+  /**
+   * Retrieves the fields helper.
+   *
+   * @return \Drupal\search_api\Utility\FieldsHelperInterface
+   *   The fields helper.
+   */
+  public function getFieldsHelper() {
+    return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
+  }
+
+  /**
+   * Sets the fields helper.
+   *
+   * @param \Drupal\search_api\Utility\FieldsHelperInterface $fields_helper
+   *   The new fields helper.
+   *
+   * @return $this
+   */
+  public function setFieldsHelper(FieldsHelperInterface $fields_helper) {
+    $this->fieldsHelper = $fields_helper;
+    return $this;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public static function supportsIndex(IndexInterface $index) {
@@ -113,6 +157,35 @@ public function addFieldValues(ItemInterface $item) {}
   public function preIndexSave() {}
 
   /**
+   * {@inheritdoc}
+   */
+  public function alterIndexedItems(array &$items) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preprocessIndexItems(array $items) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preprocessSearchQuery(QueryInterface $query) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postprocessSearchResults(ResultSetInterface $results) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresReindexing(array $old_settings = NULL, array $new_settings = NULL) {
+    // Only require re-indexing for processors that actually run during the
+    // indexing process.
+    return $this->supportsStage(ProcessorInterface::STAGE_PREPROCESS_INDEX);
+  }
+
+  /**
    * Ensures that a field with certain properties is indexed on the index.
    *
    * Can be used as a helper method in preIndexSave().
@@ -137,13 +210,16 @@ protected function ensureField($datasource_id, $property_path, $type = NULL) {
     $field = $this->findField($datasource_id, $property_path, $type);
 
     if (!$field) {
-      $property = Utility::retrieveNestedProperty($this->index->getPropertyDefinitions($datasource_id), $property_path);
+      $properties = $this->index->getPropertyDefinitions($datasource_id);
+      $property = $this->getFieldsHelper()
+        ->retrieveNestedProperty($properties, $property_path);
       if (!$property) {
         $property_id = Utility::createCombinedId($datasource_id, $property_path);
         $processor_label = $this->label();
         throw new SearchApiException("Could not find property '$property_id' which is required by the '$processor_label' processor.");
       }
-      $field = Utility::createFieldFromProperty($this->index, $property, $datasource_id, $property_path, NULL, $type);
+      $field = $this->getFieldsHelper()
+        ->createFieldFromProperty($this->index, $property, $datasource_id, $property_path, NULL, $type);
       $this->index->addField($field);
     }
 
@@ -171,8 +247,8 @@ protected function ensureField($datasource_id, $property_path, $type = NULL) {
    */
   protected function findField($datasource_id, $property_path, $type = NULL) {
     foreach ($this->index->getFieldsByDatasource($datasource_id) as $field) {
-      if ($field->getPropertyPath() == $property_path) {
-        if (!isset($type) || $field->getType() == $type) {
+      if ($field->getPropertyPath() === $property_path) {
+        if (!isset($type) || $field->getType() === $type) {
           return $field;
         }
       }
@@ -192,11 +268,15 @@ protected function findField($datasource_id, $property_path, $type = NULL) {
    *
    * @return \Drupal\search_api\Item\FieldInterface[]
    *   All fields with the given property path.
+   *
+   * @deprecated Will be removed by 8.x-1.0 RC1. Use
+   *   \Drupal\search_api\Utility\FieldsHelperInterface::filterForPropertyPath()
+   *   instead.
    */
   protected function filterForPropertyPath(array $fields, $property_path) {
     $found_fields = array();
     foreach ($fields as $field_id => $field) {
-      if ($field->getPropertyPath() == $property_path) {
+      if ($field->getPropertyPath() === $property_path) {
         $found_fields[$field_id] = $field;
       }
     }
@@ -223,125 +303,14 @@ protected function filterForPropertyPath(array $fields, $property_path) {
    * @return mixed[][][]
    *   Arrays of field values, keyed by items' indexes in $items and the given
    *   field IDs from $required_properties.
+   *
+   * @deprecated Will be removed by 8.x-1.0 RC1. Use
+   *   \Drupal\search_api\Utility\FieldsHelperInterface::extractItemValues()
+   *   instead.
    */
   protected function extractItemValues(array $items, array $required_properties, $load = TRUE) {
-    $extracted_values = array();
-
-    foreach ($items as $i => $item) {
-      $item_values = array();
-      /** @var \Drupal\search_api\Item\FieldInterface[][] $missing_fields */
-      $missing_fields = array();
-      $processor_fields = array();
-      $needed_processors = array();
-      foreach (array(NULL, $item->getDatasourceId()) as $datasource_id) {
-        if (empty($required_properties[$datasource_id])) {
-          continue;
-        }
-
-        $properties = $this->index->getPropertyDefinitions($datasource_id);
-        foreach ($required_properties[$datasource_id] as $property_path => $combined_id) {
-          // If a field with the right property path is already set on the item,
-          // use it. This might actually make problems in case the values have
-          // already been processed in some way, or use a data type that
-          // transformed their original value. But that will hopefully not be a
-          // problem in most situations.
-          foreach ($this->filterForPropertyPath($item->getFields(FALSE), $property_path) as $field) {
-            if ($field->getDatasourceId() === $datasource_id) {
-              $item_values[$combined_id] = $field->getValues();
-              continue 2;
-            }
-          }
-
-          // There are no values present on the item for this property. If we
-          // don't want to extract any fields, skip it.
-          if (!$load) {
-            continue;
-          }
-
-          // If the field is not already on the item, we need to extract it. We
-          // set our own combined ID as the field identifier as kind of a hack,
-          // to easily be able to add the field values to $property_values
-          // afterwards.
-          $property = NULL;
-          if (isset($properties[$property_path])) {
-            $property = $properties[$property_path];
-          }
-          if ($property instanceof ProcessorPropertyInterface) {
-            $field_info = array(
-              'datasource_id' => $datasource_id,
-              'property_path' => $property_path,
-            );
-            if ($property instanceof ConfigurablePropertyInterface) {
-              $field_info['configuration'] = $property->defaultConfiguration();
-            }
-            $processor_fields[] = Utility::createField($this->index, $combined_id, $field_info);
-            $needed_processors[$property->getProcessorId()] = TRUE;
-          }
-          elseif ($datasource_id) {
-            $missing_fields[$property_path][] = Utility::createField($this->index, $combined_id);
-          }
-          else {
-            // Extracting properties without a datasource is pointless.
-            $item_values[$combined_id] = array();
-          }
-        }
-      }
-      if ($missing_fields) {
-        Utility::extractFields($item->getOriginalObject(), $missing_fields);
-        foreach ($missing_fields as $property_fields) {
-          foreach ($property_fields as $field) {
-            $item_values[$field->getFieldIdentifier()] = $field->getValues();
-          }
-        }
-      }
-      if ($processor_fields) {
-        $dummy_item = clone $item;
-        $dummy_item->setFields($processor_fields);
-        $processors = $this->index->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES);
-        foreach ($processors as $processor_id => $processor) {
-          // Avoid an infinite recursion.
-          if (isset($needed_processors[$processor_id]) && $processor != $this) {
-            $processor->addFieldValues($dummy_item);
-          }
-        }
-        foreach ($processor_fields as $field) {
-          $item_values[$field->getFieldIdentifier()] = $field->getValues();
-        }
-      }
-
-      $extracted_values[$i] = $item_values;
-    }
-
-    return $extracted_values;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function alterIndexedItems(array &$items) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  public function preprocessIndexItems(array $items) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  public function preprocessSearchQuery(QueryInterface $query) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  public function postprocessSearchResults(ResultSetInterface $results) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  public function requiresReindexing(array $old_settings = NULL, array $new_settings = NULL) {
-    // Only require re-indexing for processors that actually run during the
-    // indexing process.
-    return $this->supportsStage(ProcessorInterface::STAGE_PREPROCESS_INDEX);
+    return $this->getFieldsHelper()
+      ->extractItemValues($items, $required_properties, $load);
   }
 
 }
diff --git a/src/Utility/FieldsHelper.php b/src/Utility/FieldsHelper.php
index 383bde7..43262d6 100644
--- a/src/Utility/FieldsHelper.php
+++ b/src/Utility/FieldsHelper.php
@@ -20,8 +20,9 @@
 use Drupal\search_api\Item\FieldInterface;
 use Drupal\search_api\Item\Item;
 use Drupal\search_api\Processor\ConfigurablePropertyInterface;
+use Drupal\search_api\Processor\ProcessorInterface;
+use Drupal\search_api\Processor\ProcessorPropertyInterface;
 use Drupal\search_api\SearchApiException;
-use Drupal\search_api\Utility\Utility;
 use Symfony\Component\DependencyInjection\Container;
 
 /**
@@ -178,6 +179,119 @@ public function extractFieldValues(TypedDataInterface $data) {
   /**
    * {@inheritdoc}
    */
+  public function extractItemValues(array $items, array $required_properties, $load = TRUE) {
+    $extracted_values = array();
+
+    /** @var \Drupal\search_api\Item\ItemInterface $item */
+    foreach ($items as $i => $item) {
+      $index = $item->getIndex();
+      $item_values = array();
+      /** @var \Drupal\search_api\Item\FieldInterface[][] $missing_fields */
+      $missing_fields = array();
+      $processor_fields = array();
+      $needed_processors = array();
+      foreach (array(NULL, $item->getDatasourceId()) as $datasource_id) {
+        if (empty($required_properties[$datasource_id])) {
+          continue;
+        }
+
+        $properties = $index->getPropertyDefinitions($datasource_id);
+        foreach ($required_properties[$datasource_id] as $property_path => $combined_id) {
+          $item_values[$combined_id] = array();
+
+          // If a field with the right property path is already set on the item,
+          // use it. This might actually make problems in case the values have
+          // already been processed in some way, or use a data type that
+          // transformed their original value. But that will hopefully not be a
+          // problem in most situations.
+          foreach ($this->filterForPropertyPath($item->getFields(FALSE), $datasource_id, $property_path) as $field) {
+            $item_values[$combined_id] = $field->getValues();
+            continue 2;
+          }
+
+          // There are no values present on the item for this property. If we
+          // don't want to extract any fields, skip it.
+          if (!$load) {
+            continue;
+          }
+
+          // If the field is not already on the item, we need to extract it. We
+          // set our own combined ID as the field identifier as kind of a hack,
+          // to easily be able to add the field values to $property_values
+          // afterwards.
+          $property = NULL;
+          if (isset($properties[$property_path])) {
+            $property = $properties[$property_path];
+          }
+          if ($property instanceof ProcessorPropertyInterface) {
+            $field_info = array(
+              'datasource_id' => $datasource_id,
+              'property_path' => $property_path,
+            );
+            if ($property instanceof ConfigurablePropertyInterface) {
+              $field_info['configuration'] = $property->defaultConfiguration();
+              // If the index contains a field with that property, just use the
+              // configuration from there instead of the default configuration.
+              // This will probably be what users expect in most situations.
+              foreach ($this->filterForPropertyPath($index->getFields(), $datasource_id, $property_path) as $field) {
+                $field_info['configuration'] = $field->getConfiguration();
+                break;
+              }
+            }
+            $processor_fields[] = $this->createField($index, $combined_id, $field_info);
+            $needed_processors[$property->getProcessorId()] = TRUE;
+          }
+          elseif ($datasource_id) {
+            $missing_fields[$property_path][] = $this->createField($index, $combined_id);
+          }
+        }
+      }
+      if ($missing_fields) {
+        $this->extractFields($item->getOriginalObject(), $missing_fields);
+        foreach ($missing_fields as $property_fields) {
+          foreach ($property_fields as $field) {
+            $item_values[$field->getFieldIdentifier()] = $field->getValues();
+          }
+        }
+      }
+      if ($processor_fields) {
+        $dummy_item = clone $item;
+        $dummy_item->setFields($processor_fields);
+        $dummy_item->setFieldsExtracted(TRUE);
+        $processors = $index->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES);
+        foreach ($processors as $processor_id => $processor) {
+          // Avoid an infinite recursion.
+          if (isset($needed_processors[$processor_id]) && $processor != $this) {
+            $processor->addFieldValues($dummy_item);
+          }
+        }
+        foreach ($processor_fields as $field) {
+          $item_values[$field->getFieldIdentifier()] = $field->getValues();
+        }
+      }
+
+      $extracted_values[$i] = $item_values;
+    }
+
+    return $extracted_values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filterForPropertyPath(array $fields, $datasource_id, $property_path) {
+    $found_fields = array();
+    foreach ($fields as $field_id => $field) {
+      if ($field->getDatasourceId() === $datasource_id && $field->getPropertyPath() === $property_path) {
+        $found_fields[$field_id] = $field;
+      }
+    }
+    return $found_fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getNestedProperties(ComplexDataDefinitionInterface $property) {
     $nestedProperties = $property->getPropertyDefinitions();
     if ($property instanceof EntityDataDefinitionInterface) {
diff --git a/src/Utility/FieldsHelperInterface.php b/src/Utility/FieldsHelperInterface.php
index 9354691..06a5c15 100644
--- a/src/Utility/FieldsHelperInterface.php
+++ b/src/Utility/FieldsHelperInterface.php
@@ -51,6 +51,46 @@ public function extractField(TypedDataInterface $data, FieldInterface $field);
   public function extractFieldValues(TypedDataInterface $data);
 
   /**
+   * Extracts property values from items.
+   *
+   * Values are taken from existing fields on the item, where present, and are
+   * otherwise extracted from the item's underlying object.
+   *
+   * @param \Drupal\search_api\Item\ItemInterface[] $items
+   *   The items from which properties should be extracted.
+   * @param string[][] $required_properties
+   *   The properties that should be extracted, keyed by datasource ID and
+   *   property path, with the values being the IDs that the values should be
+   *   put under in the return value.
+   * @param bool $load
+   *   (optional) If FALSE, only field values already present will be returned.
+   *   Otherwise, fields will be extracted (and underlying objects loaded) if
+   *   necessary.
+   *
+   * @return mixed[][][]
+   *   Arrays of field values, keyed by items' indexes in $items and the given
+   *   field IDs from $required_properties.
+   */
+  public function extractItemValues(array $items, array $required_properties, $load = TRUE);
+
+  /**
+   * Filters the given fields for those with the specified property path.
+   *
+   * Array keys will be preserved.
+   *
+   * @param \Drupal\search_api\Item\FieldInterface[] $fields
+   *   The fields to filter.
+   * @param string|null $datasource_id
+   *   The datasource ID of the fields that should be returned.
+   * @param string $property_path
+   *   The searched property path on the item.
+   *
+   * @return \Drupal\search_api\Item\FieldInterface[]
+   *   All fields with the given property path.
+   */
+  public function filterForPropertyPath(array $fields, $datasource_id, $property_path);
+
+  /**
    * Retrieves a list of nested properties from a complex property.
    *
    * Takes care of including bundle-specific properties for entity reference
diff --git a/tests/src/Kernel/FieldValuesExtractionTest.php b/tests/src/Kernel/FieldValuesExtractionTest.php
index 110977f..8ebf023 100644
--- a/tests/src/Kernel/FieldValuesExtractionTest.php
+++ b/tests/src/Kernel/FieldValuesExtractionTest.php
@@ -3,11 +3,15 @@
 namespace Drupal\Tests\search_api\Kernel;
 
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\search_api\Entity\Index;
 use Drupal\search_api\Utility\Utility;
+use Drupal\user\Entity\Role;
 
 /**
  * Tests extraction of field values, as used during indexing.
  *
+ * @coversDefaultClass \Drupal\search_api\Utility\FieldsHelper
+ *
  * @group search_api
  */
 class FieldValuesExtractionTest extends KernelTestBase {
@@ -27,6 +31,13 @@ class FieldValuesExtractionTest extends KernelTestBase {
   protected $entities = array();
 
   /**
+   * The fields helper service.
+   *
+   * @var \Drupal\search_api\Utility\FieldsHelperInterface
+   */
+  protected $fieldsHelper;
+
+  /**
    * Modules to enable for this test.
    *
    * @var string[]
@@ -80,27 +91,67 @@ public function setUp() {
     ));
     $this->entities[2]->save();
 
-    $this->index = $this->getMock('Drupal\search_api\IndexInterface');
+    Role::create(array(
+      'id' => 'anonymous',
+      'label' => 'anonymous',
+    ))->save();
+    user_role_grant_permissions('anonymous', array('view test entity'));
+
+    $this->index = Index::create(array(
+      'field_settings' => array(
+        'foo' => array(
+          'type' => 'text',
+          'datasource_id' => 'entity:entity_test_mulrev_changed',
+          'property_path' => 'name',
+        ),
+        'bar' => array(
+          'type' => 'text',
+          'property_path' => 'rendered_item',
+          'configuration' => array(
+            'roles' => array(
+              'anonymous' => 'anonymous',
+            ),
+            'view_mode' => array(
+              'entity:entity_test_mulrev_changed' => array(
+                'article' => 'default',
+              ),
+            ),
+          ),
+        ),
+      ),
+      'datasource_settings' => array(
+        'entity:entity_test_mulrev_changed' => array(
+          'plugin_id' => 'entity:entity_test_mulrev_changed',
+          'settings' => array(),
+        ),
+      ),
+    ));
+
+    $this->fieldsHelper = $this->container->get('search_api.fields_helper');
   }
 
   /**
    * Tests extraction of field values, as used during indexing.
+   *
+   * @covers ::extractFields
+   * @covers ::extractField
+   * @covers ::extractFieldValues
    */
   public function testFieldValuesExtraction() {
     $object = $this->entities[3]->getTypedData();
     /** @var \Drupal\search_api\Item\FieldInterface[][] $fields */
     $fields = array(
-      'type' => array(Utility::createField($this->index, 'type')),
-      'name' => array(Utility::createField($this->index, 'name')),
+      'type' => array($this->fieldsHelper->createField($this->index, 'type')),
+      'name' => array($this->fieldsHelper->createField($this->index, 'name')),
       'links:entity:name' => array(
-        Utility::createField($this->index, 'links'),
-        Utility::createField($this->index, 'links_1'),
+        $this->fieldsHelper->createField($this->index, 'links'),
+        $this->fieldsHelper->createField($this->index, 'links_1'),
       ),
       'links:entity:links:entity:name' => array(
-        Utility::createField($this->index, 'links_links'),
+        $this->fieldsHelper->createField($this->index, 'links_links'),
       ),
     );
-    Utility::extractFields($object, $fields);
+    $this->fieldsHelper->extractFields($object, $fields);
 
     $values = array();
     foreach ($fields as $property_path => $property_fields) {
@@ -131,4 +182,105 @@ public function testFieldValuesExtraction() {
     $this->assertEquals($expected, $values, 'Field values were correctly extracted');
   }
 
+  /**
+   * Tests extraction of properties, as used in processors or for result lists.
+   *
+   * @covers ::extractItemValues
+   */
+  public function testPropertyValuesExtraction() {
+    $items['foobar'] = $this->fieldsHelper->createItemFromObject(
+      $this->index,
+      $this->entities[0]->getTypedData(),
+      Utility::createCombinedId('entity:entity_test_mulrev_changed', '0:en')
+    );
+
+    $properties = array(
+      NULL => array(
+        'rendered_item' => 'a',
+        // Since there is no field defined on "aggregated_field" for the index,
+        // we won't be able to extract it.
+        'aggregated_field' => 'b',
+        'search_api_url' => 'c',
+      ),
+      'entity:entity_test_mulrev_changed' => array(
+        'name' => 'd',
+        'type' => 'e',
+      ),
+      'unknown_datasource' => array(
+        'name' => 'x',
+      ),
+    );
+
+    $expected = array(
+      'foobar' => array(
+        'a' => array(),
+        'b' => array(),
+        'c' => array(),
+        'd' => array(),
+        'e' => array(),
+      ),
+    );
+    $values = $this->fieldsHelper->extractItemValues($items, $properties, FALSE);
+    ksort($values['foobar']);
+    $this->assertEquals($expected, $values);
+
+    $expected = array(
+      'foobar' => array(
+        'b' => array(),
+        'c' => array('/entity_test_mulrev_changed/manage/1'),
+        'd' => array('Article 1'),
+        'e' => array('article'),
+      ),
+    );
+    $values = $this->fieldsHelper->extractItemValues($items, $properties);
+    ksort($values['foobar']);
+    $this->assertArrayHasKey('a', $values['foobar']);
+    $this->assertNotEmpty($values['foobar']['a']);
+    $this->assertContains('Article 1', $values['foobar']['a'][0]);
+    unset($values['foobar']['a']);
+    $this->assertEquals($expected, $values);
+
+    $items['foobar']->setFields(array(
+      'aa' => $this->fieldsHelper->createField($this->index, 'aa_foo', array(
+        'property_path' => 'aggregated_field',
+        'values' => array(1, 2),
+      )),
+      'bb' => $this->fieldsHelper->createField($this->index, 'aa_foo', array(
+        'property_path' => 'rendered_item',
+        'values' => array(3),
+      )),
+      'cc' => $this->fieldsHelper->createField($this->index, 'aa_foo', array(
+        'datasource_id' => 'entity:entity_test_mulrev_changed',
+        'property_path' => 'type',
+        'values' => array(4),
+      )),
+    ));
+
+    $expected = array(
+      'foobar' => array(
+        'a' => array(3),
+        'b' => array(1, 2),
+        'c' => array(),
+        'd' => array(),
+        'e' => array(4),
+      ),
+    );
+    $values = $this->fieldsHelper->extractItemValues($items, $properties, FALSE);
+    ksort($values['foobar']);
+    $this->assertEquals($expected, $values);
+
+    $expected = array(
+      'foobar' => array(
+        'a' => array(3),
+        'b' => array(1, 2),
+        'c' => array('/entity_test_mulrev_changed/manage/1'),
+        'd' => array('Article 1'),
+        'e' => array(4),
+      ),
+    );
+    $values = $this->fieldsHelper->extractItemValues($items, $properties);
+    ksort($values['foobar']);
+    $this->assertEquals($expected, $values);
+  }
+
 }
diff --git a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
index c0d2b3f..816f59a 100644
--- a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
+++ b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
@@ -326,10 +326,9 @@ public function testFieldExtraction() {
         ),
       ),
     ));
-    $index->method('getFieldsByDatasource')
-      ->willReturnMap(array(
-        array(NULL, array('aggregated_field' => $field)),
-      ));
+    $index->method('getFields')->willReturn(array(
+      'aggregated_field' => $field,
+    ));
     $index->method('getPropertyDefinitions')
       ->willReturnMap(array(
         array(
