diff --git service.inc service.inc
index 62bddec..7dff73d 100644
--- service.inc
+++ service.inc
@@ -36,6 +36,11 @@ class SearchApiSolrService extends SearchApiAbstractService {
   );
 
   /**
+   * @var array
+   */
+  protected $fieldNames = array();
+
+  /**
    * Metadata describing fields on the Solr/Lucene index.
    * @see SearchApiSolrService::getFields().
    *
@@ -255,36 +260,51 @@ class SearchApiSolrService extends SearchApiAbstractService {
 
   /**
    * Create a list of all indexed field names mapped to their Solr field names.
-   * The special fields "search_api_id" and "search_api_relevance" are also
-   * included.
+   *
+   * The special fields "search_api_id", "search_api_relevance", and "id" are
+   * also included. Any Solr fields that exist on search results are mapped back
+   * to their local field names in the final result set.
+   *
+   * @see SearchApiSolrService::search()
    */
-  protected function getFieldNames(SearchApiIndex $index) {
-    $ret = array(
-      'search_api_id' => 'is_search_api_id',
-      'search_api_relevance' => 'score',
-    );
-    if (empty($index->options['fields'])) {
-      return $ret;
-    }
-    $fields = $index->options['fields'];
-    foreach ($fields as $key => $field) {
-      if (empty($field['indexed'])) {
-        continue;
-      }
-      $type = $field['type'];
-      $inner_type = search_api_extract_inner_type($type);
-      $pref = self::$type_prefixes[$inner_type];
-      if ($pref != 't') {
-        $pref .= $type == $inner_type ? 's' : 'm';
+  protected function getFieldNames(SearchApiIndex $index, $reset = FALSE) {
+    if (!isset($this->fieldNames[$index->machine_name]) || $reset) {
+      // This array maps "local property name" => "solr doc property name".
+      $ret = array(
+        'search_api_id' => 'is_search_api_id',
+        'search_api_relevance' => 'score',
+        'search_api_item_id' => 'item_id',
+      );
+
+      // Add the names of any "fields" configured on the index.
+      $fields = (isset($index->options['fields']) ? $index->options['fields'] : array());
+      foreach ($fields as $key => $field) {
+        // Since this determines which fields can be searched and filtered on,
+        // don't include fields that aren't indexed.
+        if (empty($field['indexed'])) {
+          continue;
+        }
+
+        // Generate a field name; this corresponds with naming conventions in
+        // our schema.xml
+        $type = $field['type'];
+        $inner_type = search_api_extract_inner_type($type);
+        $pref = isset(self::$type_prefixes[$inner_type]) ? self::$type_prefixes[$inner_type] : '';
+        if ($pref != 't') {
+          $pref .= $type == $inner_type ? 's' : 'm';
+        }
+        $name = $pref . '_' . $key;
+
+        $ret[$key] = $name;
       }
-      $name = $pref . '_' . $key;
-      $ret[$key] = $name;
-    }
 
-    // Let modules adjust the field mappings.
-    drupal_alter('search_api_solr_field_mapping', $index, $ret);
+      // Let modules adjust the field mappings.
+      drupal_alter('search_api_solr_field_mapping', $index, $ret);
+
+      $this->fieldNames[$index->machine_name] = $ret;
+    }
 
-    return $ret;
+    return $this->fieldNames[$index->machine_name];
   }
 
   /**
@@ -444,69 +464,11 @@ class SearchApiSolrService extends SearchApiAbstractService {
       }
 
       // Extract results
-      $results = array();
-      $results['result count'] = $response->response->numFound;
-      $results['results'] = array();
-      foreach ($response->response->docs as $doc) {
-        $doc->id = $doc->item_id;
-        unset($doc->item_id);
-        foreach ($doc as $k => $v) {
-          $result[$k] = $v;
-        }
-        $results['results'][$doc->id] = $result;
-      }
+      $results = $this->extractResults($query, $response);
 
       // Extract facets
-      if (isset($response->facet_counts->facet_fields)) {
-        $results['search_api_facets'] = array();
-        $facet_fields = $response->facet_counts->facet_fields;
-        foreach ($facets as $delta => $info) {
-          $field = $fields[$info['field']];
-          if ($field[0] == 's') {
-            $field = 'f_' . $field;
-          }
-          if (!empty($facet_fields->$field)) {
-            $min_count = $info['min_count'];
-            $terms = $facet_fields->$field;
-            if ($info['missing']) {
-              // We have to correctly incorporate the "_empty_" term.
-              // This will ensure that the term with the least results is dropped, if the limit would be exceeded.
-              $terms = (array) $terms;
-              arsort($terms);
-              if (count($terms) > $info['limit']) {
-                array_pop($terms);
-              }
-            }
-            elseif (isset($terms->_empty_)) {
-              $terms = clone $terms;
-              unset($terms->_empty_);
-            }
-            $type = isset($index->options['fields'][$info['field']]['type']) ? $index->options['fields'][$info['field']]['type'] : 'string';
-            foreach ($terms as $term => $count) {
-              if ($count >= $min_count) {
-                if ($type == 'boolean') {
-                  if ($term == 'true') {
-                    $term = 1;
-                  }
-                  elseif ($term == 'false') {
-                    $term = 0;
-                  }
-                }
-                elseif ($type == 'date') {
-                  $term = isset($term) ? strtotime($term) : NULL;
-                }
-                $term = $term === '_empty_' ? '!' : '"' . $term . '"';
-                $results['search_api_facets'][$delta][] = array(
-                  'filter' => $term,
-                  'count' => $count,
-                );
-              }
-            }
-            if (empty($results['search_api_facets'][$delta]) || count($results['search_api_facets'][$delta]) <= 1) {
-              unset($results['search_api_facets'][$delta]);
-            }
-          }
-        }
+      if ($facets = $this->extractFacets($query, $response)) {
+        $results['search_api_facets'] = $facets;
       }
 
       drupal_alter('search_api_solr_search_results', $results, $query, $response);
@@ -529,6 +491,134 @@ class SearchApiSolrService extends SearchApiAbstractService {
   }
 
   /**
+   * Extract results.
+   *
+   * @param Apache_Solr_Response $response
+   *   A response object from SolrPhpClient.
+   *
+   * @return array
+   *   An array with two keys:
+   *   - 'result count' contains an integer number of total results
+   *   - 'results' is an array of search results; keys are document ids, values
+   *      are arrays of property => value pairs
+   */
+  protected function extractResults(SearchApiQueryInterface $query, Apache_Solr_Response $response) {
+    $index = $query->getIndex();
+    $fields = $this->getFieldNames($index);
+
+    // Set up the results array.
+    $results = array();
+    $results['result count'] = $response->response->numFound;
+    $results['results'] = array();
+
+    // Add each search result to the results array.
+    foreach ($response->response->docs as $doc) {
+      // Blank result array.
+      $result = array(
+        'id' => NULL,
+        'score' => NULL,
+        'fields' => array(),
+      );
+
+      // Extract properties from the Solr document, translating from Solr to
+      // Search API property names. This reverses the mapping in
+      // SearchApiSolrService::getFieldNames().
+      foreach ($fields as $search_api_property => $solr_property) {
+        if (isset($doc->{$solr_property})) {
+          $result['fields'][$search_api_property] = $doc->{$solr_property};
+        }
+      }
+
+      // We can find the entity id and score in the special 'search_api_XXXX'
+      // properties. Mappings are provided for these properties in
+      // SearchApiSolrService::getFieldNames()
+      $result['id'] = $result['fields']['search_api_item_id'];
+      $result['score'] = $result['fields']['search_api_relevance'];
+
+      // Use the result's id as the array key. By default, 'id' is mapped to
+      // 'item_id' in SearchApiSolrService::getFieldNames().
+      if ($result['id']) {
+        $results['results'][$result['id']] = $result;
+      }
+    }
+
+    return $results;
+  }
+
+  /**
+   * Extract facets.
+   *
+   * @param Apache_Solr_Response $response
+   *   A response object from SolrPhpClient.
+   *
+   * @return array
+   *   An array describing facets that apply to the current results.
+   */
+  protected function extractFacets(SearchApiQueryInterface $query, Apache_Solr_Response $response) {
+    if (isset($response->facet_counts->facet_fields)) {
+      $index = $query->getIndex();
+      $fields = $this->getFieldNames($index);
+
+      $facets = array();
+      $facet_fields = $response->facet_counts->facet_fields;
+
+      // @TODO how is this related to $facet_fields above?
+      $extract_facets = $query->getOption('search_api_facets');
+      $extract_facets = ($extract_facets ? $extract_facets : array());
+
+      foreach ($extract_facets as $delta => $info) {
+        $field = $fields[$info['field']];
+        if ($field[0] == 's') {
+          $field = 'f_' . $field;
+        }
+        if (!empty($facet_fields->$field)) {
+          $min_count = $info['min_count'];
+          $terms = $facet_fields->$field;
+          if ($info['missing']) {
+            // We have to correctly incorporate the "_empty_" term.
+            // This will ensure that the term with the least results is dropped, if the limit would be exceeded.
+            $terms = (array) $terms;
+            arsort($terms);
+            if (count($terms) > $info['limit']) {
+              array_pop($terms);
+            }
+          }
+          elseif (isset($terms->_empty_)) {
+            $terms = clone $terms;
+            unset($terms->_empty_);
+          }
+          $type = isset($index->options['fields'][$info['field']]['type']) ? $index->options['fields'][$info['field']]['type'] : 'string';
+          foreach ($terms as $term => $count) {
+            if ($count >= $min_count) {
+              if ($type == 'boolean') {
+                if ($term == 'true') {
+                  $term = 1;
+                }
+                elseif ($term == 'false') {
+                  $term = 0;
+                }
+              }
+              elseif ($type == 'date') {
+                $term = isset($term) ? strtotime($term) : NULL;
+              }
+              $term = $term === '_empty_' ? '!' : '"' . $term . '"';
+              $facets[$delta][] = array(
+                'filter' => $term,
+                'count' => $count,
+              );
+            }
+          }
+          if (empty($facets[$delta]) || count($facets[$delta]) <= 1) {
+            unset($facets[$delta]);
+          }
+        }
+      }
+
+      return $facets;
+    }
+  }
+
+  /**
    * Flatten a keys array into a single search string.
    *
    * @param array $keys
