diff --git a/includes/callback_add_hierarchy.inc b/includes/callback_add_hierarchy.inc
new file mode 100644
index 0000000..e3e252a
--- /dev/null
+++ b/includes/callback_add_hierarchy.inc
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * Search API data alteration callback that adds an URL field for all items.
+ */
+class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
+
+  /**
+   * Cached value for the hierarchical field options.
+   *
+   * @var array
+   *
+   * @see getHierarchicalFields()
+   */
+  protected $field_options;
+
+  /**
+   * Enable this data alteration only if any hierarchical fields are available.
+   *
+   * @param SearchApiIndex $index
+   *   The index to check for.
+   *
+   * @return boolean
+   *   TRUE if the callback can run on the given index; FALSE otherwise.
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return (bool) $this->getHierarchicalFields();
+  }
+
+  /**
+   * Display a form for configuring this callback.
+   *
+   * @return array
+   *   A form array for configuring this callback, or FALSE if no configuration
+   *   is possible.
+   */
+  public function configurationForm() {
+    $options = $this->getHierarchicalFields();
+    $this->options += array('fields' => array());
+    $form['fields'] = array(
+      '#title' => t('Hierarchical fields'),
+      '#description' => t('Select the fields which should be supplemented with their ancestors. ' .
+          'Each field is listed along with its children of the same type. ' .
+          'When selecting several child properties of a field, all those properties will be recursively added to that field.'),
+      '#type' => 'select',
+      '#multiple' => TRUE,
+      '#size' => min(6, count($options, COUNT_RECURSIVE)),
+      '#options' => $options,
+      '#default_value' => $this->options['fields'],
+    );
+
+    return $form;
+  }
+
+  /**
+   * Alter items before indexing.
+   *
+   * Items which are removed from the array won't be indexed, but will be marked
+   * as clean for future indexing. This could for instance be used to implement
+   * some sort of access filter for security purposes (e.g., don't index
+   * unpublished nodes or comments).
+   *
+   * @param array $items
+   *   An array of entities to be altered, keyed by entity ID.
+   */
+  public function alterItems(array &$items) {
+    if (empty($this->options['fields'])) {
+      return array();
+    }
+    foreach ($items as $item) {
+      $wrapper = entity_metadata_wrapper($this->index->entity_type, $item);
+
+      $values = array();
+      foreach ($this->options['fields'] as $field) {
+        list($key, $prop) = explode(':', $field);
+        if (!isset($wrapper->$key)) {
+          continue;
+        }
+        $child = $wrapper->$key;
+
+        $values += array($key => array());
+        $this->extractHierarchy($child, $prop, $values[$key]);
+      }
+      foreach ($values as $key => $value) {
+        $item->$key = $value;
+      }
+    }
+  }
+
+  /**
+   * Declare the properties that are (or can be) added to items with this
+   * callback. If a property with this name already exists for an entity it
+   * will be overridden, so keep a clear namespace by prefixing the properties
+   * with the module name if this is not desired.
+   *
+   * @see hook_entity_property_info()
+   *
+   * @return array
+   *   Information about all additional properties, as specified by
+   *   hook_entity_property_info() (only the inner "properties" array).
+   */
+  public function propertyInfo() {
+    if (empty($this->options['fields'])) {
+      return array();
+    }
+
+    $ret = array();
+    $info['property info alter'] = '_search_api_wrapper_add_all_properties';
+    $info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
+    $wrapper = entity_metadata_wrapper($this->index->entity_type, NULL, $info);
+    foreach ($this->options['fields'] as $field) {
+      list($key, $prop) = explode(':', $field);
+      if (!isset($wrapper->$key)) {
+        continue;
+      }
+      $child = $wrapper->$key;
+      while (search_api_is_list_type($child->type())) {
+        $child = $child[0];
+      }
+      if (!isset($child->$prop)) {
+        continue;
+      }
+      if (!isset($ret[$key])) {
+        $ret[$key] = $child->info();
+        $type = search_api_extract_inner_type($ret[$key]['type']);
+        $ret[$key]['type'] = "list<$type>";
+        $ret[$key]['getter callback'] = 'entity_property_verbatim_get';
+        // The return value of info() has some additional internal values set,
+        // which we have to unset for the use here.
+        unset($ret[$key]['name'], $ret[$key]['parent'], $ret[$key]['langcode'], $ret[$key]['clear'],
+            $ret[$key]['property info alter'], $ret[$key]['property defaults']);
+      }
+      if (isset($ret[$key]['bundle'])) {
+        $info = $child->$prop->info();
+        if (empty($info['bundle']) || $ret[$key]['bundle'] != $info['bundle']) {
+          unset($ret[$key]['bundle']);
+        }
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * Helper method for finding all hierarchical fields of an index's type.
+   *
+   * @return array
+   *   An array containing all hierarchical fields of the index, structured as
+   *   an options array grouped by primary field.
+   */
+  protected function getHierarchicalFields() {
+    if (!isset($this->field_options)) {
+      $this->field_options = array();
+      $info['property info alter'] = '_search_api_wrapper_add_all_properties';
+      $info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
+      $wrapper = entity_metadata_wrapper($this->index->entity_type, NULL, $info);
+      // Only entities can be indexed in hierarchies, as other properties don't
+      // have IDs that we can extract and store.
+      $entity_info = entity_get_info();
+      foreach ($wrapper as $key1 => $child) {
+        while (search_api_is_list_type($child->type())) {
+          $child = $child[0];
+        }
+        $info = $child->info();
+        $type = $child->type();
+        if (empty($entity_info[$type])) {
+          continue;
+        }
+        foreach ($child as $key2 => $prop) {
+          if (search_api_extract_inner_type($prop->type()) == $type) {
+            $prop_info = $prop->info();
+            $this->field_options[$info['label']]["$key1:$key2"] = $prop_info['label'];
+          }
+        }
+      }
+    }
+    return $this->field_options;
+  }
+
+  /**
+   * Extracts a hierarchy from a metadata wrapper by modifying $values.
+   */
+  public function extractHierarchy(EntityMetadataWrapper $wrapper, $property, array &$values) {
+    if (search_api_is_list_type($wrapper->type())) {
+      foreach ($wrapper as $w) {
+        $this->extractHierarchy($w, $property, $values);
+      }
+      return;
+    }
+    $v = $wrapper->value(array('identifier' => TRUE));
+    if ($v && !isset($values[$v])) {
+      $values[$v] = $v;
+    }
+    if (!$v || !isset($wrapper->$property) || !$wrapper->$property->value()) {
+      return;
+    }
+    $this->extractHierarchy($wrapper->$property, $property, $values);
+  }
+
+}
diff --git a/includes/callback_bundle_filter.inc b/includes/callback_bundle_filter.inc
index f19deb4..9d30101 100644
--- a/includes/callback_bundle_filter.inc
+++ b/includes/callback_bundle_filter.inc
@@ -37,8 +37,8 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
           '#title' => t('Which items should be indexed?'),
           '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1,
           '#options' => array(
-            1 => t('All but those from one of the listed bundles.'),
-            0 => t('Only those from the listed bundles.'),
+            1 => t('All but those from one of the selected bundles'),
+            0 => t('Only those from the selected bundles'),
           ),
         ),
         'bundles' => array(
diff --git a/includes/index_entity.inc b/includes/index_entity.inc
index fd9ded5..3c9aa65 100644
--- a/includes/index_entity.inc
+++ b/includes/index_entity.inc
@@ -435,19 +435,14 @@ class SearchApiIndex extends Entity {
       return $ret;
     }
 
-    $wrappers = array();
+    $data = array();
     foreach ($items as $id => $item) {
-      $wrappers[$id] = $this->entityWrapper($item);
+      $data[$id] = search_api_extract_fields($this->entityWrapper($item), $fields);
     }
 
-    $items = array();
-    foreach ($wrappers as $id => $wrapper) {
-      $items[$id] = search_api_extract_fields($wrapper, $fields);
-    }
-
-    $this->preprocessIndexItems($items);
+    $this->preprocessIndexItems($data);
 
-    return array_merge($ret, $this->server()->indexItems($this, $items));
+    return array_merge($ret, $this->server()->indexItems($this, $data));
   }
 
   /**
@@ -508,7 +503,7 @@ class SearchApiIndex extends Entity {
       }
     }
     // Let fields added by data-alter callbacks override default fields.
-    $property_info['properties'] = $this->added_properties + $property_info['properties'];
+    $property_info['properties'] = array_merge($property_info['properties'], $this->added_properties);
 
     return $property_info;
   }
diff --git a/search_api.info b/search_api.info
index d501b7a..ebf75da 100644
--- a/search_api.info
+++ b/search_api.info
@@ -8,6 +8,7 @@ package = Search
 files[] = search_api.test
 files[] = includes/callback.inc
 files[] = includes/callback_add_aggregation.inc
+files[] = includes/callback_add_hierarchy.inc
 files[] = includes/callback_add_url.inc
 files[] = includes/callback_add_viewed_entity.inc
 files[] = includes/callback_bundle_filter.inc
diff --git a/search_api.module b/search_api.module
index 56fe445..8e84ac8 100644
--- a/search_api.module
+++ b/search_api.module
@@ -682,6 +682,13 @@ function search_api_entity_delete($entity, $type) {
  * Implements hook_search_api_alter_callback_info().
  */
 function search_api_search_api_alter_callback_info() {
+  $callbacks['search_api_alter_bundle_filter'] = array(
+    'name' => t('Bundle filter'),
+    'description' => t('Exclude items from indexing based on their bundle (content type, vocabulary, …).'),
+    'class' => 'SearchApiAlterBundleFilter',
+    // Filters should be executed first.
+    'weight' => -10,
+  );
   $callbacks['search_api_alter_add_url'] = array(
     'name' => t('URL field'),
     'description' => t("Adds the item's URL to the indexed data."),
@@ -697,10 +704,10 @@ function search_api_search_api_alter_callback_info() {
     'description' => t('Adds an additional field containing the whole HTML content of the entity when viewed.'),
     'class' => 'SearchApiAlterAddViewedEntity',
   );
-  $callbacks['search_api_alter_bundle_filter'] = array(
-    'name' => t('Bundle filter'),
-    'description' => t('Exclude items from indexing based on their bundle (content type, vocabulary, …).'),
-    'class' => 'SearchApiAlterBundleFilter',
+  $callbacks['search_api_alter_add_hierarchy'] = array(
+    'name' => t('Index hierarchy'),
+    'description' => t('Allows to index hierarchical fields along with all their ancestors.'),
+    'class' => 'SearchApiAlterAddHierarchy',
   );
 
   return $callbacks;
@@ -1353,6 +1360,10 @@ function search_api_extract_inner_type($type) {
  *   The $fields array with additional "value" and "original_type" keys set.
  */
 function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields, array $value_options = array()) {
+  $value_options += array(
+    // For entitites, extract the identifier instead of the whole object.
+    'identifier' => TRUE,
+  );
   // If $wrapper is a list of entities, we have to aggregate their field values.
   $wrapper_info = $wrapper->info();
   if (search_api_is_list_type($wrapper_info['type'])) {
@@ -1396,12 +1407,6 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields
         }
         $property_info = $wrapper->$field->info();
         $info['original_type'] = $property_info['type'];
-        // For entities, we extract the entity ID instead of the whole object.
-        $t = search_api_extract_inner_type($property_info['type']);
-        if (isset($entity_infos[$t])) {
-          // If no object is set, set this field to NULL.
-          $info['value'] = $info['value'] ? _search_api_extract_entity_value($wrapper->$field, search_api_is_text_type($info['type'])) : NULL;
-        }
       }
       catch (EntityMetadataWrapperException $e) {
         // This might happen for entity-typed properties that are NULL, e.g.,
@@ -1448,31 +1453,6 @@ function _search_api_add_option_values(&$value, array $options) {
 }
 
 /**
- * Helper method for extracting the ID (and possibly label) of an entity-valued field.
- */
-function _search_api_extract_entity_value(EntityMetadataWrapper $wrapper, $fulltext = FALSE) {
-  $v = $wrapper->value();
-  if (is_array($v)) {
-    $ret = array();
-    foreach ($wrapper as $item) {
-      $values = _search_api_extract_entity_value($item, $fulltext);
-      if ($values) {
-        $ret[] = $values;
-      }
-    }
-    return $ret;
-  }
-  if ($v) {
-    $ret = $wrapper->getIdentifier();
-    if ($fulltext && ($label = $wrapper->label())) {
-      $ret .= ' ' . $label;
-    }
-    return $ret;
-  }
-  return NULL;
-}
-
-/**
  * Returns a list of all (enabled) search servers.
  *
  * @deprecated
