diff --git a/src/Plugin/search_api/processor/AggregatedFields.php b/src/Plugin/search_api/processor/AggregatedFields.php
index 5f4a8f0..6e6554e 100644
--- a/src/Plugin/search_api/processor/AggregatedFields.php
+++ b/src/Plugin/search_api/processor/AggregatedFields.php
@@ -5,9 +5,7 @@
 use Drupal\search_api\Datasource\DatasourceInterface;
 use Drupal\search_api\Item\ItemInterface;
 use Drupal\search_api\Plugin\search_api\processor\Property\AggregatedFieldProperty;
-use Drupal\search_api\Processor\ProcessorInterface;
 use Drupal\search_api\Processor\ProcessorPluginBase;
-use Drupal\search_api\Processor\ProcessorPropertyInterface;
 use Drupal\search_api\Utility;
 
 /**
@@ -64,70 +62,7 @@ public function addFieldValues(ItemInterface $item) {
       }
     }
 
-    // Extract the required properties.
-    $property_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) {
-      $properties = $this->index->getPropertyDefinitions($datasource_id);
-      foreach ($required_properties_by_datasource[$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) {
-            $property_values[$combined_id] = $field->getValues();
-            continue 2;
-          }
-        }
-
-        // 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) {
-          $processor_fields[] = Utility::createField($this->index, $combined_id);
-          $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.
-          $property_values[$combined_id] = array();
-        }
-      }
-    }
-    if ($missing_fields) {
-      Utility::extractFields($item->getOriginalObject(), $missing_fields);
-      foreach ($missing_fields as $property_fields) {
-        foreach ($property_fields as $field) {
-          $property_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) {
-        $property_values[$field->getFieldIdentifier()] = $field->getValues();
-      }
-    }
+    $property_values = $this->extractItemValues(array($item), $required_properties_by_datasource)[0];
 
     $aggregated_fields = $this->filterForPropertyPath($item->getFields(), 'aggregated_field');
     foreach ($aggregated_fields as $aggregated_field) {
diff --git a/src/Plugin/search_api/processor/Highlight.php b/src/Plugin/search_api/processor/Highlight.php
index dc73530..78d0dbd 100644
--- a/src/Plugin/search_api/processor/Highlight.php
+++ b/src/Plugin/search_api/processor/Highlight.php
@@ -6,7 +6,6 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
-use Drupal\search_api\SearchApiException;
 use Drupal\search_api\Processor\ProcessorPluginBase;
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\Query\ResultSetInterface;
@@ -217,9 +216,8 @@ protected function addExcerpts(array $results, array $fulltext_fields, array $ke
     $items = $this->getFulltextFields($results, $fulltext_fields);
     foreach ($items as $item_id => $item) {
       $text = array();
-      /** @var \Drupal\search_api\Item\FieldInterface $field */
-      foreach ($item as $field) {
-        $text = array_merge($text, $field->getValues());
+      foreach ($item as $values) {
+        $text = array_merge($text, $values);
       }
       // @todo This is pretty poor handling for the borders between different
       //   values/fields. Better would be to pass an array and have proper
@@ -246,8 +244,7 @@ protected function highlightFields(array $results, array $keys) {
     $highlighted_fields = array();
     foreach ($item_fields as $item_id => $fields) {
       /** @var \Drupal\search_api\Item\FieldInterface $field */
-      foreach ($fields as $field_id => $field) {
-        $values = $field->getValues();
+      foreach ($fields as $field_id => $values) {
         $change = FALSE;
         foreach ($values as $i => $value) {
           $values[$i] = $this->highlightField($value, $keys);
@@ -281,10 +278,6 @@ protected function highlightFields(array $results, array $keys) {
    *   then field ID.
    */
   protected function getFulltextFields(array $result_items, array $fulltext_fields = NULL, $load = TRUE) {
-    // @todo Add some caching, since this will sometimes be called twice for the
-    //   same result set.
-    $items = array();
-
     // All the index's fulltext fields, grouped by datasource.
     $fields_by_datasource = array();
     foreach ($this->index->getFields() as $field_id => $field) {
@@ -292,63 +285,11 @@ protected function getFulltextFields(array $result_items, array $fulltext_fields
         continue;
       }
       if (Utility::isTextType($field->getType())) {
-        $fields_by_datasource[$field->getDatasourceId()][$field_id] = $field;
-      }
-    }
-
-    $needs_extraction = array();
-    foreach ($result_items as $item_id => $result_item) {
-      $datasource_id = $result_item->getDatasourceId();
-      // Make sure this datasource even has any indexed fulltext fields.
-      if (empty($fields_by_datasource[$datasource_id])) {
-        continue;
-      }
-      /** @var \Drupal\search_api\Item\FieldInterface $field */
-      foreach ($fields_by_datasource[$datasource_id] as $field_id => $field) {
-        if ($result_item->getField($field_id, FALSE)) {
-          $items[$item_id][$field_id] = $result_item->getField($field_id, FALSE);
-        }
-        elseif ($load) {
-          $needs_extraction[$item_id][$field->getPropertyPath()][] = clone $field;
-        }
-      }
-    }
-
-    $needs_load = array();
-    foreach ($needs_extraction as $item_id => $fields) {
-      if (!$result_items[$item_id]->getOriginalObject(FALSE)) {
-        $needs_load[$item_id] = $item_id;
-      }
-    }
-
-    if ($needs_load) {
-      foreach ($this->index->loadItemsMultiple($needs_load) as $item_id => $object) {
-        $result_items[$item_id]->setOriginalObject($object);
-        unset($needs_load[$item_id]);
-      }
-    }
-
-    // Remove the fields for all items that couldn't be loaded.
-    $needs_extraction = array_diff_key($needs_extraction, $needs_load);
-
-    foreach ($needs_extraction as $item_id => $fields) {
-      try {
-        Utility::extractFields($result_items[$item_id]->getOriginalObject(), $fields);
-        foreach ($fields as $property_fields) {
-          foreach ($property_fields as $field) {
-            $field_id = $field->getFieldIdentifier();
-            $result_items[$item_id]->setField($field_id, $field);
-            $items[$item_id][$field_id] = $field;
-          }
-        }
-      }
-      catch (SearchApiException $e) {
-        // Missing highlighting will be the least problem in this case – just
-        // ignore it.
+        $fields_by_datasource[$field->getDatasourceId()][$field->getPropertyPath()] = $field_id;
       }
     }
 
-    return $items;
+    return $this->extractItemValues($result_items, $fields_by_datasource, $load);
   }
 
   /**
diff --git a/src/Processor/ProcessorPluginBase.php b/src/Processor/ProcessorPluginBase.php
index 07c93da..c10de94 100644
--- a/src/Processor/ProcessorPluginBase.php
+++ b/src/Processor/ProcessorPluginBase.php
@@ -204,6 +204,114 @@ protected function filterForPropertyPath(array $fields, $property_path) {
   }
 
   /**
+   * 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.
+   */
+  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) {
+            $processor_fields[] = Utility::createField($this->index, $combined_id, array(
+              'datasource_id' => $datasource_id,
+              'property_path' => $property_path,
+            ));
+            $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 preprocessIndexItems(array &$items) {}
diff --git a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
index 0d84adc..df9918d 100644
--- a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
+++ b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php
@@ -2,8 +2,12 @@
 
 namespace Drupal\Tests\search_api\Unit\Plugin\Processor;
 
+use Drupal\Core\TypedData\DataDefinition;
 use Drupal\search_api\Entity\Index;
+use Drupal\search_api\Item\ItemInterface;
 use Drupal\search_api\Plugin\search_api\processor\AggregatedFields;
+use Drupal\search_api\Processor\ProcessorInterface;
+use Drupal\search_api\Processor\ProcessorProperty;
 use Drupal\search_api\Utility;
 use Drupal\Tests\UnitTestCase;
 
@@ -239,4 +243,128 @@ public function testAlterPropertyDefinitions() {
     $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
   }
 
+  /**
+   * Tests that field extraction in the processor works correctly.
+   */
+  public function testFieldExtraction() {
+    /** @var \Drupal\Tests\search_api\TestComplexDataInterface|\PHPUnit_Framework_MockObject_MockObject $object */
+    $object = $this->getMock('Drupal\Tests\search_api\TestComplexDataInterface');
+    $bar_foo_property = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
+    $bar_foo_property->method('getValue')
+      ->willReturn('value3');
+    $bar_foo_property->method('getDataDefinition')
+      ->willReturn(new DataDefinition());
+    $bar_property = $this->getMock('Drupal\Tests\search_api\TestComplexDataInterface');
+    $bar_property->method('get')
+      ->willReturnMap(array(
+        array('foo', $bar_foo_property),
+      ));
+    $foobar_property = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
+    $foobar_property->method('getValue')
+      ->willReturn('wrong_value2');
+    $foobar_property->method('getDataDefinition')
+      ->willReturn(new DataDefinition());
+    $object->method('get')
+      ->willReturnMap(array(
+        array('bar', $bar_property),
+        array('foobar', $foobar_property),
+      ));
+
+    /** @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject $index */
+    $index = $this->getMock('Drupal\search_api\IndexInterface');
+
+    $field = Utility::createField($index, 'aggregated_field', array(
+      'property_path' => 'aggregated_field',
+      'configuration' => array(
+        'type' => 'union',
+        'fields' => array(
+          'aggregated_field',
+          'foo',
+          'entity:test1/bar:foo',
+          'entity:test1/baz',
+          'entity:test2/foobar',
+        ),
+      ),
+    ));
+    $index->method('getFieldsByDatasource')
+      ->willReturnMap(array(
+        array(NULL, array('aggregated_field' => $field)),
+      ));
+    $index->method('getPropertyDefinitions')
+      ->willReturnMap(array(
+        array(
+          NULL,
+          array(
+            'foo' => new ProcessorProperty(array(
+              'processor_id' => 'processor1',
+            )),
+          ),
+        ),
+        array(
+          'entity:test1',
+          array(
+            'bar' => new DataDefinition(),
+            'foobar' => new DataDefinition(),
+          ),
+        ),
+      ));
+    $processor_mock = $this->getMock('Drupal\search_api\Processor\ProcessorInterface');
+    $processor_mock->method('addFieldValues')
+      ->willReturnCallback(function (ItemInterface $item) {
+        foreach ($item->getFields(FALSE) as $field) {
+          if ($field->getCombinedPropertyPath() == 'foo') {
+            $field->setValues(array('value4', 'value5'));
+          }
+        }
+      });
+    $index->method('getProcessorsByStage')
+      ->willReturnMap(array(
+        array(
+          ProcessorInterface::STAGE_ADD_PROPERTIES,
+          array(
+            'aggregated_field' => $this->processor,
+            'processor1' => $processor_mock,
+          ),
+        ),
+      ));
+    $this->processor->setIndex($index);
+
+    /** @var \Drupal\search_api\Datasource\DatasourceInterface|\PHPUnit_Framework_MockObject_MockObject $datasource */
+    $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
+    $datasource->method('getPluginId')
+      ->willReturn('entity:test1');
+
+    $item = Utility::createItem($index, 'id', $datasource);
+    $item->setOriginalObject($object);
+    $item->setField('aggregated_field', clone $field);
+    $item->setField('test1', Utility::createField($index, 'test1', array(
+      'property_path' => 'baz',
+      'values' => array(
+        'wrong_value1',
+      ),
+    )));
+    $item->setField('test2', Utility::createField($index, 'test2', array(
+      'datasource_id' => 'entity:test1',
+      'property_path' => 'baz',
+      'values' => array(
+        'value1',
+        'value2',
+      ),
+    )));
+    $item->setFieldsExtracted(TRUE);
+
+    $this->processor->addFieldValues($item);
+
+    $expected = array(
+      'value1',
+      'value2',
+      'value3',
+      'value4',
+      'value5',
+    );
+    $actual = $item->getField('aggregated_field')->getValues();
+    sort($actual);
+    $this->assertEquals($expected, $actual);
+  }
+
 }
diff --git a/tests/src/Unit/Plugin/Processor/HighlightTest.php b/tests/src/Unit/Plugin/Processor/HighlightTest.php
index d55ab9d..6c43393 100644
--- a/tests/src/Unit/Plugin/Processor/HighlightTest.php
+++ b/tests/src/Unit/Plugin/Processor/HighlightTest.php
@@ -2,7 +2,13 @@
 
 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;
 use Drupal\search_api\Plugin\search_api\processor\Highlight;
+use Drupal\search_api\Processor\ProcessorInterface;
+use Drupal\search_api\Processor\ProcessorProperty;
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\Query\ResultSet;
 use Drupal\search_api\Utility;
@@ -148,10 +154,7 @@ public function testPostprocessSearchResultsWithResults() {
       ->will($this->returnValue('foo'));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -197,10 +200,7 @@ public function testPostprocessSearchResultsWithChangedPrefixSuffix() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -243,10 +243,7 @@ public function testPostprocessSearchResultsWithoutHighlight() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -287,10 +284,7 @@ public function testPostprocessSearchResultsWithPreviousHighlighting() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -336,10 +330,7 @@ public function testPostprocessSearchResultsExcerpt() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'congue')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -382,10 +373,7 @@ public function testPostprocessSearchResultsExerptMatchNearEnd() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'diam')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -430,10 +418,7 @@ public function testPostprocessSearchResultsWithChangedExcerptLength() {
       ->will($this->returnValue('congue'));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -478,10 +463,7 @@ public function testPostprocessSearchResultsWithoutExcerpt() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'congue')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $field = Utility::createField($this->index, 'body');
-    $field->setType('text');
-    $field->setDatasourceId('entity:node');
-    $field->setPropertyPath('body');
+    $field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -538,10 +520,7 @@ public function testPostprocessSearchResultsWithComplexKeys() {
       ->will($this->returnValue($keys));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = Utility::createField($this->index, 'body');
-    $body_field->setType('text');
-    $body_field->setDatasourceId('entity:node');
-    $body_field->setPropertyPath('body');
+    $body_field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -586,14 +565,8 @@ public function testPostprocessSearchResultsWithTwoFields() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = Utility::createField($this->index, 'body');
-    $body_field->setType('text');
-    $body_field->setDatasourceId('entity:node');
-    $body_field->setPropertyPath('body');
-    $title_field = Utility::createField($this->index, 'title');
-    $title_field->setType('text');
-    $title_field->setDatasourceId('entity:node');
-    $title_field->setPropertyPath('title');
+    $body_field = $this->createField('body', 'entity:node/body');
+    $title_field = $this->createField('title', 'title');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -630,8 +603,11 @@ public function testPostprocessSearchResultsWithTwoFields() {
     $this->assertEquals('<strong>foo</strong> bar', $output[$this->itemIds[0]]['body'][1], 'Highlighting is correctly applied to second body field value.');
     $this->assertEquals('Title <strong>foo</strong>', $output[$this->itemIds[0]]['title'][0], 'Highlighting is correctly applied to title field.');
 
-    $excerpt = '… Some <strong>foo</strong> value … <strong>foo</strong> bar … Title <strong>foo</strong> …';
-    $this->assertEquals($excerpt, $items[$this->itemIds[0]]->getExcerpt(), 'Correct excerpt created from two text fields.');
+    $excerpt = $items[$this->itemIds[0]]->getExcerpt();
+    $this->assertContains('Some <strong>foo</strong> value', $excerpt);
+    $this->assertContains('<strong>foo</strong> bar', $excerpt);
+    $this->assertContains('Title <strong>foo</strong>', $excerpt);
+    $this->assertEquals(4, substr_count($excerpt, '…'));
   }
 
   /**
@@ -647,10 +623,7 @@ public function testPostprocessSearchResultsWithTwoItems() {
       ->will($this->returnValue(array('#conjunction' => 'OR', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = Utility::createField($this->index, 'body');
-    $body_field->setType('text');
-    $body_field->setDatasourceId('entity:node');
-    $body_field->setPropertyPath('body');
+    $body_field = $this->createField('body', 'entity:node/body');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -701,14 +674,8 @@ public function testExcerptExcludeFields() {
       ->will($this->returnValue(array('#conjunction' => 'AND', 'foo')));
     /** @var \Drupal\search_api\Query\QueryInterface $query */
 
-    $body_field = Utility::createField($this->index, 'body');
-    $body_field->setType('text');
-    $body_field->setDatasourceId('entity:node');
-    $body_field->setPropertyPath('body');
-    $title_field = Utility::createField($this->index, 'title');
-    $title_field->setType('text');
-    $title_field->setDatasourceId('entity:node');
-    $title_field->setPropertyPath('title');
+    $body_field = $this->createField('body', 'entity:node/body');
+    $title_field = $this->createField('title', 'title');
 
     $this->index->expects($this->atLeastOnce())
       ->method('getFields')
@@ -754,6 +721,156 @@ public function testExcerptExcludeFields() {
   }
 
   /**
+   * Tests that field extraction in the processor works correctly.
+   */
+  public function testFieldExtraction() {
+    /** @var \Drupal\Tests\search_api\TestComplexDataInterface|\PHPUnit_Framework_MockObject_MockObject $object */
+    $object = $this->getMock('Drupal\Tests\search_api\TestComplexDataInterface');
+    $bar_foo_property = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
+    $bar_foo_property->method('getValue')
+      ->willReturn('value3 foo');
+    $bar_foo_property->method('getDataDefinition')
+      ->willReturn(new DataDefinition());
+    $bar_property = $this->getMock('Drupal\Tests\search_api\TestComplexDataInterface');
+    $bar_property->method('get')
+      ->willReturnMap(array(
+        array('foo', $bar_foo_property),
+      ));
+    $foobar_property = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
+    $foobar_property->method('getValue')
+      ->willReturn('wrong_value2 foo');
+    $foobar_property->method('getDataDefinition')
+      ->willReturn(new DataDefinition());
+    $object->method('get')
+      ->willReturnMap(array(
+        array('bar', $bar_property),
+        array('foobar', $foobar_property),
+      ));
+
+    $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'),
+      ));
+    $this->index->method('getPropertyDefinitions')
+      ->willReturnMap(array(
+        array(
+          NULL,
+          array(
+            'foo' => new ProcessorProperty(array(
+              'processor_id' => 'processor1',
+            )),
+          ),
+        ),
+        array(
+          'entity:test1',
+          array(
+            'bar' => new DataDefinition(),
+            'foobar' => new DataDefinition(),
+          ),
+        ),
+      ));
+    $processor_mock = $this->getMock('Drupal\search_api\Processor\ProcessorInterface');
+    $processor_mock->method('addFieldValues')
+      ->willReturnCallback(function (ItemInterface $item) {
+        foreach ($item->getFields(FALSE) as $field) {
+          if ($field->getCombinedPropertyPath() == 'foo') {
+            $field->setValues(array('value4 foo', 'value5 foo'));
+          }
+        }
+      });
+    $this->index->method('getProcessorsByStage')
+      ->willReturnMap(array(
+        array(
+          ProcessorInterface::STAGE_ADD_PROPERTIES,
+          array(
+            'aggregated_field' => $this->processor,
+            'processor1' => $processor_mock,
+          ),
+        ),
+      ));
+    $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')
+      ->willReturn('entity:test1');
+
+    $item = Utility::createItem($this->index, 'id', $datasource);
+    $item->setOriginalObject($object);
+    $field = $this->createField('field4', 'baz')
+      ->addValue('wrong_value1 foo');
+    $item->setField('field4', $field);
+    $field = $this->createField('field5', 'entity:test1/foobar')
+      ->addValue('value1 foo')
+      ->addValue('value2 foo');
+    $item->setField('field5', $field);
+
+    $this->processor->setConfiguration(array('excerpt' => FALSE));
+    /** @var \Drupal\search_api\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject $query */
+    $query = $this->getMock('Drupal\search_api\Query\QueryInterface');
+    $query->method('getKeys')
+      ->willReturn('foo');
+    $query->expects($this->once())
+      ->method('getProcessingLevel')
+      ->willReturn(QueryInterface::PROCESSING_FULL);
+    $results = new ResultSet($query);
+    $results->setResultCount(1)
+      ->setResultItems(array($item));
+    $this->processor->postprocessSearchResults($results);
+
+    $expected[0] = array(
+      'field1' => array(
+        'value3 <strong>foo</strong>',
+      ),
+      'field3' => array(
+        'value4 <strong>foo</strong>',
+        'value5 <strong>foo</strong>',
+      ),
+      'field5' => array(
+        'value1 <strong>foo</strong>',
+        'value2 <strong>foo</strong>',
+      ),
+    );
+    $highlighted_data = $results->getExtraData('highlighted_fields');
+    $this->assertEquals($expected, $highlighted_data);
+    $this->assertNotContains('wrong', print_r($highlighted_data, TRUE));
+  }
+
+  /**
+   * Creates a field object for testing.
+   *
+   * @param string $id
+   *   The field ID to set.
+   * @param string $combined_property_path
+   *   The combined property path of the field.
+   * @param bool $text
+   *   (optional) Whether the field should be a fulltext field or not.
+   *
+   * @return \Drupal\search_api\Item\FieldInterface
+   *   A field object.
+   */
+  protected function createField($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);
+    $field->setPropertyPath($property_path);
+    $field->setType($text ? 'text' : 'string');
+
+    return $field;
+  }
+
+  /**
    * Returns a long text to use for highlighting tests.
    *
    * @return string
