diff --git a/INSTALL.txt b/INSTALL.txt
index 18cb6c7..96b1caa 100644
--- a/INSTALL.txt
+++ b/INSTALL.txt
@@ -1,39 +1,3 @@
-Prerequisite:
-- Java 6 or higher
-
-Steps necessary:
-- Download Solr PHP client
-- Setting up Solr
-
-Download Solr PHP client
-------------------------
-
-This module uses an external PHP library for communicating with Solr servers. Go
-to [1] and download version r60 of this library. (The older version r22 is also
-still supported, but this might change in the future.)
-
-[1] http://code.google.com/p/solr-php-client/downloads/list
-
-Afterwards, unpack this archive to Drupal's libraries folder, so the directory
-tree looks like this:
-
-DRUPAL_ROOT/sites/all/libraries/
-  |- SolrPhpClient
-     |- Apache/
-     |- ChangeLog
-     ...
-
-The library should then be found by the module.
-
-Note: If you have the Libraries API [2] module installed, you can also place the
-library into any other directory recognized by the Libraries API, e.g.
-(depending on the module version):
-- DRUPAL_ROOT/libraries
-- DRUPAL_ROOT/profiles/PROFILE/libraries
-- DRUPAL_ROOT/sites/CONF_DIR/libraries
-
-[2] http://drupal.org/project/libraries
-
 Setting up Solr
 ---------------
 
@@ -45,6 +9,9 @@ module's project page [3]. Otherwise, please follow the instructions below.
 
 [3] http://drupal.org/project/search_api_solr
 
+As a pre-requisite for running your own Solr server, you'll need Java 6 or
+higher.
+
 Download the latest version of Solr 3.x from [4] and unpack the archive
 somewhere outside of your web server's document tree.
 
@@ -59,6 +26,10 @@ usually suffices. In any case, you can use it for developing andd testing. The
 following instructions will assume you are using the example application,
 otherwise you should be able to substitute the corresponding paths.
 
+CAUTION! For production sites, it is vital that you somehow prevent outside
+access to the Solr server. Otherwise, attackers could read, corrupt or delete
+all your indexed data. Using the example server WON'T prevent this by default.
+
 Before starting the Solr server you will have to make sure it uses the proper
 configuration files. These are located in the solr-conf/ directory in this
 module, in a sub-directory according to the Solr version you are using. Copy all
diff --git a/includes/document.inc b/includes/document.inc
new file mode 100644
index 0000000..010744f
--- /dev/null
+++ b/includes/document.inc
@@ -0,0 +1,435 @@
+<?php
+/**
+ * Copyright (c) 2007-2009, Conduit Internet Technologies, Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  - Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Conduit Internet Technologies, Inc. nor the names of
+ *    its contributors may be used to endorse or promote products derived from
+ *    this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @copyright Copyright 2007-2009 Conduit Internet Technologies, Inc. (http://conduit-it.com)
+ * @license New BSD (http://solr-php-client.googlecode.com/svn/trunk/COPYING)
+ * @version $Id: Document.php 15 2009-08-04 17:53:08Z donovan.jimenez $
+ *
+ * @package Apache
+ * @subpackage Solr
+ * @author Donovan Jimenez <djimenez@conduit-it.com>
+ */
+
+/**
+ * Additional code Copyright (c) 2011 by Peter Wolanin, and
+ * additional contributors.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program as the file LICENSE.txt; if not, please see
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+ */
+
+/**
+ * Holds Key / Value pairs that represent a Solr Document along with any
+ * associated boost values. Field values can be accessed by direct dereferencing
+ * such as:
+ *
+ * @code
+ *   $document->title = 'Something';
+ *   echo $document->title;
+ * @endcode
+ *
+ * Additionally, the field values can be iterated with foreach:
+ *
+ * @code
+ *   foreach ($document as $fieldName => $fieldValue) {
+ *     // ...
+ *   }
+ * @endcode
+ */
+class SearchApiSolrDocument implements IteratorAggregate {
+
+  /**
+   * Document boost value.
+   *
+   * @var float|false
+   */
+  protected $documentBoost = FALSE;
+
+  /**
+   * Document field values, indexed by name.
+   *
+   * @var array
+   */
+  protected $fields = array();
+
+  /**
+   * Document field boost values, indexed by name.
+   *
+   * @var array
+   */
+  protected $fieldBoosts = array();
+
+  /**
+   * Clears all boosts and fields from this document.
+   */
+  public function clear() {
+    $this->documentBoost = FALSE;
+
+    $this->fields = array();
+    $this->fieldBoosts = array();
+  }
+
+  /**
+   * Gets the current document boost.
+   *
+   * @return float|false
+   *   The current document boost, or FALSE if none is set.
+   */
+  public function getBoost() {
+    return $this->documentBoost;
+  }
+
+  /**
+   * Sets the document boost factor.
+   *
+   * @param float|false $boost
+   *   FALSE for default boost, or a positive number for setting a document
+   *   boost.
+   */
+  public function setBoost($boost) {
+    $boost = (float) $boost;
+
+    if ($boost > 0.0) {
+      $this->documentBoost = $boost;
+    }
+    else {
+      $this->documentBoost = FALSE;
+    }
+  }
+
+  /**
+   * Adds a value to a multi-valued field
+   *
+   * NOTE: the solr XML format allows you to specify boosts PER value even
+   * though the underlying Lucene implementation only allows a boost per field.
+   * To remedy this, the final field boost value will be the product of all
+   * specified boosts on field values - this is similar to SolrJ's
+   * functionality.
+   *
+   * @code
+   *   $doc = new ApacheSolrDocument();
+   *   $doc->addField('foo', 'bar', 2.0);
+   *   $doc->addField('foo', 'baz', 3.0);
+   *   // Resultant field boost will be 6!
+   *   echo $doc->getFieldBoost('foo');
+   * @endcode
+   *
+   * @param string $key
+   *   The name of the field.
+   * @param $value
+   *   The value to add for the field.
+   * @param float|false $boost
+   *   FALSE for default boost, or a positive number for setting a field boost.
+   */
+  public function addField($key, $value, $boost = FALSE) {
+    if (!isset($this->fields[$key])) {
+      // create holding array if this is the first value
+      $this->fields[$key] = array();
+    }
+    else if (!is_array($this->fields[$key])) {
+      // move existing value into array if it is not already an array
+      $this->fields[$key] = array($this->fields[$key]);
+    }
+
+    if ($this->getFieldBoost($key) === FALSE) {
+      // boost not already set, set it now
+      $this->setFieldBoost($key, $boost);
+    }
+    else if ((float) $boost > 0.0) {
+      // multiply passed boost with current field boost - similar to SolrJ implementation
+      $this->fieldBoosts[$key] *= (float) $boost;
+    }
+
+    // add value to array
+    $this->fields[$key][] = $value;
+  }
+
+  /**
+   * Gets information about a field stored in Solr.
+   *
+   * @param string $key
+   *   The name of the field.
+   *
+   * @return array|false
+   *   An associative array of info if the field exists, FALSE otherwise.
+   */
+  public function getField($key) {
+    if (isset($this->fields[$key])) {
+      return array(
+        'name' => $key,
+        'value' => $this->fields[$key],
+        'boost' => $this->getFieldBoost($key)
+      );
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Sets a field value.
+   *
+   * Multi-valued fields should be set as arrays or via the addField()
+   * function which will automatically make sure the field is an array.
+   *
+   * @param string $key
+   *   The name of the field.
+   * @param string|array $value
+   *   The value to set for the field.
+   * @param float|false $boost
+   *   FALSE for default boost, or a positive number for setting a field boost.
+   */
+  public function setField($key, $value, $boost = FALSE) {
+    $this->fields[$key] = $value;
+    $this->setFieldBoost($key, $boost);
+  }
+
+  /**
+   * Gets the currently set field boost for a document field.
+   *
+   * @param string $key
+   *   The name of the field.
+   *
+   * @return float|false
+   *   The currently set field boost, or FALSE if none was set.
+   */
+  public function getFieldBoost($key) {
+    return isset($this->fieldBoosts[$key]) ? $this->fieldBoosts[$key] : FALSE;
+  }
+
+  /**
+   * Sets the field boost for a document field.
+   *
+   * @param string $key
+   *   The name of the field.
+   * @param float|false $boost
+   *   FALSE for default boost, or a positive number for setting a field boost.
+   */
+  public function setFieldBoost($key, $boost) {
+    $boost = (float) $boost;
+
+    if ($boost > 0.0) {
+      $this->fieldBoosts[$key] = $boost;
+    }
+    else {
+      $this->fieldBoosts[$key] = FALSE;
+    }
+  }
+
+  /**
+   * Returns all current field boosts, indexed by field name.
+   *
+   * @return array
+   *   An associative array in the format $field_name => $field_boost.
+   */
+  public function getFieldBoosts() {
+    return $this->fieldBoosts;
+  }
+
+  /**
+   * Gets the names of all fields in this document.
+   *
+   * @return array
+   *   The names of all fields in this document.
+   */
+  public function getFieldNames() {
+    return array_keys($this->fields);
+  }
+
+  /**
+   * Gets the values of all fields in this document.
+   *
+   * @return array
+   *   The values of all fields in this document.
+   */
+  public function getFieldValues() {
+    return array_values($this->fields);
+  }
+
+  /**
+   * Implements IteratorAggregate::getIterator().
+   *
+   * Implementing the IteratorAggregate interface allows the following usage:
+   * @code
+   *   foreach ($document as $key => $value) {
+   *     // ...
+   *   }
+   * @endcode
+   *
+   * @return Traversable
+   *   An iterator over this document's fields.
+   */
+  public function getIterator() {
+    $arrayObject = new ArrayObject($this->fields);
+
+    return $arrayObject->getIterator();
+  }
+
+  /**
+   * Magic getter for field values.
+   *
+   * @param string $key
+   *   The name of the field.
+   *
+   * @return string|array|null
+   *   The value that was set for the field.
+   */
+  public function __get($key) {
+    return $this->fields[$key];
+  }
+
+  /**
+   * Magic setter for field values.
+   *
+   * Multi-valued fields should be set as arrays or via the addField() function
+   * which will automatically make sure the field is an array.
+   *
+   * @param string $key
+   *   The name of the field.
+   * @param string|array $value
+   *   The value to set for the field.
+   */
+  public function __set($key, $value) {
+    $this->setField($key, $value);
+  }
+
+  /**
+   * Magic isset for fields values.
+   *
+   * Do not call directly. Allows the following usage:
+   * @code
+   *   isset($document->some_field);
+   * @endcode
+   *
+   * @param string $key
+   *   The name of the field.
+   *
+   * @return bool
+   *   Whether the given key is set in this document.
+   */
+  public function __isset($key) {
+    return isset($this->fields[$key]);
+  }
+
+  /**
+   * Magic unset for field values.
+   *
+   * Do not call directly. Allows the following usage:
+   * @code
+   *   unset($document->some_field);
+   * @endcode
+   *
+   * @param string $key
+   *   The name of the field.
+   */
+  public function __unset($key) {
+    unset($this->fields[$key]);
+    unset($this->fieldBoosts[$key]);
+  }
+
+  /**
+   * Create an XML fragment from this document.
+   *
+   * This string can then be used inside a Solr add call.
+   *
+   * @return string
+   *   An XML formatted string for this document.
+   */
+  public function toXml() {
+    $xml = '<doc';
+
+    if ($this->documentBoost !== FALSE) {
+      $xml .= ' boost="' . $this->documentBoost . '"';
+    }
+
+    $xml .= '>';
+
+    foreach ($this->fields as $key => $value) {
+      $fieldBoost = $this->getFieldBoost($key);
+      $key = htmlspecialchars($key, ENT_COMPAT, 'UTF-8');
+
+      if (is_array($value)) {
+        foreach ($value as $multivalue) {
+          $xml .= '<field name="' . $key . '"';
+
+          if ($fieldBoost !== FALSE) {
+            $xml .= ' boost="' . $fieldBoost . '"';
+
+            // Only set the boost for the first field in the set.
+            $fieldBoost = FALSE;
+          }
+
+          $xml .= '>' . htmlspecialchars($multivalue, ENT_NOQUOTES, 'UTF-8') . '</field>';
+        }
+      }
+      else {
+        $xml .= '<field name="' . $key . '"';
+
+        if ($fieldBoost !== FALSE) {
+          $xml .= ' boost="' . $fieldBoost . '"';
+        }
+
+        $xml .= '>' . htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8') . '</field>';
+      }
+    }
+
+    $xml .= '</doc>';
+
+    // Remove any control characters to avoid Solr XML parser exception.
+    return self::stripCtrlChars($xml);
+  }
+
+  /**
+   * Sanitizes XML for sending to Solr.
+   *
+   * Replaces control (non-printable) characters that are invalid to Solr's XML
+   * parser with a space.
+   *
+   * @param string $string
+   *   The string to sanitize.
+   *
+   * @return string
+   *   A string safe for including in a Solr request.
+   */
+  public static function stripCtrlChars($string) {
+    // See:  http://w3.org/International/questions/qa-forms-utf-8.html
+    // Printable utf-8 does not include any of these chars below x7F
+    return preg_replace('@[\x00-\x08\x0B\x0C\x0E-\x1F]@', ' ', $string);
+  }
+}
\ No newline at end of file
diff --git a/includes/service.inc b/includes/service.inc
new file mode 100644
index 0000000..cf4e795
--- /dev/null
+++ b/includes/service.inc
@@ -0,0 +1,1661 @@
+<?php
+
+/**
+ * Search service class using Solr server.
+ */
+class SearchApiSolrService extends SearchApiAbstractService {
+
+  /**
+   * The date format that Solr uses, in PHP date() syntax.
+   */
+  const SOLR_DATE_FORMAT = 'Y-m-d\TH:i:s\Z';
+
+  /**
+   * The connection class used by this service.
+   *
+   * Must implement SearchApiSolrConnectionInterface.
+   *
+   * @var string
+   */
+  protected $connection_class = 'SearchApiSolrConnection';
+
+  /**
+   * A connection to the Solr server.
+   *
+   * @var SearchApiSolrConnectionInterface
+   */
+  protected $solr;
+
+  /**
+   * An array of all recognized types.
+   *
+   * Maps the type names to the prefixes used for identifying them in the Solr
+   * schema.
+   *
+   * @var array
+   */
+  protected static $type_prefixes = array(
+    'text' => 'tm',
+    'tokens' => 'tm',
+    'string' => 's',
+    'integer' => 'i',
+    'decimal' => 'f',
+    'date' => 'd',
+    'duration' => 'i',
+    'boolean' => 'b',
+    'uri' => 's',
+    'location' => 'loc',
+    'geohash' => 'geohash',
+  );
+
+  /**
+   * Static cache for getFieldNames().
+   *
+   * @var array
+   */
+  protected $fieldNames = array();
+
+  /**
+   * Metadata describing fields on the Solr/Lucene index.
+   *
+   * @see SearchApiSolrService::getFields().
+   *
+   * @var array
+   */
+  protected $fields;
+
+  /**
+   * Saves whether a commit operation was already scheduled for this server.
+   *
+   * @var bool
+   */
+  protected $commitScheduled = FALSE;
+
+  /**
+   * Request handler to use for this search query.
+   *
+   * @var string
+   */
+  protected $request_handler = NULL;
+
+  /**
+   * Overrides SearchApiAbstractService::configurationForm().
+   */
+  public function configurationForm(array $form, array &$form_state) {
+    if ($this->options) {
+      // Editing this server
+      $url = 'http://' . $this->options['host'] . ':' . $this->options['port'] . $this->options['path'];
+      $form['server_description'] = array(
+        '#type' => 'item',
+        '#title' => t('Solr server URI'),
+        '#description' => l($url, $url),
+      );
+    }
+
+    $options = $this->options + array(
+      'host' => 'localhost',
+      'port' => '8983',
+      'path' => '/solr',
+      'http_user' => '',
+      'http_pass' => '',
+      'excerpt' => FALSE,
+      'retrieve_data' => FALSE,
+      'highlight_data' => FALSE,
+      'http_method' => 'POST',
+      'autocorrect_spell' => TRUE,
+      'autocorrect_suggest_words' => TRUE,
+    );
+
+    $form['host'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Solr host'),
+      '#description' => t('The host name or IP of your Solr server, e.g. <code>localhost</code> or <code>www.example.com</code>.'),
+      '#default_value' => $options['host'],
+      '#required' => TRUE,
+    );
+    $form['port'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Solr port'),
+      '#description' => t('The Jetty example server is at port 8983, while Tomcat uses 8080 by default.'),
+      '#default_value' => $options['port'],
+      '#required' => TRUE,
+    );
+    $form['path'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Solr path'),
+      '#description' => t('The path that identifies the Solr instance to use on the server.'),
+      '#default_value' => $options['path'],
+    );
+
+    $form['http'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Basic HTTP authentication'),
+      '#description' => t('If your Solr server is protected by basic HTTP authentication, enter the login data here.'),
+      '#collapsible' => TRUE,
+      '#collapsed' => empty($options['http_user']),
+    );
+    $form['http']['http_user'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Username'),
+      '#default_value' => $options['http_user'],
+    );
+    $form['http']['http_pass'] = array(
+      '#type' => 'password',
+      '#title' => t('Password'),
+      '#default_value' => $options['http_pass'],
+    );
+
+    $form['advanced'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Advanced'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+    );
+    $form['advanced']['excerpt'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Return an excerpt for all results'),
+      '#description' => t("If search keywords are given, use Solr's capabilities to create a highlighted search excerpt for each result. " .
+          'Whether the excerpts will actually be displayed depends on the settings of the search, though.'),
+      '#default_value' => $options['excerpt'],
+    );
+    $form['advanced']['retrieve_data'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Retrieve result data from Solr'),
+      '#description' => t('When checked, result data will be retrieved directly from the Solr server. ' .
+          'This might make item loads unnecessary. Only indexed fields can be retrieved. ' .
+          'Note also that the returned field data might not always be correct, due to preprocessing and caching issues.'),
+      '#default_value' => $options['retrieve_data'],
+    );
+    $form['advanced']['highlight_data'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Highlight retrieved data'),
+      '#description' => t('When retrieving result data from the Solr server, try to highlight the search terms in the returned fulltext fields.'),
+      '#default_value' => $options['highlight_data'],
+    );
+    // Highlighting retrieved data only makes sense when we retrieve data.
+    // (Actually, internally it doesn't really matter. However, from a user's
+    // perspective, having to check both probably makes sense.)
+    $form['advanced']['highlight_data']['#states']['invisible']
+        [':input[name="options[form][advanced][retrieve_data]"]']['checked'] = FALSE;
+
+    $form['advanced']['http_method'] = array(
+      '#type' => 'select',
+      '#title' => t('HTTP method'),
+      '#description' => t('The HTTP method to use for sending queries. Usually, POST will work fine in all cases.'),
+      '#default_value' => $options['http_method'],
+      '#options' => array(
+        'POST' => 'POST',
+        'GET' => 'GET',
+      ),
+    );
+
+    if (module_exists('search_api_autocomplete')) {
+      $form['advanced']['autocomplete'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Autocomplete'),
+        '#collapsible' => TRUE,
+        '#collapsed' => TRUE,
+      );
+      $form['advanced']['autocomplete']['autocorrect_spell'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Use spellcheck for autocomplete suggestions'),
+        '#description' => t('If activated, spellcheck suggestions ("Did you mean") will be included in the autocomplete suggestions. Since the used dictionary contains words from all indexes, this might lead to leaking of sensitive data, depending on your setup.'),
+        '#default_value' => $options['autocorrect_spell'],
+      );
+      $form['advanced']['autocomplete']['autocorrect_suggest_words'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Suggest additional words'),
+        '#description' => t('If activated and the user enters a complete word, Solr will suggest additional words the user wants to search, which are often found (not searched!) together. This has been known to lead to strange results in some configurations – if you see inappropriate additional-word suggestions, you might want to deactivate this option.'),
+        '#default_value' => $options['autocorrect_suggest_words'],
+      );
+    }
+
+    return $form;
+  }
+
+  /**
+   * Overrides SearchApiAbstractService::configurationFormValidate().
+   */
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    if (isset($values['port']) && (!is_numeric($values['port']) || $values['port'] < 0 || $values['port'] > 65535)) {
+      form_error($form['port'], t('The port has to be an integer between 0 and 65535.'));
+    }
+  }
+
+  /**
+   * Overrides SearchApiAbstractService::configurationFormSubmit().
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    // Since the form is nested into another, we can't simply use #parents for
+    // doing this array restructuring magic. (At least not without creating an
+    // unnecessary dependency on internal implementation.)
+    $values += $values['http'];
+    $values += $values['advanced'];
+    $values += !empty($values['autocomplete']) ? $values['autocomplete'] : array();
+    unset($values['http'], $values['advanced'], $values['autocomplete']);
+    // Highlighting retrieved data only makes sense when we retrieve data.
+    $values['highlight_data'] &= $values['retrieve_data'];
+
+    parent::configurationFormSubmit($form, $values, $form_state);
+  }
+
+  /**
+   * Overrides SearchApiAbstractService::supportsFeature().
+   */
+  public function supportsFeature($feature) {
+    $supported = drupal_map_assoc(array(
+      'search_api_autocomplete',
+      'search_api_facets',
+      'search_api_facets_operator_or',
+      'search_api_mlt',
+      'search_api_multi',
+      'search_api_spellcheck',
+      'search_api_data_type_location',
+      'search_api_data_type_geohash',
+    ));
+    return isset($supported[$feature]);
+  }
+
+  /**
+   * Overrides SearchApiAbstractService::viewSettings().
+   */
+  public function viewSettings() {
+    $output = '';
+    $options = $this->options;
+
+    $url = 'http://' . $options['host'] . ':' . $options['port'] . $options['path'];
+    $output .= "<dl>\n  <dt>";
+    $output .= t('Solr server URI');
+    $output .= "</dt>\n  <dd>";
+    $output .= l($url, $url);
+    $output .= '</dd>';
+    if ($options['http_user']) {
+      $output .= "\n  <dt>";
+      $output .= t('Basic HTTP authentication');
+      $output .= "</dt>\n  <dd>";
+      $output .= t('Username: @user', array('@user' => $options['http_user']));
+      $output .= "</dd>\n  <dd>";
+      $output .= t('Password: @pass', array('@pass' => str_repeat('*', strlen($options['http_pass']))));
+      $output .= '</dd>';
+    }
+    $output .= "\n</dl>";
+
+    return $output;
+  }
+
+  /**
+   * Create a connection to the Solr server as configured in $this->options.
+   */
+  protected function connect() {
+    if (!$this->solr) {
+      if (!class_exists($this->connection_class)) {
+        throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $this->connection_class)));
+      }
+      $options = $this->options + array('server' => $this->server->machine_name);
+      $this->solr = new $this->connection_class($options);
+      if (!($this->solr instanceof SearchApiSolrConnectionInterface)) {
+        $this->solr = NULL;
+        throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $this->connection_class)));
+      }
+    }
+  }
+
+  /**
+   * Overrides SearchApiAbstractService::addIndex().
+   */
+  public function addIndex(SearchApiIndex $index) {
+    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+      views_invalidate_cache();
+    }
+  }
+
+  /**
+   * Overrides SearchApiAbstractService::fieldsUpdated().
+   */
+  public function fieldsUpdated(SearchApiIndex $index) {
+    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+      views_invalidate_cache();
+    }
+    return TRUE;
+  }
+
+  /**
+   * Overrides SearchApiAbstractService::removeIndex().
+   */
+  public function removeIndex($index) {
+    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+      views_invalidate_cache();
+    }
+    $id = is_object($index) ? $index->machine_name : $index;
+    // Only delete the index's data if the index isn't read-only.
+    if (!is_object($index) || empty($index->read_only)) {
+      try {
+        $this->connect();
+        $this->solr->deleteByQuery("index_id:" . $id);
+      }
+      catch (Exception $e) {
+        watchdog_exception('search_api_solr', $e, "%type while deleting an index's data: !message in %function (line %line of %file).");
+      }
+    }
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::indexItems().
+   */
+  public function indexItems(SearchApiIndex $index, array $items) {
+    $documents = array();
+    $ret = array();
+    $index_id = $index->machine_name;
+    $fields = $this->getFieldNames($index);
+
+    foreach ($items as $id => $item) {
+      try {
+        $doc = new SearchApiSolrDocument();
+        $doc->setField('id', $this->createId($index_id, $id));
+        $doc->setField('index_id', $index_id);
+        $doc->setField('item_id', $id);
+
+        foreach ($item as $key => $field) {
+          if (!isset($fields[$key])) {
+            throw new SearchApiException(t('Unknown field @field.', array('@field' => $key)));
+          }
+          $this->addIndexField($doc, $fields[$key], $field['value'], $field['type']);
+        }
+
+        $documents[] = $doc;
+        $ret[] = $id;
+      }
+      catch (Exception $e) {
+        watchdog_exception('search_api_solr', $e, "%type while indexing @type with ID @id: !message in %function (line %line of %file).", array('@type' => $index->item_type, '@id' => $id), WATCHDOG_WARNING);
+      }
+    }
+
+    if (!$documents) {
+      return array();
+    }
+    try {
+      $this->connect();
+      $this->solr->addDocuments($documents);
+      if (!empty($index->options['index_directly'])) {
+        $this->scheduleCommit();
+      }
+      return $ret;
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api_solr', $e, "%type while indexing: !message in %function (line %line of %file).");
+    }
+    return array();
+  }
+
+  /**
+   * Creates an ID used as the unique identifier at the Solr server.
+   *
+   * This has to consist of both index and item ID.
+   */
+  protected function createId($index_id, $item_id) {
+    return "$index_id-$item_id";
+  }
+
+  /**
+   * Create a list of all indexed field names mapped to their Solr field names.
+   *
+   * 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()
+   */
+  public 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' => 'ss_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) {
+        // Generate a field name; this corresponds with naming conventions in
+        // our schema.xml
+        $type = $field['type'];
+
+        // Use the real type of the field if the server supports this type.
+        if (isset($field['real_type'])) {
+          $custom_type = search_api_extract_inner_type($field['real_type']);
+          if ($this->supportsFeature('search_api_data_type_' . $custom_type)) {
+            $type = $field['real_type'];
+          }
+        }
+
+        $inner_type = search_api_extract_inner_type($type);
+        $pref = isset(self::$type_prefixes[$inner_type]) ? self::$type_prefixes[$inner_type] : '';
+        if ($pref != 'tm') {
+          $pref .= $type == $inner_type ? 's' : 'm';
+        }
+        $name = $pref . '_' . $key;
+
+        $ret[$key] = $name;
+      }
+
+      // Let modules adjust the field mappings.
+      drupal_alter('search_api_solr_field_mapping', $index, $ret);
+
+      $this->fieldNames[$index->machine_name] = $ret;
+    }
+
+    return $this->fieldNames[$index->machine_name];
+  }
+
+  /**
+   * Helper method for indexing.
+   *
+   * Adds $value with field name $key to the document $doc. The format of $value
+   * is the same as specified in SearchApiServiceInterface::indexItems().
+   */
+  protected function addIndexField(SearchApiSolrDocument $doc, $key, $value, $type, $multi_valued = FALSE) {
+    // Don't index empty values (i.e., when field is missing).
+    if (!isset($value)) {
+      return;
+    }
+    if (search_api_is_list_type($type)) {
+      $type = substr($type, 5, -1);
+      foreach ($value as $v) {
+        $this->addIndexField($doc, $key, $v, $type, TRUE);
+      }
+      return;
+    }
+    switch ($type) {
+      case 'tokens':
+        foreach ($value as $v) {
+          $doc->addField($key, $v['value']);
+        }
+        return;
+      case 'boolean':
+        $value = $value ? 'true' : 'false';
+        break;
+      case 'date':
+        $value = is_numeric($value) ? (int) $value : strtotime($value);
+        if ($value === FALSE) {
+          return;
+        }
+        $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC');
+        break;
+      case 'integer':
+        $value = (int) $value;
+        break;
+      case 'decimal':
+        $value = (float) $value;
+        break;
+    }
+    if ($multi_valued) {
+      $doc->addField($key, $value);
+    }
+    else {
+      $doc->setField($key, $value);
+    }
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::deleteItems().
+   *
+   * This method has a custom, Solr-specific extension:
+   *
+   * If $ids is a string other than "all", it is treated as a Solr query. All
+   * items matching that Solr query are then deleted. If $index is additionally
+   * specified, then only those items also lying on that index will be deleted.
+   *
+   * It is up to the caller to ensure $ids is a valid query when the method is
+   * called in this fashion.
+   */
+  public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
+    try {
+      $this->connect();
+      if ($index) {
+        $index_id = $index->machine_name;
+        if (is_array($ids)) {
+          $solr_ids = array();
+          foreach ($ids as $id) {
+            $solr_ids[] = $this->createId($index_id, $id);
+          }
+          $this->solr->deleteByMultipleIds($solr_ids);
+        }
+        elseif ($ids == 'all') {
+          $this->solr->deleteByQuery("index_id:" . $index_id);
+        }
+        else {
+          $this->solr->deleteByQuery("index_id:" . $index_id . ' (' . $ids . ')');
+        }
+      }
+      else {
+        $q = $ids == 'all' ? '*:*' : $ids;
+        $this->solr->deleteByQuery($q);
+      }
+      $this->scheduleCommit();
+    }
+    catch(SearchApiException $e) {
+      watchdog_exception('search_api_solr', $e, '%type while deleting items from server @server: !message in %function (line %line of %file).', array('@server' => $this->server->name));
+    }
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::search().
+   */
+  public function search(SearchApiQueryInterface $query) {
+    $time_method_called = microtime(TRUE);
+    // Reset request handler.
+    $this->request_handler = NULL;
+    // Get field information.
+    $index = $query->getIndex();
+    $fields = $this->getFieldNames($index);
+
+    // Extract keys.
+    $keys = $query->getKeys();
+    if (is_array($keys)) {
+      $keys = $this->flattenKeys($keys);
+    }
+
+    // Set searched fields.
+    $options = $query->getOptions();
+    $search_fields = $query->getFields();
+    // Get the index fields to be able to retrieve boosts.
+    $index_fields = $index->getFields();
+    $qf = array();
+    foreach ($search_fields as $f) {
+      $boost = '';
+      $boost = isset($index_fields[$f]['boost']) ? '^' . $index_fields[$f]['boost'] : '';
+      $qf[] = $fields[$f] . $boost;
+    }
+
+    // Extract filters.
+    $filter = $query->getFilter();
+    $fq = $this->createFilterQueries($filter, $fields, $index->options['fields']);
+    $fq[] = 'index_id:' . $index->machine_name;
+
+    // Extract sort.
+    $sort = array();
+    foreach ($query->getSort() as $f => $order) {
+      $f = $fields[$f];
+      if (substr($f, 0, 3) == 'ss_') {
+        $f = 'sort_' . substr($f, 3);
+      }
+      $order = strtolower($order);
+      $sort[] = "$f $order";
+    }
+
+    // Get facet fields.
+    $facets = $query->getOption('search_api_facets', array());
+    $facet_params = $this->getFacetParams($facets, $fields, $fq);
+
+    // Handle highlighting.
+    $highlight_params = $this->getHighlightParams($query);
+
+    // Handle More Like This query.
+    $mlt = $query->getOption('search_api_mlt');
+    if ($mlt) {
+      $mlt_params['qt'] = 'mlt';
+      // The fields to look for similarities in.
+      $mlt_fl = array();
+      foreach($mlt['fields'] as $f) {
+        $mlt_fl[] = $fields[$f];
+        // For non-text fields, set minimum word length to 0.
+        if (isset($index->options['fields'][$f]['type']) && !search_api_is_text_type($index->options['fields'][$f]['type'])) {
+          $mlt_params['f.' . $fields[$f] . '.mlt.minwl'] = 0;
+        }
+      }
+      $mlt_params['mlt.fl'] = implode(',', $mlt_fl);
+      $id = $this->createId($index->machine_name, $mlt['id']);
+      $id = call_user_func(array($this->connection_class, 'phrase'), $id);
+      $keys = 'id:' . $id;
+    }
+
+    // Set defaults.
+    if (!$keys) {
+      $keys = NULL;
+    }
+
+    // Collect parameters.
+    $params = array(
+      'fl' => 'item_id,score',
+      'qf' => $qf,
+      'fq' => $fq,
+    );
+    if (isset($options['offset'])) {
+      $params['start'] = $options['offset'];
+    }
+    if (isset($options['limit'])) {
+      $params['rows'] = $options['limit'];
+    }
+    if ($sort) {
+      $params['sort'] = implode(', ', $sort);
+    }
+    if (!empty($facet_params['facet.field'])) {
+      $params += $facet_params;
+    }
+    if (!empty($highlight_params)) {
+      $params += $highlight_params;
+    }
+    if (!empty($options['search_api_spellcheck'])) {
+      $params['spellcheck'] = 'true';
+    }
+    if (!empty($mlt_params['mlt.fl'])) {
+      $params += $mlt_params;
+    }
+    if (!empty($this->options['retrieve_data'])) {
+      $params['fl'] = '*,score';
+    }
+    // Retrieve http method from server options.
+    $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'POST';
+
+    $call_args = array(
+      'query'       => &$keys,
+      'params'      => &$params,
+      'http_method' => &$http_method,
+    );
+    if ($this->request_handler) {
+      $this->setRequestHandler($this->request_handler, $call_args);
+    }
+
+    try {
+      // Send search request.
+      $time_processing_done = microtime(TRUE);
+      $this->connect();
+      drupal_alter('search_api_solr_query', $call_args, $query);
+      $this->preQuery($call_args, $query);
+
+      $response = $this->solr->search($keys, $params, $http_method);
+      $time_query_done = microtime(TRUE);
+
+      // Extract results.
+      $results = $this->extractResults($query, $response);
+
+      // Extract facets.
+      if ($facets = $this->extractFacets($query, $response)) {
+        $results['search_api_facets'] = $facets;
+      }
+
+      drupal_alter('search_api_solr_search_results', $results, $query, $response);
+      $this->postQuery($results, $query, $response);
+
+      // Compute performance.
+      $time_end = microtime(TRUE);
+      $results['performance'] = array(
+        'complete' => $time_end - $time_method_called,
+        'preprocessing' => $time_processing_done - $time_method_called,
+        'execution' => $time_query_done - $time_processing_done,
+        'postprocessing' => $time_end - $time_query_done,
+      );
+
+      return $results;
+    }
+    catch (SearchApiException $e) {
+      throw new SearchApiException(t('An error occurred while trying to search with Solr: @msg.', array('@msg' => $e->getMessage())));
+    }
+  }
+
+  /**
+   * Extract results from a Solr response.
+   *
+   * @param object $response
+   *   A HTTP response object.
+   *
+   * @return array
+   *   An array with two keys:
+   *   - result count: The number of total results.
+   *   - results: An array of search results, as specified by
+   *     SearchApiQueryInterface::execute().
+   */
+  protected function extractResults(SearchApiQueryInterface $query, $response) {
+    $index = $query->getIndex();
+    $fields = $this->getFieldNames($index);
+    $field_options = $index->options['fields'];
+
+    // Set up the results array.
+    $results = array();
+    $results['results'] = array();
+    // In some rare cases (e.g., MLT query with nonexistent ID) the response
+    // will be NULL.
+    if (!isset($response->response)) {
+      $results['result count'] = 0;
+      return $results;
+    }
+    $results['result count'] = $response->response->numFound;
+
+    // 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};
+          // Date fields need some special treatment to become valid date values
+          // (i.e., timestamps) again.
+          if (isset($field_options[$search_api_property]['type'])
+              && $field_options[$search_api_property]['type'] == 'date'
+              && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $result['fields'][$search_api_property])) {
+            $result['fields'][$search_api_property] = strtotime($result['fields'][$search_api_property]);
+          }
+        }
+      }
+
+      // We can find the item id and score in the special 'search_api_*'
+      // 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'];
+
+      $solr_id = $this->createId($index->machine_name, $result['id']);
+      $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields);
+      if ($excerpt) {
+        $result['excerpt'] = $excerpt;
+      }
+
+      // 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;
+      }
+    }
+
+    // Check for spellcheck suggestions.
+    if (module_exists('search_api_spellcheck') && $query->getOption('search_api_spellcheck')) {
+      $results['search_api_spellcheck'] = new SearchApiSpellcheckSolr($response);
+    }
+
+    return $results;
+  }
+
+  /**
+   * Extract and format highlighting information for a specific item from a Solr response.
+   *
+   * Will also use highlighted fields to replace retrieved field data, if the
+   * corresponding option is set.
+   */
+  protected function getExcerpt($response, $id, array &$fields, array $field_mapping) {
+    if (!isset($response->highlighting->$id)) {
+      return FALSE;
+    }
+    $output = '';
+
+    if (!empty($this->options['excerpt']) && !empty($response->highlighting->$id->spell)) {
+      foreach ($response->highlighting->$id->spell as $snippet) {
+        $snippet = strip_tags($snippet);
+        $snippet = preg_replace('/^.*>|<.*$/', '', $snippet);
+        $snippet = $this->formatHighlighting($snippet);
+        // The created fragments sometimes have leading or trailing punctuation.
+        // We remove that here for all common cases, but take care not to remove
+        // < or > (so HTML tags stay valid).
+        $snippet = trim($snippet, "\00..\x2F:;=\x3F..\x40\x5B..\x60");
+        $output .= $snippet . ' … ';
+      }
+    }
+    if (!empty($this->options['highlight_data'])) {
+      foreach ($field_mapping as $search_api_property => $solr_property) {
+        if (substr($solr_property, 0, 2) == 't_' && !empty($response->highlighting->$id->$solr_property)) {
+          // Contrary to above, we here want to preserve HTML, so we just
+          // replace the [HIGHLIGHT] tags with the appropriate format here.
+          $fields[$search_api_property] = $this->formatHighlighting($response->highlighting->$id->$solr_property);
+        }
+      }
+    }
+
+    return $output;
+  }
+
+
+  protected function formatHighlighting($snippet) {
+    return preg_replace('#\[(/?)HIGHLIGHT\]#', '<$1strong>', $snippet);
+  }
+
+  /**
+   * Extract facets from a Solr response.
+   *
+   * @param object $response
+   *   A response object from SolrPhpClient.
+   *
+   * @return array
+   *   An array describing facets that apply to the current results.
+   */
+  protected function extractFacets(SearchApiQueryInterface $query, $response) {
+    if (isset($response->facet_counts->facet_fields)) {
+      $index = $query->getIndex();
+      $fields = $this->getFieldNames($index);
+
+      $facets = array();
+      $facet_fields = $response->facet_counts->facet_fields;
+
+      $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 (!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.
+            if (isset($terms->_empty_) && $terms->_empty_ < $min_count) {
+              unset($terms->_empty_);
+            }
+            else {
+              $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])) {
+            unset($facets[$delta]);
+          }
+        }
+      }
+
+      return $facets;
+    }
+  }
+
+  /**
+   * Flatten a keys array into a single search string.
+   *
+   * @param array $keys
+   *   The keys array to flatten, formatted as specified by
+   *   SearchApiQueryInterface::getKeys().
+   *
+   * @return string
+   *   A Solr query string representing the same keys.
+   */
+  protected function flattenKeys(array $keys) {
+    $k = array();
+    $or = $keys['#conjunction'] == 'OR';
+    $neg = !empty($keys['#negation']);
+    foreach (element_children($keys) as $i) {
+      $key = $keys[$i];
+      if (!$key) {
+        continue;
+      }
+      if (is_array($key)) {
+        $subkeys = $this->flattenKeys($key);
+        if ($subkeys) {
+          $nested_expressions = TRUE;
+          // If this is a negated OR expression, we can't just use nested keys
+          // as-is, but have to put them into parantheses.
+          if ($or && $neg) {
+            $subkeys = "($subkeys)";
+          }
+          $k[] = $subkeys;
+        }
+      }
+      else {
+        $key = trim($key);
+        $key = call_user_func(array($this->connection_class, 'phrase'), $key);
+        $k[] = $key;
+      }
+    }
+    if (!$k) {
+      return '';
+    }
+
+    // Formatting the keys into a Solr query can be a bit complex. The following
+    // code will produce that look like this:
+    //
+    // #conjunction | #negation | return value
+    // ----------------------------------------------------------------
+    // AND          | FALSE     | A B C
+    // AND          | TRUE      | -(A B C)
+    // OR           | FALSE     | ((A) OR (B) OR (C))
+    // OR           | TRUE      | -A -B -C
+
+    // If there was just a single, unnested key, we can ignore all this.
+    if (count($k) == 1 && empty($nested_expressions)) {
+      $k = reset($k);
+      return $neg ? "-$k" : $k;
+    }
+
+    if ($or) {
+      if ($neg) {
+        return '-' . implode(' -', $k);
+      }
+      return '((' . implode(') OR (', $k) . '))';
+    }
+    $k = implode(' ', $k);
+    return $neg ? "-($k)" : $k;
+  }
+
+  /**
+   * Transforms a query filter into a flat array of Solr filter queries, using
+   * the field names in $fields.
+   */
+  protected function createFilterQueries(SearchApiQueryFilterInterface $filter, array $solr_fields, array $fields) {
+    $or = $filter->getConjunction() == 'OR';
+    $fq = array();
+    foreach ($filter->getFilters() as $f) {
+      if (is_array($f)) {
+        if (!isset($fields[$f[0]])) {
+          throw new SearchApiException(t('Filter term on unknown or unindexed field @field.', array('@field' => $f[0])));
+        }
+        if ($f[1] !== '') {
+          $fq[] = $this->createFilterQuery($solr_fields[$f[0]], $f[1], $f[2], $fields[$f[0]]);
+        }
+      }
+      else {
+        $q = $this->createFilterQueries($f, $solr_fields, $fields);
+        if ($filter->getConjunction() != $f->getConjunction()) {
+          // $or == TRUE means the nested filter has conjunction AND, and vice versa
+          $sep = $or ? ' ' : ' OR ';
+          $fq[] = count($q) == 1 ? reset($q) : '((' . implode(')' . $sep . '(', $q) . '))';
+        }
+        else {
+          $fq = array_merge($fq, $q);
+        }
+      }
+    }
+    return ($or && count($fq) > 1) ? array('((' . implode(') OR (', $fq) . '))') : $fq;
+  }
+
+  /**
+   * Create a single search query string according to the given field, value
+   * and operator.
+   */
+  protected function createFilterQuery($field, $value, $operator, $field_info) {
+    $field = call_user_func(array($this->connection_class, 'escapeFieldName'), $field);
+    if ($value === NULL) {
+      return ($operator == '=' ? '-' : '') . "$field:[* TO *]";
+    }
+    $value = trim($value);
+    $value = $this->formatFilterValue($value, search_api_extract_inner_type($field_info['type']));
+    switch ($operator) {
+      case '<>':
+        return "-($field:$value)";
+      case '<':
+        return "$field:{* TO $value}";
+      case '<=':
+        return "$field:[* TO $value]";
+      case '>=':
+        return "$field:[$value TO *]";
+      case '>':
+        return "$field:{{$value} TO *}";
+
+      default:
+        return "$field:$value";
+    }
+  }
+
+  /**
+   * Format a value for filtering on a field of a specific type.
+   */
+  protected function formatFilterValue($value, $type) {
+    switch ($type) {
+      case 'boolean':
+        $value = $value ? 'true' : 'false';
+        break;
+      case 'date':
+        $value = is_numeric($value) ? (int) $value : strtotime($value);
+        if ($value === FALSE) {
+          return 0;
+        }
+        $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC');
+        break;
+    }
+    return call_user_func(array($this->connection_class, 'phrase'), $value);
+  }
+
+  /**
+   * Helper method for creating the facet field parameters.
+   */
+  protected function getFacetParams(array $facets, array $fields, array &$fq = array()) {
+    if (!$facets) {
+      return array();
+    }
+    $facet_params['facet'] = 'true';
+    $facet_params['facet.sort'] = 'count';
+    $facet_params['facet.limit'] = 10;
+    $facet_params['facet.mincount'] = 1;
+    $facet_params['facet.missing'] = 'false';
+    $taggedFields = array();
+    foreach ($facets as $info) {
+      if (empty($fields[$info['field']])) {
+        continue;
+      }
+      // String fields have their own corresponding facet fields.
+      $field = $fields[$info['field']];
+      // Check for the "or" operator.
+      if (isset($info['operator']) && $info['operator'] === 'or') {
+        // Remember that filters for this field should be tagged.
+        $escaped = call_user_func(array($this->connection_class, 'escapeFieldName'), $fields[$info['field']]);
+        $taggedFields[$escaped] = "{!tag=$escaped}";
+        // Add the facet field.
+        $facet_params['facet.field'][] = "{!ex=$escaped}$field";
+      }
+      else {
+        // Add the facet field.
+        $facet_params['facet.field'][] = $field;
+      }
+      // Set limit, unless it's the default.
+      if ($info['limit'] != 10) {
+        $facet_params["f.$field.facet.limit"] = $info['limit'] ? $info['limit'] : -1;
+      }
+      // Set mincount, unless it's the default.
+      if ($info['min_count'] != 1) {
+        $facet_params["f.$field.facet.mincount"] = $info['min_count'];
+      }
+      // Set missing, if specified.
+      if ($info['missing']) {
+        $facet_params["f.$field.facet.missing"] = 'true';
+      }
+    }
+    // Tag filters of fields with "OR" facets.
+    foreach ($taggedFields as $field => $tag) {
+      $regex = '#(?<![^( ])' . preg_quote($field, '#') . ':#';
+      foreach ($fq as $i => $filter) {
+        // Solr can't handle two tags on the same filter, so we don't add two.
+        // Another option here would even be to remove the other tag, too,
+        // since we can be pretty sure that this filter does not originate from
+        // a facet – however, wrong results would still be possible, and this is
+        // definitely an edge case, so don't bother.
+        if (preg_match($regex, $filter) && substr($filter, 0, 6) != '{!tag=') {
+          $fq[$i] = $tag . $filter;
+        }
+      }
+    }
+
+    return $facet_params;
+  }
+
+  /**
+   * Helper method for creating the highlighting parameters.
+   *
+   * (The $query parameter currently isn't used and only here for the potential
+   * sake of subclasses.)
+   */
+  protected function getHighlightParams(SearchApiQueryInterface $query) {
+    $highlight_params = array();
+
+    if (!empty($this->options['excerpt']) || !empty($this->options['highlight_data'])) {
+      $highlight_params['hl'] = 'true';
+      $highlight_params['hl.fl'] = 'spell';
+      $highlight_params['hl.simple.pre'] = '[HIGHLIGHT]';
+      $highlight_params['hl.simple.post'] = '[/HIGHLIGHT]';
+      $highlight_params['hl.snippets'] = 3;
+      $highlight_params['hl.fragsize'] = 70;
+      $highlight_params['hl.mergeContiguous'] = 'true';
+    }
+
+    if (!empty($this->options['highlight_data'])) {
+      $highlight_params['hl.fl'] = 't_*';
+      $highlight_params['hl.snippets'] = 1;
+      $highlight_params['hl.fragsize'] = 0;
+      if (!empty($this->options['excerpt'])) {
+        // If we also generate a "normal" excerpt, set the settings for the
+        // "spell" field (which we use to generate the excerpt) back to the
+        // above values.
+        $highlight_params['f.spell.hl.snippets'] = 3;
+        $highlight_params['f.spell.hl.fragsize'] = 70;
+        // It regrettably doesn't seem to be possible to set hl.fl to several
+        // values, if one contains wild cards (i.e., "t_*,spell" wouldn't work).
+        $highlight_params['hl.fl'] = '*';
+      }
+    }
+
+    return $highlight_params;
+  }
+
+  /**
+   * Sets the request handler.
+   *
+   * This should also make the needed adjustments to the request parameters.
+   *
+   * @param $handler
+   *   Name of the handler to set.
+   * @param array $call_args
+   *   An associative array containing all three arguments to the
+   *   SearchApiSolrConnectionInterface::search() call ("query", "params" and
+   *   "method") as references.
+   *
+   * @return bool
+   *   TRUE iff this method invocation handled the given handler. This allows
+   *   subclasses to recognize whether the request handler was already set by
+   *   this method.
+   */
+  protected function setRequestHandler($handler, array &$call_args) {
+    if ($handler == 'pinkPony') {
+      $call_args['params']['qt'] = $handler;
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Empty method called before sending a search query to Solr.
+   *
+   * This allows subclasses to apply custom changes before the query is sent to
+   * Solr. Works exactly like hook_search_api_solr_query_alter().
+   *
+   * @param array $call_args
+   *   An associative array containing all three arguments to the
+   *   SearchApiSolrConnectionInterface::search() call ("query", "params" and
+   *   "method") as references.
+   * @param SearchApiQueryInterface $query
+   *   The SearchApiQueryInterface object representing the executed search query.
+   */
+  protected function preQuery(array &$call_args, SearchApiQueryInterface $query) {
+  }
+
+  /**
+   * Empty method to allow subclasses to apply custom changes before search results are returned.
+   *
+   * Works exactly like hook_search_api_solr_search_results_alter().
+   *
+   * @param array $results
+   *   The results array that will be returned for the search.
+   * @param SearchApiQueryInterface $query
+   *   The SearchApiQueryInterface object representing the executed search query.
+   * @param object $response
+   *   The response object returned by Solr.
+   */
+  protected function postQuery(array &$results, SearchApiQueryInterface $query, $response) {
+  }
+
+  //
+  // Autocompletion feature
+  //
+
+  /**
+   * Implements SearchApiAutocompleteInterface::getAutocompleteSuggestions().
+   */
+  // Largely copied from the apachesolr_autocomplete module.
+  public function getAutocompleteSuggestions(SearchApiQueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) {
+    $suggestions = array();
+    // Reset request handler
+    $this->request_handler = NULL;
+    // Turn inputs to lower case, otherwise we get case sensivity problems.
+    $incomp = drupal_strtolower($incomplete_key);
+
+    $index = $query->getIndex();
+    $fields = $this->getFieldNames($index);
+    $complete = $query->getOriginalKeys();
+
+    // Extract keys
+    $keys = $query->getKeys();
+    if (is_array($keys)) {
+      $keys_array = array();
+      while ($keys) {
+        reset($keys);
+        if (!element_child(key($keys))) {
+          array_shift($keys);
+          continue;
+        }
+        $key = array_shift($keys);
+        if (is_array($key)) {
+          $keys = array_merge($keys, $key);
+        }
+        else {
+          $keys_array[$key] = $key;
+        }
+      }
+      $keys = $this->flattenKeys($query->getKeys());
+    }
+    else {
+      $keys_array = drupal_map_assoc(preg_split('/[-\s():{}\[\]\\\\"]+/', $keys, -1, PREG_SPLIT_NO_EMPTY));
+    }
+    if (!$keys) {
+      $keys = NULL;
+    }
+
+    // Set searched fields
+    $options = $query->getOptions();
+    $search_fields = $query->getFields();
+    $qf = array();
+    foreach ($search_fields as $f) {
+      $qf[] = $fields[$f];
+    }
+
+    // Extract filters
+    $fq = $this->createFilterQueries($query->getFilter(), $fields, $index->options['fields']);
+    $fq[] = 'index_id:' . $index->machine_name;
+
+    // Autocomplete magic
+    $facet_fields = array();
+    foreach ($search_fields as $f) {
+      $facet_fields[] = $fields[$f];
+    }
+
+    $limit = $query->getOption('limit', 10);
+
+    $params = array(
+      'qf' => $qf,
+      'fq' => $fq,
+      'rows' => 0,
+      'facet' => 'true',
+      'facet.field' => $facet_fields,
+      'facet.prefix' => $incomp,
+      'facet.limit' => $limit * 5,
+      'facet.mincount' => 1,
+      'spellcheck' => (!isset($this->options['autocorrect_spell']) || $this->options['autocorrect_spell']) ? 'true' : 'false',
+      'spellcheck.count' => 1,
+    );
+    // Retrieve http method from server options.
+    $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'POST';
+
+    $call_args = array(
+      'query'       => &$keys,
+      'params'      => &$params,
+      'http_method' => &$http_method,
+    );
+    if ($this->request_handler) {
+      $this->setRequestHandler($this->request_handler, $call_args);
+    }
+    $second_pass = !isset($this->options['autocorrect_suggest_words']) || $this->options['autocorrect_suggest_words'];
+    for ($i = 0; $i < ($second_pass ? 2 : 1); ++$i) {
+      try {
+        // Send search request
+        $this->connect();
+        drupal_alter('search_api_solr_query', $call_args, $query);
+        $this->preQuery($call_args, $query);
+        $response = $this->solr->search($keys, $params, $http_method);
+
+        if (!empty($response->spellcheck->suggestions)) {
+          $replace = array();
+          foreach ($response->spellcheck->suggestions as $word => $data) {
+            $replace[$word] = $data->suggestion[0];
+          }
+          $corrected = str_ireplace(array_keys($replace), array_values($replace), $user_input);
+          if ($corrected != $user_input) {
+            array_unshift($suggestions, array(
+              'prefix' => t('Did you mean') . ':',
+              'user_input' => $corrected,
+            ));
+          }
+        }
+
+        $matches = array();
+        if (isset($response->facet_counts->facet_fields)) {
+          foreach ($response->facet_counts->facet_fields as $terms) {
+            foreach ($terms as $term => $count) {
+              if (isset($matches[$term])) {
+                // If we just add the result counts, we can easily get over the
+                // total number of results if terms appear in multiple fields.
+                // Therefore, we just take the highest value from any field.
+                $matches[$term] = max($matches[$term], $count);
+              }
+              else {
+                $matches[$term] = $count;
+              }
+            }
+          }
+
+          if ($matches) {
+            // Eliminate suggestions that are too short or already in the query.
+            foreach ($matches as $term => $count) {
+              if (strlen($term) < 3 || isset($keys_array[$term])) {
+                unset($matches[$term]);
+              }
+            }
+
+            // Don't suggest terms that are too frequent (by default in more
+            // than 90% of results).
+            $result_count = $response->response->numFound;
+            $max_occurrences = $result_count * variable_get('search_api_solr_autocomplete_max_occurrences', 0.9);
+            if (($max_occurrences >= 1 || $i > 0) && $max_occurrences < $result_count) {
+              foreach ($matches as $match => $count) {
+                if ($count > $max_occurrences) {
+                  unset($matches[$match]);
+                }
+              }
+            }
+
+            // The $count in this array is actually a score. We want the
+            // highest ones first.
+            arsort($matches);
+
+            // Shorten the array to the right ones.
+            $additional_matches = array_slice($matches, $limit - count($suggestions), NULL, TRUE);
+            $matches = array_slice($matches, 0, $limit, TRUE);
+
+            // Build suggestions using returned facets
+            $incomp_length = strlen($incomp);
+            foreach ($matches as $term => $count) {
+              if (drupal_strtolower(substr($term, 0, $incomp_length)) == $incomp) {
+                $suggestions[] = array(
+                  'suggestion_suffix' => substr($term, $incomp_length),
+                  'results' => $count,
+                );
+              }
+              else {
+                $suggestions[] = array(
+                  'suggestion_suffix' => ' ' . $term,
+                  'results' => $count,
+                );
+              }
+            }
+          }
+        }
+      }
+      catch (SearchApiException $e) {
+        watchdog_exception('search_api_solr', $e, "%type during autocomplete Solr query: !message in %function (line %line of %file).", array(), WATCHDOG_WARNING);
+      }
+
+      if (count($suggestions) >= $limit) {
+        break;
+      }
+      // Change parameters for second query.
+      unset($params['facet.prefix']);
+      $keys = trim ($keys . ' ' . $incomplete_key);
+    }
+
+    return $suggestions;
+  }
+
+  //
+  // SearchApiMultiServiceInterface methods
+  //
+
+  /**
+   * Implements SearchApiMultiServiceInterface::queryMultiple().
+   */
+  public function queryMultiple(array $options = array()) {
+    return new SearchApiMultiQuery($this->server, $options);
+  }
+
+  /**
+   * Implements SearchApiMultiServiceInterface::searchMultiple().
+   */
+  public function searchMultiple(SearchApiMultiQueryInterface $query) {
+    $time_method_called = microtime(TRUE);
+    // Get field information
+    $solr_fields = array(
+      'search_api_id' => 'ss_search_api_id',
+      'search_api_relevance' => 'score',
+      'search_api_multi_index' => 'index_id',
+    );
+    $fields = array(
+      'search_api_multi_index' => array(
+        'type' => 'string',
+      ),
+    );
+    foreach ($query->getIndexes() as $index_id => $index) {
+      if (empty($index->options['fields'])) {
+        continue;
+      }
+      $prefix = $index_id . ':';
+      foreach ($this->getFieldNames($index) as $field => $key) {
+        if (!isset($solr_fields[$field])) {
+          $solr_fields[$prefix . $field] = $key;
+        }
+      }
+      foreach ($index->options['fields'] as $field => $info) {
+        $fields[$prefix . $field] = $info;
+      }
+    }
+
+    // Extract keys
+    $keys = $query->getKeys();
+    if (is_array($keys)) {
+      $keys = $this->flattenKeys($keys);
+    }
+
+    // Set searched fields
+    $search_fields = $query->getFields();
+    $qf = array();
+    foreach ($search_fields as $f) {
+      $qf[] = $solr_fields[$f];
+    }
+
+    // Extract filters
+    $filter = $query->getFilter();
+    $fq = $this->createFilterQueries($filter, $solr_fields, $fields);
+
+    // Restrict search to searched indexes.
+    $index_filter = array();
+    foreach ($query->getIndexes() as $index_id => $index) {
+      $index_filter[] = 'index_id:' . call_user_func(array($this->connection_class, 'phrase'), $index_id);
+    }
+    $fq[] = implode(' OR ', $index_filter);
+
+    // Extract sort
+    $sort = array();
+    foreach ($query->getSort() as $f => $order) {
+      $f = $solr_fields[$f];
+      if (substr($f, 0, 3) == 'ss_') {
+        $f = 'sort_' . substr($f, 3);
+      }
+      $order = strtolower($order);
+      $sort[] = "$f $order";
+    }
+
+    // Get facet fields
+    $facets = $query->getOption('search_api_facets') ? $query->getOption('search_api_facets') : array();
+    $facet_params = $this->getFacetParams($facets, $solr_fields);
+
+    // Set defaults
+    if (!$keys) {
+      $keys = NULL;
+    }
+    $options = $query->getOptions();
+
+    // Collect parameters
+    $params = array(
+      'fl' => 'item_id,index_id,score',
+      'qf' => $qf,
+      'fq' => $fq,
+    );
+    if (isset($options['offset'])) {
+      $params['start'] = $options['offset'];
+    }
+    if (isset($options['limit'])) {
+      $params['rows'] = $options['limit'];
+    }
+    if ($sort) {
+      $params['sort'] = implode(', ', $sort);
+    }
+    if (!empty($facet_params['facet.field'])) {
+      $params += $facet_params;
+    }
+
+    // Retrieve http method from server options.
+    $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'POST';
+
+    // Send search request
+    $time_processing_done = microtime(TRUE);
+    $this->connect();
+
+    $call_args = array(
+      'query'       => &$keys,
+      'params'      => &$params,
+      'http_method' => &$http_method,
+    );
+    drupal_alter('search_api_solr_multi_query', $call_args, $query);
+
+    $response = $this->solr->search($keys, $params, $http_method);
+    $time_query_done = microtime(TRUE);
+
+    // Extract results
+    $results = array();
+    $results['result count'] = $response->response->numFound;
+    $results['results'] = array();
+    $tmp = array();
+    foreach ($response->response->docs as $id => $doc) {
+      $result = array(
+        'id' => $doc->item_id,
+        'index_id' => $doc->index_id,
+        'score' => $doc->score,
+      );
+      $excerpt = $this->getExcerpt($response, $id, $tmp, array());
+      if ($excerpt) {
+        $result['excerpt'] = $excerpt;
+      }
+      $results['results'][$id] = $result;
+    }
+
+    // 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 = $this->getFacetField($solr_fields[$info['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);
+            }
+          }
+          foreach ($terms as $term => $count) {
+            if ($count >= $min_count) {
+              $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]);
+          }
+        }
+      }
+    }
+
+    // Compute performance
+    $time_end = microtime(TRUE);
+    $results['performance'] = array(
+      'complete' => $time_end - $time_method_called,
+      'preprocessing' => $time_processing_done - $time_method_called,
+      'execution' => $time_query_done - $time_processing_done,
+      'postprocessing' => $time_end - $time_query_done,
+    );
+
+    return $results;
+  }
+
+  //
+  // Additional methods that might be used when knowing the service class.
+  //
+
+  /**
+   * Ping the Solr server to tell whether it can be accessed.
+   *
+   * Uses the admin/ping request handler.
+   */
+  public function ping() {
+    $this->connect();
+    return $this->solr->ping();
+  }
+
+  /**
+   * Sends a commit command to the Solr server.
+   */
+  public function commit() {
+    try {
+      $this->connect();
+      return $this->solr->commit(FALSE);
+    }
+    catch (SearchApiException $e) {
+      watchdog_exception('search_api_solr', $e,
+          '%type while trying to commit on server @server: !message in %function (line %line of %file).',
+          array('@server' => $this->server->machine_name), WATCHDOG_WARNING);
+    }
+  }
+
+  /**
+   * Schedules a commit operation for this server.
+   *
+   * The commit will be sent at the end of the current page request. Multiple
+   * calls to this method will still only result in one commit operation.
+   */
+  public function scheduleCommit() {
+    if (!$this->commitScheduled) {
+      $this->commitScheduled = TRUE;
+      drupal_register_shutdown_function(array($this, 'commit'));
+    }
+  }
+
+  /**
+   * Gets the Solr connection class used by this service.
+   *
+   * @return string
+   *   The name of a class which implements SearchApiSolrConnectionInterface.
+   */
+  public function getConnectionClass() {
+    return $this->connection_class;
+  }
+
+  /**
+   * Sets the Solr connection class used by this service.
+   *
+   * @param string $class
+   *   The name of a class which implements SearchApiSolrConnectionInterface.
+   */
+  public function setConnectionClass($class) {
+    $this->connection_class = $class;
+    unset($this->solr);
+  }
+
+  /**
+   * Gets the currently used Solr connection object.
+   *
+   * @return SearchApiSolrConnectionInterface
+   *   The solr connection object used by this server.
+   */
+  public function getSolrConnection() {
+    $this->connect();
+    return $this->solr;
+  }
+
+  /**
+   * Get metadata about fields in the Solr/Lucene index.
+   *
+   * @param int $num_terms
+   *   Number of 'top terms' to return.
+   *
+   * @return array
+   *   An array of SearchApiSolrField objects.
+   *
+   * @see SearchApiSolrConnectionInterface::getFields()
+   */
+  public function getFields($num_terms = 0) {
+    $this->connect();
+    return $this->solr->getFields($num_terms);
+  }
+
+}
diff --git a/includes/solr_connection.inc b/includes/solr_connection.inc
new file mode 100644
index 0000000..f68722c
--- /dev/null
+++ b/includes/solr_connection.inc
@@ -0,0 +1,875 @@
+<?php
+
+/**
+ * Copyright (c) 2007-2009, Conduit Internet Technologies, Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  - Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Conduit Internet Technologies, Inc. nor the names of
+ *    its contributors may be used to endorse or promote products derived from
+ *    this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @copyright Copyright 2007-2009 Conduit Internet Technologies, Inc. (http://conduit-it.com)
+ * @license New BSD (http://solr-php-client.googlecode.com/svn/trunk/COPYING)
+ * @version $Id: Service.php 22 2009-11-09 22:46:54Z donovan.jimenez $
+ *
+ * @package Apache
+ * @subpackage Solr
+ * @author Donovan Jimenez <djimenez@conduit-it.com>
+ */
+
+/**
+ * Additional code Copyright (c) 2008-2011 by Robert Douglass, James McKinney,
+ * Jacob Singh, Alejandro Garza, Peter Wolanin, and additional contributors.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program as the file LICENSE.txt; if not, please see
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+ */
+
+/**
+ * Starting point for the Solr API. Represents a Solr server resource and has
+ * methods for pinging, adding, deleting, committing, optimizing and searching.
+ */
+
+class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
+
+  /**
+   * Defines how NamedLists should be formatted in the output.
+   *
+   * This specifically affects facet counts. Valid values are 'map' (default) or
+   * 'flat'.
+   */
+  const NAMED_LIST_FORMAT = 'map';
+
+  /**
+   * Path to the ping servlet.
+   */
+  const PING_SERVLET = 'admin/ping';
+
+  /**
+   * Path to the update servlet.
+   */
+  const UPDATE_SERVLET = 'update';
+
+  /**
+   * Path to the search servlet.
+   */
+  const SEARCH_SERVLET = 'select';
+
+  /**
+   * Path to the luke servlet.
+   */
+  const LUKE_SERVLET = 'admin/luke';
+
+  /**
+   * Path to the system servlet.
+   */
+  const SYSTEM_SERVLET = 'admin/system';
+
+  /**
+   * Path to the stats servlet.
+   */
+  const STATS_SERVLET = 'admin/stats.jsp';
+
+  /**
+   * Path to the stats servlet for Solr 4.x servers.
+   */
+  const STATS_SERVLET_4 = 'admin/mbeans?wt=xml&stats=true';
+
+  /**
+   * The options passed when creating this connection.
+   *
+   * @var array
+   */
+  protected $options;
+
+  /**
+   * The Solr server's URL.
+   *
+   * @var string
+   */
+  protected $base_url;
+
+  /**
+   * Cached URL to the update servlet.
+   *
+   * @var string
+   */
+  protected $update_url;
+
+  /**
+   * The HTTP method to use for search requests.
+   *
+   * @var string
+   */
+  protected $method;
+
+  /**
+   * HTTP Basic Authentication header to set for requests to the Solr server.
+   *
+   * @var string
+   */
+  protected $http_auth;
+
+  /**
+   * Cache for the metadata from admin/luke.
+   *
+   * Contains an array of response objects, keyed by the number of "top terms".
+   *
+   * @var array
+   *
+   * @see getLuke()
+   */
+  protected $luke = array();
+
+  /**
+   * Cache for information about the Solr core.
+   *
+   * @var SimpleXMLElement
+   *
+   * @see getStats()
+   */
+  protected $stats;
+
+  /**
+   * Cache for system information.
+   *
+   * @var array
+   *
+   * @see getSystemInfo()
+   */
+  protected $system_info;
+
+  /**
+   * Flag that denotes whether to use soft commits for Solr 4.x.
+   *
+   * Defaults to FALSE.
+   *
+   * @var bool
+   */
+  protected $soft_commit = FALSE;
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::__construct().
+   *
+   * Valid options include:
+   *   - scheme: Scheme of the base URL of the Solr server. Most probably "http"
+   *     or "https". Defaults to "http".
+   *   - host: The host name (or IP) of the Solr server. Defaults to
+   *     "localhost".
+   *   - port: The port of the Solr server. Defaults to 8983.
+   *   - path: The base path to the Solr server. Defaults to "/solr/".
+   *   - http_user: If both this and "http_pass" are set, will use this
+   *     information to add basic HTTP authentication to all requests to the
+   *     Solr server. Not set by default.
+   *   - http_pass: See "http_user".
+   *   - http_method: The HTTP method to use for searches. Can be either "GET"
+   *     or "POST". Defaults to "POST".
+   */
+  public function __construct(array $options) {
+    $options += array(
+      'scheme' => 'http',
+      'host' => 'localhost',
+      'port' => 8983,
+      'path' => 'solr',
+      'http_user' => NULL,
+      'http_pass' => NULL,
+      'http_method' => 'POST',
+    );
+    $this->options = $options;
+
+    $path = '/' . trim($options['path'], '/') . '/';
+    $this->base_url = $options['scheme'] . '://' . $options['host'] . ':' . $options['port'] . $path;
+
+    // Make sure we always have a valid method set, default to POST.
+    $this->method = $options['http_method'] == 'GET' ? 'GET' : 'POST';
+
+    // Set HTTP Basic Authentication parameter, if login data was set.
+    if (strlen($options['http_user']) && strlen($options['http_pass'])) {
+      $this->http_auth = 'Basic ' . base64_encode($options['http_user'] . ':' . $options['http_pass']);
+    }
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::ping().
+   */
+  public function ping($timeout = 2) {
+    $start = microtime(TRUE);
+
+    if ($timeout <= 0.0) {
+      $timeout = -1;
+    }
+    $pingUrl = $this->constructUrl(self::PING_SERVLET);
+
+    // Attempt a HEAD request to the Solr ping url.
+    $options = array(
+      'method' => 'HEAD',
+      'timeout' => $timeout,
+    );
+    $response = $this->makeHttpRequest($pingUrl, $options);
+
+    if ($response->code == 200) {
+      // Add 1 µs to the ping time so we never return 0.
+      return (microtime(TRUE) - $start) + 1E-6;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  /**
+   * Sets whether this connection will use soft commits when comitting.
+   *
+   * @param $soft_commit
+   *   TRUE if soft commits should be used, FALSE otherwise. Default is FALSE.
+   */
+  public function setSoftCommit($soft_commit) {
+    $this->soft_commit = (bool) $soft_commit;
+  }
+
+  /**
+   * Tells whether this connection will use soft commits when comitting.
+   *
+   * @return
+   *   TRUE if soft commits will be used, FALSE otherwise.
+   */
+  public function getSoftCommit() {
+    return $this->soft_commit;
+  }
+
+  /**
+   * Computes the cache ID to use for this connection.
+   *
+   * @param $suffix
+   *   (optional) A suffix to append to the string to make it unique.
+   *
+   * @return
+   *   The cache ID to use for this connection and usage.
+   */
+  protected function getCacheId($suffix = '') {
+    if (empty($this->options['server'])) {
+      $cid = $this->options['server'];
+      return $suffix ? "$cid:$suffix" : $cid;
+    }
+  }
+
+  /**
+   * Call the /admin/system servlet to retrieve system information.
+   *
+   * Stores the retrieved information in $system_info.
+   *
+   * @see getSystemInfo()
+   */
+  protected function setSystemInfo() {
+    $cid = $this->getCacheId(__FUNCTION__);
+    if ($cid) {
+      $cache = cache_get($cid, 'cache_search_api_solr');
+      if ($cache) {
+        $this->system_info = json_decode($cache->data);
+      }
+    }
+    // Second pass to populate the cache if necessary.
+    if (empty($this->system_info)) {
+      $url = $this->constructUrl(self::SYSTEM_SERVLET, array('wt' => 'json'));
+      $response = $this->sendRawGet($url);
+      $this->system_info = json_decode($response->data);
+      if ($cid) {
+        cache_set($cid, $response->data, 'cache_search_api_solr');
+      }
+    }
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::getSystemInfo().
+   */
+  public function getSystemInfo() {
+    if (!isset($this->system_info)) {
+      $this->setSystemInfo();
+    }
+    return $this->system_info;
+  }
+
+  /**
+   * Sets $this->luke with the metadata about the index from admin/luke.
+   */
+  protected function setLuke($num_terms = 0) {
+    if (empty($this->luke[$num_terms])) {
+      $cid = $this->getCacheId(__FUNCTION__ . ":$num_terms");
+      if ($cid) {
+        $cache = cache_get($cid, 'cache_search_api_solr');
+        if (isset($cache->data)) {
+          $this->luke = $cache->data;
+        }
+      }
+      // Second pass to populate the cache if necessary.
+      if (empty($this->luke[$num_terms])) {
+        $params = array(
+          'numTerms' => "$num_terms",
+          'wt' => 'json',
+          'json.nl' => self::NAMED_LIST_FORMAT,
+        );
+        $url = $this->constructUrl(self::LUKE_SERVLET, $params);
+        $this->luke[$num_terms] = $this->sendRawGet($url);
+        if ($cid) {
+          cache_set($cid, $this->luke, 'cache_search_api_solr');
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::getFields().
+   */
+  public function getFields($num_terms = 0) {
+    $fields = array();
+    foreach ($this->getLuke($num_terms)->fields as $name => $info) {
+      $fields[$name] = new SearchApiSolrField($info);
+    }
+    return $fields;
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::getLuke().
+   */
+  public function getLuke($num_terms = 0) {
+    if (!isset($this->luke[$num_terms])) {
+      $this->setLuke($num_terms);
+    }
+    return $this->luke[$num_terms];
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::getSolrVersion().
+   */
+  public function getSolrVersion() {
+    $system_info = $this->getSystemInfo();
+    // Get our solr version number
+    if (isset($system_info->lucene->{'solr-spec-version'})) {
+      return $system_info->lucene->{'solr-spec-version'}[0];
+    }
+    return 0;
+  }
+
+  /**
+   * Stores information about the Solr core in $this->stats.
+   */
+  protected function setStats() {
+    $data = $this->getLuke();
+    $solr_version = $this->getSolrVersion();
+    // Only try to get stats if we have connected to the index.
+    if (empty($this->stats) && isset($data->index->numDocs)) {
+      $cid = $this->getCacheId(__FUNCTION__);
+      if ($cid) {
+        $cache = cache_get($cid, 'cache_search_api_solr');
+        if (isset($cache->data)) {
+          $this->stats = simplexml_load_string($cache->data);
+        }
+      }
+      // Second pass to populate the cache if necessary.
+      if (empty($this->stats)) {
+        if ($solr_version >= 4) {
+          $url = $this->constructUrl(self::STATS_SERVLET_4);
+        }
+        else {
+          $url = $this->constructUrl(self::STATS_SERVLET);
+        }
+        $response = $this->sendRawGet($url);
+        $this->stats = simplexml_load_string($response->data);
+        if ($this->env_id) {
+          cache_set($cid, $response->data, 'cache_search_api_solr');
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::getStats().
+   */
+  public function getStats() {
+    if (!isset($this->stats)) {
+      $this->setStats();
+    }
+    return $this->stats;
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::getStatsSummary().
+   */
+  public function getStatsSummary() {
+    $stats = $this->getStats();
+    $solr_version = $this->getSolrVersion();
+
+    $summary = array(
+     '@pending_docs' => '',
+     '@autocommit_time_seconds' => '',
+     '@autocommit_time' => '',
+     '@deletes_by_id' => '',
+     '@deletes_by_query' => '',
+     '@deletes_total' => '',
+     '@schema_version' => '',
+     '@core_name' => '',
+     '@index_size' => '',
+    );
+
+    if (!empty($stats)) {
+      if ($solr_version <= 3) {
+        $docs_pending_xpath = $stats->xpath('//stat[@name="docsPending"]');
+        $summary['@pending_docs'] = (int) trim(current($docs_pending_xpath));
+        $max_time_xpath = $stats->xpath('//stat[@name="autocommit maxTime"]');
+        $max_time = (int) trim(current($max_time_xpath));
+        // Convert to seconds.
+        $summary['@autocommit_time_seconds'] = $max_time / 1000;
+        $summary['@autocommit_time'] = format_interval($max_time / 1000);
+        $deletes_id_xpath = $stats->xpath('//stat[@name="deletesById"]');
+        $summary['@deletes_by_id'] = (int) trim(current($deletes_id_xpath));
+        $deletes_query_xpath = $stats->xpath('//stat[@name="deletesByQuery"]');
+        $summary['@deletes_by_query'] = (int) trim(current($deletes_query_xpath));
+        $summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query'];
+        $schema = $stats->xpath('/solr/schema[1]');
+        $summary['@schema_version'] = trim($schema[0]);
+        $core = $stats->xpath('/solr/core[1]');
+        $summary['@core_name'] = trim($core[0]);
+        $size_xpath = $stats->xpath('//stat[@name="indexSize"]');
+        $summary['@index_size'] = trim(current($size_xpath));
+      }
+      else {
+        $system_info = $this->getSystemInfo();
+        $docs_pending_xpath = $stats->xpath('//lst["stats"]/long[@name="docsPending"]');
+        $summary['@pending_docs'] = (int) trim(current($docs_pending_xpath));
+        $max_time_xpath = $stats->xpath('//lst["stats"]/str[@name="autocommit maxTime"]');
+        $max_time = (int) trim(current($max_time_xpath));
+        // Convert to seconds.
+        $summary['@autocommit_time_seconds'] = $max_time / 1000;
+        $summary['@autocommit_time'] = format_interval($max_time / 1000);
+        $deletes_id_xpath = $stats->xpath('//lst["stats"]/long[@name="deletesById"]');
+        $summary['@deletes_by_id'] = (int) trim(current($deletes_id_xpath));
+        $deletes_query_xpath = $stats->xpath('//lst["stats"]/long[@name="deletesByQuery"]');
+        $summary['@deletes_by_query'] = (int) trim(current($deletes_query_xpath));
+        $summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query'];
+        $schema = $system_info->core->schema;
+        $summary['@schema_version'] = $schema;
+        $core = $stats->xpath('//lst["core"]/str[@name="coreName"]');
+        $summary['@core_name'] = trim(current($core));
+        $size_xpath = $stats->xpath('//lst["core"]/str[@name="indexSize"]');
+        $summary['@index_size'] = trim(current($size_xpath));
+      }
+    }
+
+    return $summary;
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::clearCache().
+   */
+  public function clearCache() {
+    if ($cid = $this->getCacheId()) {
+      cache_clear_all($cid, 'cache_search_api_solr', TRUE);
+      cache_clear_all($cid, 'cache_search_api_solr', TRUE);
+    }
+    $this->luke = array();
+    $this->stats = NULL;
+    $this->system_info = NULL;
+  }
+
+  /**
+   * Checks the reponse code and throws an exception if it's not 200.
+   *
+   * @param object $response
+   *   A response object.
+   *
+   * @return object
+   *   The passed response object.
+   *
+   * @throws SearchApiException
+   *   If the object's HTTP status is not 200.
+   */
+  protected function checkResponse($response) {
+    $code = (int) $response->code;
+
+    if ($code != 200) {
+      if ($code >= 400 && $code != 403 && $code != 404) {
+        // Add details, like Solr's exception message.
+        $response->status_message .= $response->data;
+      }
+      throw new SearchApiException('"' . $code . '" Status: ' . $response->status_message);
+    }
+
+    return $response;
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::makeServletRequest().
+   */
+  public function makeServletRequest($servlet, array $params = array(), array $options = array()) {
+    // Add default params.
+    $params += array(
+      'wt' => 'json',
+      'json.nl' => self::NAMED_LIST_FORMAT,
+    );
+
+    $url = $this->constructUrl($servlet, $params);
+    $response = $this->makeHttpRequest($url, $options);
+
+    return $this->checkResponse($response);
+  }
+
+  /**
+   * Central method for making a GET operation against this Solr Server
+   */
+  protected function sendRawGet($url, array $options = array()) {
+    $options['method'] = 'GET';
+    $response = $this->makeHttpRequest($url, $options);
+
+    return $this->checkResponse($response);
+  }
+
+  /**
+   * Central method for making a POST operation against this Solr Server
+   */
+  protected function sendRawPost($url, array $options = array()) {
+    $options['method'] = 'POST';
+    // Normally we use POST to send XML documents.
+    if (empty($options['headers']['Content-Type'])) {
+      $options['headers']['Content-Type'] = 'text/xml; charset=UTF-8';
+    }
+    $response = $this->makeHttpRequest($url, $options);
+
+    return $this->checkResponse($response);
+  }
+
+  /**
+   * Sends an HTTP request to Solr.
+   *
+   * This is just a wrapper around drupal_http_request().
+   */
+  protected function makeHttpRequest($url, array $options = array()) {
+    if (empty($options['method']) || $options['method'] == 'GET' || $options['method'] == 'HEAD') {
+      // Make sure we are not sending a request body.
+      $options['data'] = NULL;
+    }
+    if ($this->http_auth) {
+      $options['headers']['Authorization'] = $this->http_auth;
+    }
+
+    $result = drupal_http_request($url, $options);
+
+    if (!isset($result->code) || $result->code < 0) {
+      $result->code = 0;
+      $result->status_message = 'Request failed';
+      $result->protocol = 'HTTP/1.0';
+    }
+    // Additional information may be in the error property.
+    if (isset($result->error)) {
+      $result->status_message .= ': ' . check_plain($result->error);
+    }
+
+    if (!isset($result->data)) {
+      $result->data = '';
+      $result->response = NULL;
+    }
+    else {
+      $response = json_decode($result->data);
+      if (is_object($response)) {
+        foreach ($response as $key => $value) {
+          $result->$key = $value;
+        }
+      }
+    }
+
+    return $result;
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::escape().
+   */
+  public static function escape($value) {
+    $replacements = array();
+
+    $specials = array('+', '-', '&&', '||', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', "\\");
+    // Solr 4.x introduces regular expressions, making the slash also a special
+    // character.
+    if ($this->getSolrVersion() >= 4) {
+      $specials[] = '/';
+    }
+
+    foreach ($specials as $special) {
+      $replacements[$special] = "\\$special";
+    }
+
+    return strtr($value, $replacements);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::escapePhrase().
+   */
+  public static function escapePhrase($value) {
+    $replacements['"'] = '\"';
+    $replacements["\\"] = "\\\\";
+    return strtr($value, $replacements);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::phrase().
+   */
+  public static function phrase($value) {
+    return '"' . self::escapePhrase($value) . '"';
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::escapeFieldName().
+   */
+  public static function escapeFieldName($value) {
+    $value = str_replace(':', '\:', $value);
+    return $value;
+  }
+
+  /**
+   * Returns the HTTP URL for a certain servlet on the Solr server.
+   *
+   * @param $servlet
+   *   A string path to a Solr request handler.
+   * @param array $params
+   *   Additional GET parameters to append to the URL.
+   * @param $added_query_string
+   *   Additional query string to append to the URL.
+   *
+   * @return string
+   */
+  protected function constructUrl($servlet, array $params = array(), $added_query_string = NULL) {
+    // PHP's built in http_build_query() doesn't give us the format Solr wants.
+    $query_string = $this->httpBuildQuery($params);
+
+    if ($query_string) {
+      $query_string = '?' . $query_string;
+      if ($added_query_string) {
+        $query_string = $query_string . '&' . $added_query_string;
+      }
+    }
+    elseif ($added_query_string) {
+      $query_string = '?' . $added_query_string;
+    }
+
+    return $this->base_url . $servlet . $query_string;
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::getBaseUrl().
+   */
+  public function getBaseUrl() {
+    return $this->base_url;
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::setBaseUrl().
+   */
+  public function setBaseUrl($url) {
+    $this->base_url = $url;
+    unset($this->update_url);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::update().
+   */
+  public function update($rawPost, $timeout = FALSE) {
+    if (empty($this->update_url)) {
+      // Store the URL in an instance variable since many updates may be sent
+      // via a single instance of this class.
+      $this->update_url = $this->constructUrl(self::UPDATE_SERVLET, array('wt' => 'json'));
+    }
+    $options['data'] = $rawPost;
+    if ($timeout) {
+      $options['timeout'] = $timeout;
+    }
+    return $this->sendRawPost($this->update_url, $options);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::addDocuments().
+   */
+  public function addDocuments(array $documents, $overwrite = NULL, $commitWithin = NULL) {
+    $attr = '';
+
+    if (isset($overwrite)) {
+      $attr .= ' overwrite="' . ($overwrite ? 'true"' : 'false"');
+    }
+    if (isset($commitWithin)) {
+      $attr .= ' commitWithin="' . ((int) $commitWithin) . '"';
+    }
+
+    $rawPost = "<add$attr>";
+    foreach ($documents as $document) {
+      if (is_object($document) && ($document instanceof SearchApiSolrDocument)) {
+        $rawPost .= $document->toXml();
+      }
+    }
+    $rawPost .= '</add>';
+
+    return $this->update($rawPost);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::commit().
+   */
+  public function commit($waitSearcher = TRUE, $timeout = 3600) {
+    return $this->optimizeOrCommit('commit', $waitSearcher, $timeout);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::deleteById().
+   */
+  public function deleteById($id, $timeout = 3600) {
+    return $this->deleteByMultipleIds(array($id), $timeout);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::deleteByMultipleIds().
+   */
+  public function deleteByMultipleIds(array $ids, $timeout = 3600) {
+    $rawPost = '<delete>';
+
+    foreach ($ids as $id) {
+      $rawPost .= '<id>' . htmlspecialchars($id, ENT_NOQUOTES, 'UTF-8') . '</id>';
+    }
+    $rawPost .= '</delete>';
+
+    return $this->update($rawPost, $timeout);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::deleteByQuery().
+   */
+  public function deleteByQuery($rawQuery, $timeout = 3600) {
+    $rawPost = '<delete><query>' . htmlspecialchars($rawQuery, ENT_NOQUOTES, 'UTF-8') . '</query></delete>';
+
+    return $this->update($rawPost, $timeout);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::optimize().
+   */
+  public function optimize($waitSearcher = TRUE, $timeout = 3600) {
+    return $this->optimizeOrCommit('optimize', $waitSearcher, $timeout);
+  }
+
+  /**
+   * Sends an commit or optimize command to the Solr server.
+   *
+   * Will be synchronous unless $waitSearcher is set to FALSE.
+   *
+   * @param $type
+   *   Either "commit" or "optimize".
+   * @param $waitSearcher
+   *   (optional) Wait until a new searcher is opened and registered as the main
+   *   query searcher, making the changes visible. Defaults to true.
+   * @param $timeout
+   *   Seconds to wait until timing out with an exception. Defaults to an hour.
+   *
+   * @return
+   *   A response object.
+   *
+   * @throws SearchApiException
+   *   If an error occurs during the service call.
+   */
+  protected function optimizeOrCommit($type, $waitSearcher = TRUE, $timeout = 3600) {
+    $waitSearcher = $waitSearcher ? '' : ' waitSearcher="false"';
+
+    if ($this->getSolrVersion() <= 3) {
+      $rawPost = "<$type$waitSearcher />";
+    }
+    else {
+      $softCommit = ' softCommit="' . $this->soft_commit . '"' ? 'true' : '';
+      $rawPost = "<$type$waitSearcher$softCommit />";
+    }
+
+    $response = $this->update($rawPost, $timeout);
+    $this->clearCache();
+
+    return $response;
+  }
+
+  /**
+   * Like PHP's built in http_build_query(), but uses rawurlencode() and no [] for repeated params.
+   */
+  protected function httpBuildQuery(array $query, $parent = '') {
+    $params = array();
+
+    foreach ($query as $key => $value) {
+      $key = ($parent ? $parent : rawurlencode($key));
+
+      // Recurse into children.
+      if (is_array($value)) {
+        $params[] = $this->httpBuildQuery($value, $key);
+      }
+      // If a query parameter value is NULL, only append its key.
+      elseif (!isset($value)) {
+        $params[] = $key;
+      }
+      else {
+        $params[] = $key . '=' . rawurlencode($value);
+      }
+    }
+
+    return implode('&', $params);
+  }
+
+  /**
+   * Implements SearchApiSolrConnectionInterface::search().
+   */
+  public function search($query = NULL, array $params = array(), $method = 'GET') {
+    // Always use JSON. See
+    // http://code.google.com/p/solr-php-client/issues/detail?id=6#c1 for
+    // reasoning.
+    $params['wt'] = 'json';
+    // Additional default params.
+    $params += array(
+      'json.nl' => self::NAMED_LIST_FORMAT,
+    );
+    if ($query) {
+      $params['q'] = $query;
+    }
+    // PHP's built-in http_build_query() doesn't give us the format Solr wants.
+    $queryString = $this->httpBuildQuery($params);
+
+    if ($this->method == 'GET') {
+      $searchUrl = $this->constructUrl(self::SEARCH_SERVLET, array(), $queryString);
+      return $this->sendRawGet($searchUrl);
+    }
+    else if ($this->method == 'POST') {
+      $searchUrl = $this->constructUrl(self::SEARCH_SERVLET);
+      $options['data'] = $queryString;
+      $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
+      return $this->sendRawPost($searchUrl, $options);
+    }
+  }
+}
diff --git a/includes/solr_connection.interface.inc b/includes/solr_connection.interface.inc
new file mode 100644
index 0000000..d5fe41b
--- /dev/null
+++ b/includes/solr_connection.interface.inc
@@ -0,0 +1,319 @@
+<?php
+
+/**
+ * The interface for a Solr connection class.
+ */
+interface SearchApiSolrConnectionInterface {
+
+  /**
+   * Constructs a Solr connection objects.
+   *
+   * @param array $options
+   *   An array containing construction arguments.
+   */
+  public function __construct(array $options);
+
+  /**
+   * Calls the /admin/ping servlet, to test the connection to the server.
+   *
+   * @param int|false $timeout
+   *   Maximum time to wait for ping in seconds, -1 for unlimited (default 2).
+   *
+   * @return float|false
+   *   Seconds taken to ping the server, FALSE if timeout occured.
+   */
+  public function ping($timeout = 2);
+
+  /**
+   * Gets information about the Solr Core.
+   *
+   * @return array
+   *   An array with system information.
+   */
+  public function getSystemInfo();
+
+  /**
+   * Get metadata about fields in the Solr/Lucene index.
+   *
+   * @param int $num_terms
+   *   Number of 'top terms' to return.
+   *
+   * @return array
+   *   An array of SearchApiSolrField objects.
+   */
+  public function getFields($num_terms = 0);
+
+  /**
+   * Gets meta-data about the index.
+   *
+   * @param int $num_terms
+   *   Number of 'top terms' to return.
+   *
+   * @return object
+   *   A response object filled with data from Solr's Luke.
+   */
+  public function getLuke($num_terms = 0);
+
+  /**
+   * Gets information about the Solr core.
+   *
+   * @return SimpleXMLElement
+   *   A Simple XMl document.
+   */
+  public function getStats();
+
+  /**
+   * Gets summary information about the Solr Core.
+   */
+  public function getStatsSummary();
+
+  /**
+   * Clears the cached Solr data.
+   */
+  public function clearCache();
+
+  /**
+   * Makes a request to a servlet (a path) that's not a standard path.
+   *
+   * @param string $servlet
+   *   A path to be added to the base Solr path. e.g. 'extract/tika'.
+   * @param array $params
+   *   Any request parameters when constructing the URL.
+   * @param array $options
+   *  Options to be passed to drupal_http_request().
+   *
+   * @return object
+   *  The HTTP response object.
+   *
+   * @throws Exception
+   */
+  public function makeServletRequest($servlet, array $params = array(), array $options = array());
+
+  /**
+   * Gets the base URL of the Solr server.
+   *
+   * @return string
+   *   The base URL of the Solr server.
+   */
+  public function getBaseUrl();
+
+  /**
+   * Sets the base URL of the Solr server.
+   *
+   * @param string $url
+   *   The new base URL of the Solr server.
+   */
+  public function setBaseUrl($url);
+
+  /**
+   * Sends a raw update request to the Solr server.
+   *
+   * Takes a raw post body and sends it to the update service. Post body should
+   * be a complete and well-formed XML document.
+   *
+   * @param string $rawPost
+   *   The XML document to send to the Solr server's update service.
+   * @param int|false $timeout
+   *   (optional) Maximum expected duration (in seconds). Defaults to not timing
+   *   out.
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call
+   */
+  public function update($rawPost, $timeout = FALSE);
+
+  /**
+   * Adds an array of Solr Documents to the index all at once
+   *
+   * @param array $documents
+   *   Should be an array of ApacheSolrDocument instances
+   * @param bool $overwrite
+   *   (optional) Set whether existing documents with the same IDs should be
+   *   overwritten. Defaults to TRUE.
+   * @param bool $commitWithin
+   *   (optional) The time in which the indexed documents should be committed to
+   *   the index, in milliseconds. This works in addition to the Solr server's
+   *   auto commit settings. Defaults to no additional handling.
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call.
+   */
+  public function addDocuments(array $documents, $overwrite = NULL, $commitWithin = NULL);
+
+  /**
+   * Sends a commit command to the Solr server.
+   *
+   * Will be synchronous unless $waitSearcher is set to FALSE.
+   *
+   * @param bool $waitSearcher
+   *   (optional) Wait until a new searcher is opened and registered as the main
+   *   query searcher, making the changes visible. Defaults to true.
+   * @param int|false $timeout
+   *   Seconds to wait until timing out with an exception. Defaults to an hour.
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call.
+   */
+  public function commit($waitSearcher = TRUE, $timeout = 3600);
+
+  /**
+   * Sends a delete request based on a document ID.
+   *
+   * @param string $id
+   *   The ID of the document which should be deleted. Expected to be UTF-8
+   *   encoded.
+   * @param int|false $timeout
+   *   Seconds to wait until timing out with an exception. Defaults to an hour.
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call.
+   */
+  public function deleteById($id, $timeout = 3600);
+
+  /**
+   * Sends a delete request for several documents, based on the document IDs.
+   *
+   * @param array $id
+   *   The IDs of the documents which should be deleted. Expected to be UTF-8
+   *   encoded.
+   * @param int|false $timeout
+   *   Seconds to wait until timing out with an exception. Defaults to an hour.
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call.
+   */
+  public function deleteByMultipleIds(array $ids, $timeout = 3600);
+
+  /**
+   * Sends a delete request for all documents that match the given Solr query.
+   *
+   * @param string $rawQuery
+   *   The query whose results should be deleted. Expected to be UTF-8 encoded.
+   * @param int|false $timeout
+   *   Seconds to wait until timing out with an exception. Defaults to an hour.
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call.
+   */
+  public function deleteByQuery($rawQuery, $timeout = 3600);
+
+  /**
+   * Sends an optimize command to the Solr server.
+   *
+   * Will be synchronous unless $waitSearcher is set to FALSE.
+   *
+   * @param bool $waitSearcher
+   *   (optional) Wait until a new searcher is opened and registered as the main
+   *   query searcher, making the changes visible. Defaults to true.
+   * @param int|false $timeout
+   *   Seconds to wait until timing out with an exception. Defaults to an hour.
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call.
+   */
+  public function optimize($waitSearcher = TRUE, $timeout = 3600);
+
+  /**
+   * Executes a search on the Solr server.
+   *
+   * @param string|null $query
+   *   (optional) The raw query string. Defaults to an empty query.
+   * @param array $params
+   *   (optional) Key / value pairs for other query parameters (see Solr
+   *   documentation). Use arrays for parameter keys used more than once (e.g.,
+   *   facet.field).
+   * @param string $method
+   *   The HTTP method to use. Must be either "GET" or "POST". Defaults to
+   *   "GET".
+   *
+   * @return object
+   *   A response object.
+   *
+   * @throws Exception
+   *   If an error occurs during the service call.
+   */
+  public function search($query = NULL, array $params = array(), $method = 'GET');
+
+  /**
+   * Escapes special characters from a Solr query.
+   *
+   * A complete list of special characters in Solr queries can be viewed at
+   * http://lucene.apache.org/java/docs/queryparsersyntax.html#Escaping%20Special%20Characters
+   *
+   * @param string $value
+   *   The string to escape.
+   *
+   * @return string
+   *   An escaped string suitable for passing to Solr.
+   */
+  static public function escape($value);
+
+  /**
+   * Escapes a string that should be included in a Solr phrase.
+   *
+   * In contrast to escape(), this only escapes '"' and '\'.
+   *
+   * @param string $value
+   *   The string to escape.
+   *
+   * @return string
+   *   An escaped string suitable for passing to Solr.
+   */
+  static public function escapePhrase($value);
+
+  /**
+   * Converts a string to a Solr phrase.
+   *
+   * @param string $value
+   *   The string to convert to a phrase.
+   *
+   * @return string
+   *   A phrase string suitable for passing to Solr.
+   */
+  static public function phrase($value);
+
+  /**
+   * Escapes a Search API field name for passing to Solr.
+   *
+   * Since field names can only contain one special character, ":", there is no
+   * need to use the complete escape() method.
+   *
+   * @param string $value
+   *   The field name to escape.
+   *
+   * @return string
+   *   An escaped string suitable for passing to Solr.
+   */
+  public static function escapeFieldName($value);
+
+  /**
+   * Gets the current solr version.
+   *
+   * @return int
+   *   1, 3 or 4. Does not give a more detailed version, for that you need to
+   *   use getSystemInfo().
+   */
+  public function getSolrVersion();
+
+}
diff --git a/includes/solr_field.inc b/includes/solr_field.inc
new file mode 100644
index 0000000..1274251
--- /dev/null
+++ b/includes/solr_field.inc
@@ -0,0 +1,271 @@
+<?php
+
+/**
+ * Logic around Solr field schema information.
+ */
+class SearchApiSolrField {
+
+  /**
+   * @var array
+   *   Human-readable labels for Solr schema properties.
+   */
+  public static $schemaLabels = array(
+    'I' => 'Indexed',
+    'T' => 'Tokenized',
+    'S' => 'Stored',
+    'M' => 'Multivalued',
+    'V' => 'TermVector Stored',
+    'o' => 'Store Offset With TermVector',
+    'p' => 'Store Position With TermVector',
+    'O' => 'Omit Norms',
+    'L' => 'Lazy',
+    'B' => 'Binary',
+    'C' => 'Compressed',
+    'f' => 'Sort Missing First',
+    'l' => 'Sort Missing Last',
+  );
+
+  /**
+   * @var stdclass
+   *   The original field object.
+   */
+  protected $field;
+
+  /**
+   * @var array
+   *   An array of schema properties for this field. This will be a subset of
+   *   the SearchApiSolrField::schemaLabels array.
+   */
+  protected $schema;
+
+  /**
+   * Constructs a field information object.
+   *
+   * @param stdClass $field
+   *   A field object from Solr's "Luke" servlet.
+   */
+  public function __construct($field) {
+    $this->field = $field;
+  }
+
+  /**
+   * Gets the type of the Solr field, according to the Solr schema.
+   *
+   * Note that field types like "text", "boolean", and "date" are conventions,
+   * but their presence and behavior are entirely determined by the particular
+   * schema.xml file used by a Solr core.
+   *
+   * @return string
+   *   The type of the Solr field.
+   */
+  public function getType() {
+    return $this->field->type;
+  }
+
+  /**
+   * Gets an array of field properties.
+   *
+   * @return array
+   *   An array of properties describing the solr schema. The array keys are
+   *   single-character codes, and the values are human-readable labels. This
+   *   will be a subset of the SearchApiSolrField::schemaLabels array.
+   */
+  public function getSchema() {
+    if (!isset($this->schema)) {
+      foreach (str_split(str_replace('-', '', $this->field->schema)) as $key) {
+        $this->schema[$key] = isset(self::$schemaLabels[$key]) ? self::$schemaLabels[$key] : $key;
+      }
+    }
+    return $this->schema;
+  }
+
+  /**
+   * Gets the "dynamic base" of this field.
+   *
+   * This typically looks like 'ss_*, and is used to aggregate fields based on
+   * "hungarian" naming conventions.
+   *
+   * @return string
+   *   The mask describing the solr aggregate field, if there is one.
+   */
+  public function getDynamicBase() {
+    return isset($this->field->dynamicBase) ? $this->field->dynamicBase : NULL;
+  }
+
+  /**
+   * Determines whether this field may be suitable for use as a key field.
+   *
+   * Unfortunately, it seems like the best way to find an actual uniqueKey field
+   * according to Solr is to examine the Solr core's schema.xml.
+   *
+   * @return bool
+   *   Whether the field is suitable for use as a key.
+   */
+  public function isPossibleKey() {
+    return !$this->getDynamicBase()
+      && !in_array($this->getType(), array('boolean', 'date', 'text'))
+      && $this->isStored()
+      && !$this->isMultivalued();
+  }
+
+  /**
+   * Determines whether a field is suitable for sorting.
+   *
+   * In order for a field to yield useful sorted results in Solr, it must be
+   * indexed, not multivalued, and not tokenized. It's ok if a field is
+   * tokenized and yields only one token, but there's no general way to check
+   * for that.
+   *
+   * @return bool
+   *   Whether the field is suitable for sorting.
+   */
+  public function isSortable() {
+    return $this->isIndexed()
+      && !$this->isMultivalued()
+      && !$this->isTokenized();
+  }
+
+  /**
+   * Determines whether this field is indexed.
+   *
+   * @return bool
+   *   TRUE if the field is indexed, FALSE otherwise.
+   */
+  public function isIndexed() {
+    $this->getSchema();
+    return isset($this->schema['I']);
+  }
+
+  /**
+   * Determines whether this field is tokenized.
+   *
+   * @return bool
+   *   TRUE if the field is tokenized, FALSE otherwise.
+   */
+  public function isTokenized() {
+    $this->getSchema();
+    return isset($this->schema['T']);
+  }
+
+  /**
+   * Determines whether this field is stored.
+   *
+   * @return bool
+   *   TRUE if the field is stored, FALSE otherwise.
+   */
+  public function isStored() {
+    $this->getSchema();
+    return isset($this->schema['S']);
+  }
+
+  /**
+   * Determines whether this field is multi-valued.
+   *
+   * @return bool
+   *   TRUE if the field is multi-valued, FALSE otherwise.
+   */
+  public function isMultivalued() {
+    $this->getSchema();
+    return isset($this->schema['M']);
+  }
+
+  /**
+   * Determines whether this field has stored term vectors.
+   *
+   * @return bool
+   *   TRUE if the field has stored term vectors, FALSE otherwise.
+   */
+  public function isTermVectorStored() {
+    $this->getSchema();
+    return isset($this->schema['V']);
+  }
+
+  /**
+   * Determines whether this field has the "termOffsets" option set.
+   *
+   * @return bool
+   *   TRUE if the field has the "termOffsets" option set, FALSE otherwise.
+   */
+  public function isStoreOffsetWithTermVector() {
+    $this->getSchema();
+    return isset($this->schema['o']);
+  }
+
+  /**
+   * Determines whether this field has the "termPositions" option set.
+   *
+   * @return bool
+   *   TRUE if the field has the "termPositions" option set, FALSE otherwise.
+   */
+  public function isStorePositionWithTermVector() {
+    $this->getSchema();
+    return isset($this->schema['p']);
+  }
+
+  /**
+   * Determines whether this field omits norms when indexing.
+   *
+   * @return bool
+   *   TRUE if the field omits norms, FALSE otherwise.
+   */
+  public function isOmitNorms() {
+    $this->getSchema();
+    return isset($this->schema['O']);
+  }
+
+  /**
+   * Determines whether this field is lazy-loaded.
+   *
+   * @return bool
+   *   TRUE if the field is lazy-loaded, FALSE otherwise.
+   */
+  public function isLazy() {
+    $this->getSchema();
+    return isset($this->schema['L']);
+  }
+
+  /**
+   * Determines whether this field is binary.
+   *
+   * @return bool
+   *   TRUE if the field is binary, FALSE otherwise.
+   */
+  public function isBinary() {
+    $this->getSchema();
+    return isset($this->schema['B']);
+  }
+
+  /**
+   * Determines whether this field is compressed.
+   *
+   * @return bool
+   *   TRUE if the field is compressed, FALSE otherwise.
+   */
+  public function isCompressed() {
+    $this->getSchema();
+    return isset($this->schema['C']);
+  }
+
+  /**
+   * Determines whether this field sorts missing entries first.
+   *
+   * @return bool
+   *   TRUE if the field sorts missing entries first, FALSE otherwise.
+   */
+  public function isSortMissingFirst() {
+    $this->getSchema();
+    return isset($this->schema['f']);
+  }
+
+  /**
+   * Determines whether this field sorts missing entries last.
+   *
+   * @return bool
+   *   TRUE if the field sorts missing entries last, FALSE otherwise.
+   */
+  public function isSortMissingLast() {
+    $this->getSchema();
+    return isset($this->schema['l']);
+  }
+
+}
diff --git a/includes/solr_httptransport.inc b/includes/solr_httptransport.inc
deleted file mode 100644
index bf6f9c9..0000000
--- a/includes/solr_httptransport.inc
+++ /dev/null
@@ -1,100 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains the SearchApiSolrHttpTransport class.
- */
-
-/**
- * Drupal-based implementation of the HTTP transport interface.
- *
- * Uses drupal_http_request() for sending the request.
- */
-class SearchApiSolrHttpTransport extends Apache_Solr_HttpTransport_Abstract {
-
-  /**
-   * If set, an HTTP authentification string to use.
-   *
-   * @var string
-   */
-  protected $http_auth;
-
-  /**
-   * Constructor.
-   *
-   * @param $http_auth
-   *   If set, an HTTP authentification string to use.
-   */
-  public function __construct($http_auth = NULL) {
-    $this->http_auth = $http_auth;
-  }
-
-  /**
-   * Perform a GET HTTP operation with an optional timeout and return the response
-   * contents, use getLastResponseHeaders to retrieve HTTP headers
-   *
-   * @param string $url
-   * @param float $timeout
-   * @return Apache_Solr_HttpTransport_Response HTTP response
-   */
-  public function performGetRequest($url, $timeout = false) {
-    return $this->performHttpRequest('GET', $url, $timeout);
-  }
-
-  /**
-   * Perform a HEAD HTTP operation with an optional timeout and return the response
-   * headers - NOTE: head requests have no response body
-   *
-   * @param string $url
-   * @param float $timeout
-   * @return Apache_Solr_HttpTransport_Response HTTP response
-   */
-  public function performHeadRequest($url, $timeout = false) {
-    return $this->performHttpRequest('HEAD', $url, $timeout);
-  }
-
-  /**
-   * Perform a POST HTTP operation with an optional timeout and return the response
-   * contents, use getLastResponseHeaders to retrieve HTTP headers
-   *
-   * @param string $url
-   * @param string $rawPost
-   * @param string $contentType
-   * @param float $timeout
-   * @return Apache_Solr_HttpTransport_Response HTTP response
-   */
-  public function performPostRequest($url, $rawPost, $contentType, $timeout = false) {
-    return $this->performHttpRequest('POST', $url, $timeout, $rawPost, $contentType);
-  }
-
-  /**
-   * Helper method for making an HTTP request.
-   */
-  protected function performHttpRequest($method, $url, $timeout, $rawPost = NULL, $contentType = NULL) {
-    $options = array(
-      'method' => $method,
-      'timeout' => $timeout && $timeout > 0 ? $timeout : $this->getDefaultTimeout(),
-      'headers' => array(),
-    );
-
-    if ($this->http_auth) {
-      $options['headers']['Authorization'] = $this->http_auth;
-    }
-    if ($timeout) {
-      $options['timeout'] = $timeout;
-    }
-    if ($rawPost) {
-      $options['data'] = $rawPost;
-    }
-    if ($contentType) {
-      $options['headers']['Content-Type'] = $contentType;
-    }
-
-    $response = drupal_http_request($url, $options);
-
-    $type = isset($response->headers['content-type']) ? $response->headers['content-type'] : 'text/xml';
-    $body = isset($response->data) ? $response->data : NULL;
-    return new Apache_Solr_HttpTransport_Response($response->code, $type, $body);
-  }
-
-}
diff --git a/includes/spellcheck.inc b/includes/spellcheck.inc
index 3e89ce0..58875b3 100644
--- a/includes/spellcheck.inc
+++ b/includes/spellcheck.inc
@@ -11,15 +11,15 @@
 class SearchApiSpellcheckSolr extends SearchApiSpellcheck {
 
   /**
-   * Constructor.
+   * Constructs a SearchApiSpellcheckSolr object.
    *
-   * If solr has returned spelling suggestion then loop through them and add
+   * If Solr has returned spelling suggestion then loop through them and add
    * them to this spellcheck service.
    *
-   * @param Apache_Solr_Response $response
+   * @param object $response
    *   The Solr response object.
    */
-  function __construct(Apache_Solr_Response $response) {
+  function __construct($response) {
     if (isset($response->spellcheck->suggestions)) {
       $suggestions = $response->spellcheck->suggestions;
       foreach ($suggestions as $word => $data) {
diff --git a/search_api_solr.api.php b/search_api_solr.api.php
index f407407..b3e83d8 100644
--- a/search_api_solr.api.php
+++ b/search_api_solr.api.php
@@ -18,9 +18,9 @@
  * is set afterwards.
  *
  * @param array $call_args
- *   An associative array containing all four arguments to the
- *   Apache_Solr_Service::search() call ("query", "offset", "limit" and
- *   "params") as references.
+ *   An associative array containing all three arguments to the
+ *   SearchApiSolrConnectionInterface::search() call ("query", "params" and
+ *   "method") as references.
  * @param SearchApiQueryInterface $query
  *   The SearchApiQueryInterface object representing the executed search query.
  */
@@ -33,7 +33,7 @@ function hook_search_api_solr_query_alter(array &$call_args, SearchApiQueryInter
 /**
  * Change the way the index's field names are mapped to Solr field names.
  *
- * @param $index
+ * @param SearchApiIndex $index
  *   The index whose field mappings are altered.
  * @param array $fields
  *   An associative array containing the index field names mapped to their Solr
@@ -47,34 +47,32 @@ function hook_search_api_solr_field_mapping_alter(SearchApiIndex $index, array &
 }
 
 /**
- * Lets modules alter the search results returned from a Solr search, based on
- * the original Solr response.
+ * Lets modules alter the search results returned from a Solr search.
  *
  * @param array $results
  *   The results array that will be returned for the search.
  * @param SearchApiQueryInterface $query
  *   The SearchApiQueryInterface object representing the executed search query.
- * @param Apache_Solr_Response $response
- *   The response object returned by Solr.
+ * @param object $response
+ *   The Solr response object.
  */
-function hook_search_api_solr_search_results_alter(array &$results, SearchApiQueryInterface $query, Apache_Solr_Response $response) {
+function hook_search_api_solr_search_results_alter(array &$results, SearchApiQueryInterface $query, $response) {
   if (isset($response->facet_counts->facet_fields->custom_field)) {
     // Do something with $results.
   }
 }
 
 /**
- * Lets modules alter a Solr search request for a multi-index search before
- * sending it.
+ * Lets modules alter a Solr search request for a multi-index search.
  *
- * Apache_Solr_Service::search() is called afterwards with these parameters.
- * Please see this method for details on what should be altered where and what
- * is set afterwards.
+ * SearchApiSolrConnectionInterface::search() is called afterwards with these
+ * parameters. Please see this method for details on what should be altered
+ * where and what is set afterwards.
  *
  * @param array $call_args
- *   An associative array containing all four arguments to the
- *   Apache_Solr_Service::search() call ("query", "offset", "limit" and
- *   "params") as references.
+ *   An associative array containing all three arguments to the
+ *   SearchApiSolrConnectionInterface::search() call ("query", "params" and
+ *   "method") as references.
  * @param SearchApiMultiQueryInterface $query
  *   The object representing the executed search query.
  */
diff --git a/search_api_solr.info b/search_api_solr.info
index ffb7d4b..a7c272a 100644
--- a/search_api_solr.info
+++ b/search_api_solr.info
@@ -4,8 +4,9 @@ dependencies[] = search_api
 core = 7.x
 package = Search
 
-files[] = service.inc
-files[] = solr_connection.inc
-files[] = solr_field.inc
-files[] = includes/solr_httptransport.inc
+files[] = includes/document.inc
+files[] = includes/service.inc
+files[] = includes/solr_connection.inc
+files[] = includes/solr_connection.interface.inc
+files[] = includes/solr_field.inc
 files[] = includes/spellcheck.inc
diff --git a/search_api_solr.install b/search_api_solr.install
index 60270c5..b1ad5d4 100644
--- a/search_api_solr.install
+++ b/search_api_solr.install
@@ -1,44 +1,21 @@
 <?php
 
 /**
+ * Implements hook_schema().
+ */
+function search_api_solr_schema() {
+  // See, e.g., block_schema() for this trick. Seems to be the best way to get a
+  // cache table definition.
+  $schema['cache_search_api_solr'] = drupal_get_schema_unprocessed('system', 'cache');
+  $schema['cache_search_api_solr']['description'] = 'Cache table for the Search API Solr module to store various data related to Solr servers.';
+  return $schema;
+}
+
+/**
  * Implements hook_requirements().
  */
 function search_api_solr_requirements($phase) {
-  if ($phase == 'install') {
-    $t = get_t();
-    module_load_include('module', 'search_api_solr');
-    spl_autoload_register('_search_api_solr_autoload');
-    if (class_exists('Apache_Solr_Service')) {
-      $version = trim(Apache_Solr_Service::SVN_REVISION, '$ :A..Za..z');
-      if ($version < 59) {
-        return array(
-          'search_api_solr' => array(
-            'title' => $t('Solr PHP library'),
-            'value' => $t('The library is correctly installed, but out of date'),
-            'description' => $t('It is suggested to install the newest version (@version).', array('@version' => 'r60')),
-            'severity' => REQUIREMENT_WARNING,
-          ),
-        );
-      }
-      return array(
-        'search_api_solr' => array(
-          'title' => $t('Solr PHP library'),
-          'value' => $t('The library was correctly installed'),
-          'severity' => REQUIREMENT_OK,
-        ),
-      );
-    }
-    else {
-      return array(
-        'search_api_solr' => array(
-          'title' => $t('Solr PHP library'),
-          'value' => $t('The library was not correctly installed. Please see INSTALL.txt for instructions.'),
-          'severity' => REQUIREMENT_ERROR,
-        ),
-      );
-    }
-  }
-  elseif ($phase == 'runtime') {
+  if ($phase == 'runtime') {
     $servers = search_api_server_load_multiple(FALSE, array('class' => 'search_api_solr_service', 'enabled' => TRUE));
     $count = 0;
     $unavailable = 0;
@@ -72,18 +49,6 @@ function search_api_solr_requirements($phase) {
       $ret['search_api_solr']['severity'] = REQUIREMENT_OK;
     }
 
-    // Check version of the SolrPhpClient library.
-    $version = trim(Apache_Solr_Service::SVN_REVISION, '$ :A..Za..z');
-    if ($version < 59) {
-      $ret['search_api_solr_client'] = array(
-        'title' => t('Solr PHP library'),
-        'value' => t('Version @version', array('@version' => "r$version")),
-        'description' => t('The library is correctly installed, but out of date. ' .
-            'It is suggested to install the <a href="@url">newest version</a> (@version).',
-            array('@url' => 'http://code.google.com/p/solr-php-client/downloads/list', '@version' => 'r60')),
-        'severity' => REQUIREMENT_WARNING,
-      );
-    }
     return $ret;
   }
 }
@@ -123,8 +88,6 @@ function search_api_solr_update_dependencies() {
 }
 
 /**
- * Implements hook_update_N().
- *
  * Implements transition from using the index IDs to using machine names.
  */
 function search_api_solr_update_7101() {
@@ -152,3 +115,14 @@ function search_api_solr_update_7101() {
       'Please stop your Solr servers, replace their schema.xml with the new version and then start them again. ' .
       'All data indexed on Solr servers will have to be reindexed.');
 }
+
+/**
+ * Create the Search API Solr cache table {cache_search_api_solr}.
+ */
+function search_api_solr_update_7102() {
+  if (!db_table_exists('cache_search_api_solr')) {
+    $table = drupal_get_schema_unprocessed('system', 'cache');
+    $table['description'] = 'Cache table for the Search API Solr module to store various data related to Solr servers.';
+    db_create_table('cache_search_api_solr', $table);
+  }
+}
diff --git a/search_api_solr.module b/search_api_solr.module
index 9e30397..ffeb83c 100644
--- a/search_api_solr.module
+++ b/search_api_solr.module
@@ -1,79 +1,11 @@
 <?php
+
 /**
  * @file
  * Provides a Solr-based service class for the Search API.
  */
 
 /**
- * Implements hook_init().
- */
-function search_api_solr_init() {
-  spl_autoload_register('_search_api_solr_autoload');
-}
-
-/**
- * Return path to SolrPhpClient library path, or FALSE if not found.
- */
-function _search_api_solr_solrphpclient_path() {
-  static $path = NULL;
-
-  if (!isset($path)) {
-    $path = FALSE;
-    // If Libraries API is installed, we first use that to try and find the
-    // library. Otherwise we manually check a few locations.
-    $search_dirs = array();
-    if (function_exists('libraries_get_path')) {
-      $dir = libraries_get_path('SolrPhpClient');
-      // Confusingly, Libraries API 1.x will return sites/all/libraries/NAME on
-      // failure, while Libraries API 2.x returns FALSE in that case.
-      if ($dir) {
-        $search_dirs[] = $dir;
-      }
-    }
-    else {
-      // Include libraries + current profile folders in searched directories.
-      $search_dirs[] = 'sites/all/libraries/SolrPhpClient';
-      $search_dirs[] = 'profiles/' . drupal_get_profile() . '/libraries/SolrPhpClient';
-    }
-    $search_dirs[] = drupal_get_path('module', 'search_api_solr') . '/SolrPhpClient';
-    foreach ($search_dirs as $dir) {
-      $dir = DRUPAL_ROOT . '/' . $dir;
-      if (is_dir($dir)) {
-        $path = $dir;
-        break;
-      }
-    }
-  }
-
-  if ($path == FALSE) {
-    throw new Exception('SolrPhpClient library not found! Please follow the instructions in search_api_solr/INSTALL.txt for installing the Solr search module.');
-  }
-
-  return $path;
-}
-
-/**
- * Autoloader for the SolrPhpClient classes.
- */
-function _search_api_solr_autoload($name) {
-  static $lookup_cache = array();
-
-  if (isset($lookup_cache[$name])) {
-    return $lookup_cache[$name];
-  }
-  elseif (substr($name, 0, 11) == 'Apache_Solr') {
-    $path = _search_api_solr_solrphpclient_path();
-    if (file_exists($file_path = $path . '/' . str_replace('_', '/', $name) . '.php')) {
-      require_once $file_path;
-      $lookup_cache[$name] = TRUE;
-      return TRUE;
-    }
-  }
-  $lookup_cache[$name] = FALSE;
-  return FALSE;
-}
-
-/**
  * Implements hook_search_api_service_info().
  */
 function search_api_solr_search_api_service_info() {
@@ -143,3 +75,20 @@ function search_api_solr_cron() {
     }
   }
 }
+
+/**
+ * Implements hook_flush_caches().
+ */
+function search_api_solr_flush_caches() {
+  return array('cache_search_api_solr');
+}
+
+
+/**
+ * Implements hook_search_api_server_update().
+ */
+function search_api_solr_search_api_server_update(SearchApiServer $server) {
+  if ($server->class === 'search_api_solr_service') {
+    $server->getSolrConnection()->clearCache();
+  }
+}
diff --git a/service.inc b/service.inc
deleted file mode 100644
index 9047b08..0000000
--- a/service.inc
+++ /dev/null
@@ -1,1655 +0,0 @@
-<?php
-
-/**
- * Search service class using Solr server.
- */
-class SearchApiSolrService extends SearchApiAbstractService {
-
-  /**
-   * The date format that Solr uses, in PHP date() syntax.
-   */
-  const SOLR_DATE_FORMAT = 'Y-m-d\TH:i:s\Z';
-
-  /**
-   * A connection to the Solr server.
-   *
-   * @var SearchApiSolrConnection
-   */
-  protected $solr;
-
-  /**
-   * An array of all recognized types mapped to a prefix used for identifying
-   * them in the Solr schema.
-   *
-   * @var array
-   */
-  protected static $type_prefixes = array(
-    'text' => 'tm',
-    'tokens' => 'tm',
-    'string' => 's',
-    'integer' => 'i',
-    'decimal' => 'f',
-    'date' => 'd',
-    'duration' => 'i',
-    'boolean' => 'b',
-    'uri' => 's',
-    'location' => 'loc',
-    'geohash' => 'geohash',
-  );
-
-  /**
-   * @var array
-   */
-  protected $fieldNames = array();
-
-  /**
-   * Metadata describing fields on the Solr/Lucene index.
-   *
-   * @see SearchApiSolrService::getFields().
-   *
-   * @var array
-   */
-  protected $fields;
-
-  /**
-   * Saves whether a commit operation was already scheduled for this server.
-   *
-   * @var boolean
-   */
-  protected $commitScheduled = FALSE;
-
-  /**
-   * Request handler to use for this search query.
-   *
-   * @var string
-   */
-  protected $request_handler = NULL;
-
-  public function __construct(SearchApiServer $server) {
-    parent::__construct($server);
-  }
-
-  public function configurationForm(array $form, array &$form_state) {
-    if ($this->options) {
-      // Editing this server
-      $url = 'http://' . $this->options['host'] . ':' . $this->options['port'] . $this->options['path'];
-      $form['server_description'] = array(
-        '#type' => 'item',
-        '#title' => t('Solr server URI'),
-        '#description' => l($url, $url),
-      );
-    }
-
-    $options = $this->options + array(
-      'host' => 'localhost',
-      'port' => '8983',
-      'path' => '/solr',
-      'http_user' => '',
-      'http_pass' => '',
-      'excerpt' => FALSE,
-      'retrieve_data' => FALSE,
-      'highlight_data' => FALSE,
-      'http_method' => Apache_Solr_Service::METHOD_POST,
-      'autocorrect_spell' => TRUE,
-      'autocorrect_suggest_words' => TRUE,
-    );
-
-    $form['host'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Solr host'),
-      '#description' => t('The host name or IP of your Solr server, e.g. <code>localhost</code> or <code>www.example.com</code>.'),
-      '#default_value' => $options['host'],
-      '#required' => TRUE,
-    );
-    $form['port'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Solr port'),
-      '#description' => t('The Jetty example server is at port 8983, while Tomcat uses 8080 by default.'),
-      '#default_value' => $options['port'],
-      '#required' => TRUE,
-    );
-    $form['path'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Solr path'),
-      '#description' => t('The path that identifies the Solr instance to use on the server.'),
-      '#default_value' => $options['path'],
-    );
-
-    $form['http'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Basic HTTP authentication'),
-      '#description' => t('If your Solr server is protected by basic HTTP authentication, enter the login data here.'),
-      '#collapsible' => TRUE,
-      '#collapsed' => empty($options['http_user']),
-    );
-    $form['http']['http_user'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Username'),
-      '#default_value' => $options['http_user'],
-    );
-    $form['http']['http_pass'] = array(
-      '#type' => 'password',
-      '#title' => t('Password'),
-      '#default_value' => $options['http_pass'],
-    );
-
-    $form['advanced'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Advanced'),
-      '#collapsible' => TRUE,
-      '#collapsed' => TRUE,
-    );
-    $form['advanced']['excerpt'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Return an excerpt for all results'),
-      '#description' => t("If search keywords are given, use Solr's capabilities to create a highlighted search excerpt for each result. " .
-          'Whether the excerpts will actually be displayed depends on the settings of the search, though.'),
-      '#default_value' => $options['excerpt'],
-    );
-    $form['advanced']['retrieve_data'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Retrieve result data from Solr'),
-      '#description' => t('When checked, result data will be retrieved directly from the Solr server. ' .
-          'This might make item loads unnecessary. Only indexed fields can be retrieved. ' .
-          'Note also that the returned field data might not always be correct, due to preprocessing and caching issues.'),
-      '#default_value' => $options['retrieve_data'],
-    );
-    $form['advanced']['highlight_data'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Highlight retrieved data'),
-      '#description' => t('When retrieving result data from the Solr server, try to highlight the search terms in the returned fulltext fields.'),
-      '#default_value' => $options['highlight_data'],
-    );
-    // Highlighting retrieved data only makes sense when we retrieve data.
-    // (Actually, internally it doesn't really matter. However, from a user's
-    // perspective, having to check both probably makes sense.)
-    $form['advanced']['highlight_data']['#states']['invisible']
-        [':input[name="options[form][advanced][retrieve_data]"]']['checked'] = FALSE;
-
-    $form['advanced']['http_method'] = array(
-      '#type' => 'select',
-      '#title' => t('HTTP method'),
-      '#description' => t('The HTTP method to use for sending queries. Usually, POST will work fine in all cases.'),
-      '#default_value' => $options['http_method'],
-      '#options' => array(
-        Apache_Solr_Service::METHOD_POST => 'POST',
-        Apache_Solr_Service::METHOD_GET => 'GET',
-      ),
-    );
-
-    if (module_exists('search_api_autocomplete')) {
-      $form['advanced']['autocomplete'] = array(
-        '#type' => 'fieldset',
-        '#title' => t('Autocomplete'),
-        '#collapsible' => TRUE,
-        '#collapsed' => TRUE,
-      );
-      $form['advanced']['autocomplete']['autocorrect_spell'] = array(
-        '#type' => 'checkbox',
-        '#title' => t('Use spellcheck for autocomplete suggestions'),
-        '#description' => t('If activated, spellcheck suggestions ("Did you mean") will be included in the autocomplete suggestions. Since the used dictionary contains words from all indexes, this might lead to leaking of sensitive data, depending on your setup.'),
-        '#default_value' => $options['autocorrect_spell'],
-      );
-      $form['advanced']['autocomplete']['autocorrect_suggest_words'] = array(
-        '#type' => 'checkbox',
-        '#title' => t('Suggest additional words'),
-        '#description' => t('If activated and the user enters a complete word, Solr will suggest additional words the user wants to search, which are often found (not searched!) together. This has been known to lead to strange results in some configurations – if you see inappropriate additional-word suggestions, you might want to deactivate this option.'),
-        '#default_value' => $options['autocorrect_suggest_words'],
-      );
-    }
-
-    return $form;
-  }
-
-  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
-    if (isset($values['port']) && (!is_numeric($values['port']) || $values['port'] < 0 || $values['port'] > 65535)) {
-      form_error($form['port'], t('The port has to be an integer between 0 and 65535.'));
-    }
-  }
-
-  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
-    // Since the form is nested into another, we can't simply use #parents for
-    // doing this array restructuring magic. (At least not without creating an
-    // unnecessary dependency on internal implementation.)
-    $values += $values['http'];
-    $values += $values['advanced'];
-    $values += !empty($values['autocomplete']) ? $values['autocomplete'] : array();
-    unset($values['http'], $values['advanced'], $values['autocomplete']);
-    // Highlighting retrieved data only makes sense when we retrieve data.
-    $values['highlight_data'] &= $values['retrieve_data'];
-
-    parent::configurationFormSubmit($form, $values, $form_state);
-  }
-
-  public function supportsFeature($feature) {
-    $supported = drupal_map_assoc(array(
-      'search_api_autocomplete',
-      'search_api_facets',
-      'search_api_facets_operator_or',
-      'search_api_mlt',
-      'search_api_multi',
-      'search_api_spellcheck',
-      'search_api_data_type_location',
-      'search_api_data_type_geohash',
-    ));
-    return isset($supported[$feature]);
-  }
-
-  /**
-   * View this server's settings.
-   */
-  public function viewSettings() {
-    $output = '';
-    $options = $this->options;
-
-    $url = 'http://' . $options['host'] . ':' . $options['port'] . $options['path'];
-    $output .= "<dl>\n  <dt>";
-    $output .= t('Solr server URI');
-    $output .= "</dt>\n  <dd>";
-    $output .= l($url, $url);
-    $output .= '</dd>';
-    if ($options['http_user']) {
-      $output .= "\n  <dt>";
-      $output .= t('Basic HTTP authentication');
-      $output .= "</dt>\n  <dd>";
-      $output .= t('Username: @user', array('@user' => $options['http_user']));
-      $output .= "</dd>\n  <dd>";
-      $output .= t('Password: @pass', array('@pass' => str_repeat('*', strlen($options['http_pass']))));
-      $output .= '</dd>';
-    }
-    $output .= "\n</dl>";
-
-    return $output;
-  }
-
-  /**
-   * Create a connection to the Solr server as configured in $this->options.
-   */
-  protected function connect() {
-    if (!$this->solr) {
-      if (!class_exists('Apache_Solr_Service')) {
-        throw new Exception(t('SolrPhpClient library not found! Please follow the instructions in search_api_solr/INSTALL.txt for installing the Solr search module.'));
-      }
-      $this->solr = new SearchApiSolrConnection($this->options);
-    }
-  }
-
-  public function addIndex(SearchApiIndex $index) {
-    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
-      views_invalidate_cache();
-    }
-  }
-
-  public function fieldsUpdated(SearchApiIndex $index) {
-    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
-      views_invalidate_cache();
-    }
-    return TRUE;
-  }
-
-  public function removeIndex($index) {
-    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
-      views_invalidate_cache();
-    }
-    $id = is_object($index) ? $index->machine_name : $index;
-    // Only delete the index's data if the index isn't read-only.
-    if (!is_object($index) || empty($index->read_only)) {
-      try {
-        $this->connect();
-        $this->solr->deleteByQuery("index_id:" . $id);
-      }
-      catch (Exception $e) {
-        watchdog_exception('search_api_solr', $e, "%type while deleting an index's data: !message in %function (line %line of %file).");
-      }
-    }
-  }
-
-  public function indexItems(SearchApiIndex $index, array $items) {
-    $documents = array();
-    $ret = array();
-    $index_id = $index->machine_name;
-    $fields = $this->getFieldNames($index);
-
-    foreach ($items as $id => $item) {
-      try {
-        $doc = new Apache_Solr_Document();
-        $doc->setField('id', $this->createId($index_id, $id));
-        $doc->setField('index_id', $index_id);
-        $doc->setField('item_id', $id);
-
-        foreach ($item as $key => $field) {
-          if (!isset($fields[$key])) {
-            throw new SearchApiException(t('Unknown field @field.', array('@field' => $key)));
-          }
-          $this->addIndexField($doc, $fields[$key], $field['value'], $field['type']);
-        }
-
-        $documents[] = $doc;
-        $ret[] = $id;
-      }
-      catch (Exception $e) {
-        watchdog_exception('search_api_solr', $e, "%type while indexing @type with ID @id: !message in %function (line %line of %file).", array('@type' => $index->item_type, '@id' => $id), WATCHDOG_WARNING);
-      }
-    }
-
-    if (!$documents) {
-      return array();
-    }
-    try {
-      $this->connect();
-      $response = $this->solr->addDocuments($documents);
-      if ($response->getHttpStatus() == 200) {
-        if (!empty($index->options['index_directly'])) {
-          $this->scheduleCommit();
-        }
-        return $ret;
-      }
-      throw new SearchApiException(t('HTTP status @status: @msg.',
-          array('@status' => $response->getHttpStatus(), '@msg' => $response->getHttpStatusMessage())));
-    }
-    catch (Exception $e) {
-      watchdog_exception('search_api_solr', $e, "%type while indexing: !message in %function (line %line of %file).");
-    }
-    return array();
-  }
-
-  /**
-   * Creates an ID used as the unique identifier at the Solr server. This has to
-   * consist of both index and item ID.
-   */
-  protected function createId($index_id, $item_id) {
-    return "$index_id-$item_id";
-  }
-
-  /**
-   * Create a list of all indexed field names mapped to their Solr field names.
-   *
-   * 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()
-   */
-  public 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' => 'ss_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) {
-        // Generate a field name; this corresponds with naming conventions in
-        // our schema.xml
-        $type = $field['type'];
-
-        // Use the real type of the field if the server supports this type.
-        if (isset($field['real_type'])) {
-          $custom_type = search_api_extract_inner_type($field['real_type']);
-          if ($this->supportsFeature('search_api_data_type_' . $custom_type)) {
-            $type = $field['real_type'];
-          }
-        }
-
-        $inner_type = search_api_extract_inner_type($type);
-        $pref = isset(self::$type_prefixes[$inner_type]) ? self::$type_prefixes[$inner_type] : '';
-        if ($pref != 'tm') {
-          $pref .= $type == $inner_type ? 's' : 'm';
-        }
-        $name = $pref . '_' . $key;
-
-        $ret[$key] = $name;
-      }
-
-      // Let modules adjust the field mappings.
-      drupal_alter('search_api_solr_field_mapping', $index, $ret);
-
-      $this->fieldNames[$index->machine_name] = $ret;
-    }
-
-    return $this->fieldNames[$index->machine_name];
-  }
-
-  /**
-   * Helper method for indexing.
-   * Add $field with field name $key to the document $doc. The format of $field
-   * is the same as specified in SearchApiServiceInterface::indexItems().
-   */
-  protected function addIndexField(Apache_Solr_Document $doc, $key, $value, $type, $multi_valued = FALSE) {
-    // Don't index empty values (i.e., when field is missing)
-    if (!isset($value)) {
-      return;
-    }
-    if (search_api_is_list_type($type)) {
-      $type = substr($type, 5, -1);
-      foreach ($value as $v) {
-        $this->addIndexField($doc, $key, $v, $type, TRUE);
-      }
-      return;
-    }
-    switch ($type) {
-      case 'tokens':
-        foreach ($value as $v) {
-          $doc->addField($key, $v['value']);
-        }
-        return;
-      case 'boolean':
-        $value = $value ? 'true' : 'false';
-        break;
-      case 'date':
-        $value = is_numeric($value) ? (int) $value : strtotime($value);
-        if ($value === FALSE) {
-          return;
-        }
-        $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC');
-        break;
-      case 'integer':
-        $value = (int) $value;
-        break;
-      case 'decimal':
-        $value = (float) $value;
-        break;
-    }
-    if ($multi_valued) {
-      $doc->addField($key, $value);
-    }
-    else {
-      $doc->setField($key, $value);
-    }
-  }
-
-  /**
-   * Delete items from an index on this server.
-   *
-   * This method has a custom, Solr-specific extension:
-   * If $ids is a string other than "all", it is treated as a Solr query. All
-   * items matching that Solr query are then deleted. If $index is additionally
-   * specified, then only those items also lying on that index will be deleted.
-   * It is up to the caller to ensure $ids is a valid query when the method is
-   * called in this fashion.
-   */
-  public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
-    try {
-      $this->connect();
-      if ($index) {
-        $index_id = $index->machine_name;
-        if (is_array($ids)) {
-          $solr_ids = array();
-          foreach ($ids as $id) {
-            $solr_ids[] = $this->createId($index_id, $id);
-          }
-          $this->solr->deleteByMultipleIds($solr_ids);
-        }
-        elseif ($ids == 'all') {
-          $this->solr->deleteByQuery("index_id:" . $index_id);
-        }
-        else {
-          $this->solr->deleteByQuery("index_id:" . $index_id . ' (' . $ids . ')');
-        }
-      }
-      else {
-        $q = $ids == 'all' ? '*:*' : $ids;
-        $this->solr->deleteByQuery($q);
-      }
-      $this->scheduleCommit();
-    }
-    catch(Exception $e) {
-      watchdog_exception('search_api_solr', $e, '%type while deleting items from server @server: !message in %function (line %line of %file).', array('@server' => $this->server->name));
-    }
-  }
-
-  public function search(SearchApiQueryInterface $query) {
-    $time_method_called = microtime(TRUE);
-    // Reset request handler
-    $this->request_handler = NULL;
-    // Get field information
-    $index = $query->getIndex();
-    $fields = $this->getFieldNames($index);
-
-    // Extract keys
-    $keys = $query->getKeys();
-    if (is_array($keys)) {
-      $keys = $this->flattenKeys($keys);
-    }
-
-    // Set searched fields
-    $options = $query->getOptions();
-    $search_fields = $query->getFields();
-    // Get the index fields to be able to retrieve boosts.
-    $index_fields = $index->getFields();
-    $qf = array();
-    foreach ($search_fields as $f) {
-      $boost = '';
-      $boost = isset($index_fields[$f]['boost']) ? '^' . $index_fields[$f]['boost'] : '';
-      $qf[] = $fields[$f] . $boost;
-    }
-
-    // Extract filters
-    $filter = $query->getFilter();
-    $fq = $this->createFilterQueries($filter, $fields, $index->options['fields']);
-    $fq[] = 'index_id:' . $index->machine_name;
-
-    // Extract sort
-    $sort = array();
-    foreach ($query->getSort() as $f => $order) {
-      $f = $fields[$f];
-      if (substr($f, 0, 3) == 'ss_') {
-        $f = 'sort_' . substr($f, 3);
-      }
-      $order = strtolower($order);
-      $sort[] = "$f $order";
-    }
-
-    // Get facet fields
-    $facets = $query->getOption('search_api_facets', array());
-    $facet_params = $this->getFacetParams($facets, $fields, $fq);
-
-    // Handle highlighting
-    $highlight_params = $this->getHighlightParams($query);
-
-    // Handle More Like This query
-    $mlt = $query->getOption('search_api_mlt');
-    if ($mlt) {
-      $mlt_params['qt'] = 'mlt';
-      // The fields to look for similarities in.
-      $mlt_fl = array();
-      foreach($mlt['fields'] as $f) {
-        $mlt_fl[] = $fields[$f];
-        // For non-text fields, set minimum word length to 0.
-        if (isset($index->options['fields'][$f]['type']) && !search_api_is_text_type($index->options['fields'][$f]['type'])) {
-          $mlt_params['f.' . $fields[$f] . '.mlt.minwl'] = 0;
-        }
-      }
-      $mlt_params['mlt.fl'] = implode(',', $mlt_fl);
-      $keys = 'id:' . SearchApiSolrConnection::phrase($this->createId($index->machine_name, $mlt['id']));
-    }
-
-    // Set defaults
-    if (!$keys) {
-      $keys = NULL;
-    }
-    $offset = isset($options['offset']) ? $options['offset'] : 0;
-    $limit = isset($options['limit']) ? $options['limit'] : 1000000;
-
-    // Collect parameters
-    $params = array(
-      'fl' => 'item_id,score',
-      'qf' => $qf,
-      'fq' => $fq,
-    );
-    if ($sort) {
-      $params['sort'] = implode(', ', $sort);
-    }
-    if (!empty($facet_params['facet.field'])) {
-      $params += $facet_params;
-    }
-    if (!empty($highlight_params)) {
-      $params += $highlight_params;
-    }
-    if (!empty($options['search_api_spellcheck'])) {
-      $params['spellcheck'] = 'true';
-    }
-    if (!empty($mlt_params['mlt.fl'])) {
-      $params += $mlt_params;
-    }
-    if (!empty($this->options['retrieve_data'])) {
-      $params['fl'] = '*,score';
-    }
-    $call_args = array(
-      'query'  => &$keys,
-      'offset' => &$offset,
-      'limit'  => &$limit,
-      'params' => &$params,
-    );
-    if ($this->request_handler) {
-      $this->setRequestHandler($this->request_handler, $call_args);
-    }
-
-    try {
-      // Send search request
-      $time_processing_done = microtime(TRUE);
-      $this->connect();
-      drupal_alter('search_api_solr_query', $call_args, $query);
-      $this->preQuery($call_args, $query);
-
-      // Retrieve http method from server options.
-      $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : Apache_Solr_Service::METHOD_POST;
-      $response = $this->solr->search($keys, $offset, $limit, $params, $http_method);
-      $time_query_done = microtime(TRUE);
-
-      if ($response->getHttpStatus() != 200) {
-        throw new SearchApiException(t('The Solr server responded with status code @status: @msg.',
-            array('@status' => $response->getHttpStatus(), '@msg' => $response->getHttpStatusMessage())));
-      }
-
-      // Extract results
-      $results = $this->extractResults($query, $response);
-
-      // Extract facets
-      if ($facets = $this->extractFacets($query, $response)) {
-        $results['search_api_facets'] = $facets;
-      }
-
-      drupal_alter('search_api_solr_search_results', $results, $query, $response);
-      $this->postQuery($results, $query, $response);
-
-      // Compute performance
-      $time_end = microtime(TRUE);
-      $results['performance'] = array(
-        'complete' => $time_end - $time_method_called,
-        'preprocessing' => $time_processing_done - $time_method_called,
-        'execution' => $time_query_done - $time_processing_done,
-        'postprocessing' => $time_end - $time_query_done,
-      );
-
-      return $results;
-    }
-    catch (Exception $e) {
-      throw new SearchApiException(t('An error occurred while trying to search with Solr: @msg.', array('@msg' => $e->getMessage())));
-    }
-  }
-
-  /**
-   * Extract results from a Solr response.
-   *
-   * @param Apache_Solr_Response $response
-   *   A response object from SolrPhpClient.
-   *
-   * @return array
-   *   An array with two keys:
-   *   - result count: The number of total results.
-   *   - results: An array of search results, as specified by
-   *     SearchApiQueryInterface::execute().
-   */
-  protected function extractResults(SearchApiQueryInterface $query, Apache_Solr_Response $response) {
-    $index = $query->getIndex();
-    $fields = $this->getFieldNames($index);
-    $field_options = $index->options['fields'];
-
-    // Set up the results array.
-    $results = array();
-    $results['results'] = array();
-    // In some rare cases (e.g., MLT query with nonexistent ID) the response
-    // will be NULL.
-    if (!isset($response->response)) {
-      $results['result count'] = 0;
-      return $results;
-    }
-    $results['result count'] = $response->response->numFound;
-
-    // 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};
-          // Date fields need some special treatment to become valid date values
-          // (i.e., timestamps) again.
-          if (isset($field_options[$search_api_property]['type'])
-              && $field_options[$search_api_property]['type'] == 'date'
-              && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $result['fields'][$search_api_property])) {
-            $result['fields'][$search_api_property] = strtotime($result['fields'][$search_api_property]);
-          }
-        }
-      }
-
-      // We can find the item id and score in the special 'search_api_*'
-      // 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'];
-
-      $solr_id = $this->createId($index->machine_name, $result['id']);
-      $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields);
-      if ($excerpt) {
-        $result['excerpt'] = $excerpt;
-      }
-
-      // 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;
-      }
-    }
-
-    // Check for spellcheck suggestions.
-    if (module_exists('search_api_spellcheck') && $query->getOption('search_api_spellcheck')) {
-      $results['search_api_spellcheck'] = new SearchApiSpellcheckSolr($response);
-    }
-
-    return $results;
-  }
-
-  /**
-   * Extract and format highlighting information for a specific item from a Solr response.
-   *
-   * Will also use highlighted fields to replace retrieved field data, if the
-   * corresponding option is set.
-   */
-  protected function getExcerpt(Apache_Solr_Response $response, $id, array &$fields, array $field_mapping) {
-    if (!isset($response->highlighting->$id)) {
-      return FALSE;
-    }
-    $output = '';
-
-    if (!empty($this->options['excerpt']) && !empty($response->highlighting->$id->spell)) {
-      foreach ($response->highlighting->$id->spell as $snippet) {
-        $snippet = strip_tags($snippet);
-        $snippet = preg_replace('/^.*>|<.*$/', '', $snippet);
-        $snippet = $this->formatHighlighting($snippet);
-        // The created fragments sometimes have leading or trailing punctuation.
-        // We remove that here for all common cases, but take care not to remove
-        // < or > (so HTML tags stay valid).
-        $snippet = trim($snippet, "\00..\x2F:;=\x3F..\x40\x5B..\x60");
-        $output .= $snippet . ' … ';
-      }
-    }
-    if (!empty($this->options['highlight_data'])) {
-      foreach ($field_mapping as $search_api_property => $solr_property) {
-        if (substr($solr_property, 0, 2) == 't_' && !empty($response->highlighting->$id->$solr_property)) {
-          // Contrary to above, we here want to preserve HTML, so we just
-          // replace the [HIGHLIGHT] tags with the appropriate format here.
-          $fields[$search_api_property] = $this->formatHighlighting($response->highlighting->$id->$solr_property);
-        }
-      }
-    }
-
-    return $output;
-  }
-
-
-  protected function formatHighlighting($snippet) {
-    return preg_replace('#\[(/?)HIGHLIGHT\]#', '<$1strong>', $snippet);
-  }
-
-  /**
-   * Extract facets from a Solr response.
-   *
-   * @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;
-
-      $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 (!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.
-            if (isset($terms->_empty_) && $terms->_empty_ < $min_count) {
-              unset($terms->_empty_);
-            }
-            else {
-              $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])) {
-            unset($facets[$delta]);
-          }
-        }
-      }
-
-      return $facets;
-    }
-  }
-
-  /**
-   * Flatten a keys array into a single search string.
-   *
-   * @param array $keys
-   *   The keys array to flatten, formatted as specified by
-   *   SearchApiQueryInterface::getKeys().
-   *
-   * @return string
-   *   A Solr query string representing the same keys.
-   */
-  protected function flattenKeys(array $keys) {
-    $k = array();
-    $or = $keys['#conjunction'] == 'OR';
-    $neg = !empty($keys['#negation']);
-    foreach (element_children($keys) as $i) {
-      $key = $keys[$i];
-      if (!$key) {
-        continue;
-      }
-      if (is_array($key)) {
-        $subkeys = $this->flattenKeys($key);
-        if ($subkeys) {
-          $nested_expressions = TRUE;
-          // If this is a negated OR expression, we can't just use nested keys
-          // as-is, but have to put them into parantheses.
-          if ($or && $neg) {
-            $subkeys = "($subkeys)";
-          }
-          $k[] = $subkeys;
-        }
-      }
-      else {
-        $key = trim($key);
-        $key = SearchApiSolrConnection::phrase($key);
-        $k[] = $key;
-      }
-    }
-    if (!$k) {
-      return '';
-    }
-
-    // Formatting the keys into a Solr query can be a bit complex. The following
-    // code will produce that look like this:
-    //
-    // #conjunction | #negation | return value
-    // ----------------------------------------------------------------
-    // AND          | FALSE     | A B C
-    // AND          | TRUE      | -(A B C)
-    // OR           | FALSE     | ((A) OR (B) OR (C))
-    // OR           | TRUE      | -A -B -C
-
-    // If there was just a single, unnested key, we can ignore all this.
-    if (count($k) == 1 && empty($nested_expressions)) {
-      $k = reset($k);
-      return $neg ? "-$k" : $k;
-    }
-
-    if ($or) {
-      if ($neg) {
-        return '-' . implode(' -', $k);
-      }
-      return '((' . implode(') OR (', $k) . '))';
-    }
-    $k = implode(' ', $k);
-    return $neg ? "-($k)" : $k;
-  }
-
-  /**
-   * Transforms a query filter into a flat array of Solr filter queries, using
-   * the field names in $fields.
-   */
-  protected function createFilterQueries(SearchApiQueryFilterInterface $filter, array $solr_fields, array $fields) {
-    $or = $filter->getConjunction() == 'OR';
-    $fq = array();
-    foreach ($filter->getFilters() as $f) {
-      if (is_array($f)) {
-        if (!isset($fields[$f[0]])) {
-          throw new SearchApiException(t('Filter term on unknown or unindexed field @field.', array('@field' => $f[0])));
-        }
-        if ($f[1] !== '') {
-          $fq[] = $this->createFilterQuery($solr_fields[$f[0]], $f[1], $f[2], $fields[$f[0]]);
-        }
-      }
-      else {
-        $q = $this->createFilterQueries($f, $solr_fields, $fields);
-        if ($filter->getConjunction() != $f->getConjunction()) {
-          // $or == TRUE means the nested filter has conjunction AND, and vice versa
-          $sep = $or ? ' ' : ' OR ';
-          $fq[] = count($q) == 1 ? reset($q) : '((' . implode(')' . $sep . '(', $q) . '))';
-        }
-        else {
-          $fq = array_merge($fq, $q);
-        }
-      }
-    }
-    return ($or && count($fq) > 1) ? array('((' . implode(') OR (', $fq) . '))') : $fq;
-  }
-
-  /**
-   * Create a single search query string according to the given field, value
-   * and operator.
-   */
-  protected function createFilterQuery($field, $value, $operator, $field_info) {
-    $field = SearchApiSolrConnection::escapeFieldName($field);
-    if ($value === NULL) {
-      return ($operator == '=' ? '-' : '') . "$field:[* TO *]";
-    }
-    $value = trim($value);
-    $value = $this->formatFilterValue($value, search_api_extract_inner_type($field_info['type']));
-    switch ($operator) {
-      case '<>':
-        return "-($field:$value)";
-      case '<':
-        return "$field:{* TO $value}";
-      case '<=':
-        return "$field:[* TO $value]";
-      case '>=':
-        return "$field:[$value TO *]";
-      case '>':
-        return "$field:{{$value} TO *}";
-
-      default:
-        return "$field:$value";
-    }
-  }
-
-  /**
-   * Format a value for filtering on a field of a specific type.
-   */
-  protected function formatFilterValue($value, $type) {
-    switch ($type) {
-      case 'boolean':
-        $value = $value ? 'true' : 'false';
-        break;
-      case 'date':
-        $value = is_numeric($value) ? (int) $value : strtotime($value);
-        if ($value === FALSE) {
-          return 0;
-        }
-        $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC');
-        break;
-    }
-    return SearchApiSolrConnection::phrase($value);
-  }
-
-  /**
-   * Helper method for creating the facet field parameters.
-   */
-  protected function getFacetParams(array $facets, array $fields, array &$fq = array()) {
-    if (!$facets) {
-      return array();
-    }
-    $facet_params['facet'] = 'true';
-    $facet_params['facet.sort'] = 'count';
-    $facet_params['facet.limit'] = 10;
-    $facet_params['facet.mincount'] = 1;
-    $facet_params['facet.missing'] = 'false';
-    $taggedFields = array();
-    foreach ($facets as $info) {
-      if (empty($fields[$info['field']])) {
-        continue;
-      }
-      // String fields have their own corresponding facet fields.
-      $field = $fields[$info['field']];
-      // Check for the "or" operator.
-      if (isset($info['operator']) && $info['operator'] === 'or') {
-        // Remember that filters for this field should be tagged.
-        $escaped = SearchApiSolrConnection::escapeFieldName($fields[$info['field']]);
-        $taggedFields[$escaped] = "{!tag=$escaped}";
-        // Add the facet field.
-        $facet_params['facet.field'][] = "{!ex=$escaped}$field";
-      }
-      else {
-        // Add the facet field.
-        $facet_params['facet.field'][] = $field;
-      }
-      // Set limit, unless it's the default.
-      if ($info['limit'] != 10) {
-        $facet_params["f.$field.facet.limit"] = $info['limit'] ? $info['limit'] : -1;
-      }
-      // Set mincount, unless it's the default.
-      if ($info['min_count'] != 1) {
-        $facet_params["f.$field.facet.mincount"] = $info['min_count'];
-      }
-      // Set missing, if specified.
-      if ($info['missing']) {
-        $facet_params["f.$field.facet.missing"] = 'true';
-      }
-    }
-    // Tag filters of fields with "OR" facets.
-    foreach ($taggedFields as $field => $tag) {
-      $regex = '#(?<![^( ])' . preg_quote($field, '#') . ':#';
-      foreach ($fq as $i => $filter) {
-        // Solr can't handle two tags on the same filter, so we don't add two.
-        // Another option here would even be to remove the other tag, too,
-        // since we can be pretty sure that this filter does not originate from
-        // a facet – however, wrong results would still be possible, and this is
-        // definitely an edge case, so don't bother.
-        if (preg_match($regex, $filter) && substr($filter, 0, 6) != '{!tag=') {
-          $fq[$i] = $tag . $filter;
-        }
-      }
-    }
-
-    return $facet_params;
-  }
-
-  /**
-   * Helper method for creating the highlighting parameters.
-   *
-   * (The $query parameter currently isn't used and only here for the potential
-   * sake of subclasses.)
-   */
-  protected function getHighlightParams(SearchApiQueryInterface $query) {
-    $highlight_params = array();
-
-    if (!empty($this->options['excerpt']) || !empty($this->options['highlight_data'])) {
-      $highlight_params['hl'] = 'true';
-      $highlight_params['hl.fl'] = 'spell';
-      $highlight_params['hl.simple.pre'] = '[HIGHLIGHT]';
-      $highlight_params['hl.simple.post'] = '[/HIGHLIGHT]';
-      $highlight_params['hl.snippets'] = 3;
-      $highlight_params['hl.fragsize'] = 70;
-      $highlight_params['hl.mergeContiguous'] = 'true';
-    }
-
-    if (!empty($this->options['highlight_data'])) {
-      $highlight_params['hl.fl'] = 't_*';
-      $highlight_params['hl.snippets'] = 1;
-      $highlight_params['hl.fragsize'] = 0;
-      if (!empty($this->options['excerpt'])) {
-        // If we also generate a "normal" excerpt, set the settings for the
-        // "spell" field (which we use to generate the excerpt) back to the
-        // above values.
-        $highlight_params['f.spell.hl.snippets'] = 3;
-        $highlight_params['f.spell.hl.fragsize'] = 70;
-        // It regrettably doesn't seem to be possible to set hl.fl to several
-        // values, if one contains wild cards (i.e., "t_*,spell" wouldn't work).
-        $highlight_params['hl.fl'] = '*';
-      }
-    }
-
-    return $highlight_params;
-  }
-
-  /**
-   * Helper method for setting the request handler, and making necessary
-   * adjustments to the request parameters.
-   *
-   * @param $handler
-   *   Name of the handler to set.
-   * @param array $call_args
-   *   An associative array containing all four arguments to the
-   *   Apache_Solr_Service::search() call ("query", "offset", "limit" and
-   *   "params") as references.
-   *
-   * @return boolean
-   *   TRUE iff this method invocation handled the given handler. This allows
-   *   subclasses to recognize whether the request handler was already set by
-   *   this method.
-   */
-  protected function setRequestHandler($handler, array &$call_args) {
-    if ($handler == 'pinkPony') {
-      $call_args['params']['qt'] = $handler;
-      return TRUE;
-    }
-    return FALSE;
-  }
-
-  /**
-   * Empty method to allow subclasses to apply custom changes before the query
-   * is sent to Solr. Works exactly like hook_search_api_solr_query_alter().
-   *
-   * @param array $call_args
-   *   An associative array containing all four arguments to the
-   *   Apache_Solr_Service::search() call ("query", "offset", "limit" and
-   *   "params") as references.
-   * @param SearchApiQueryInterface $query
-   *   The SearchApiQueryInterface object representing the executed search query.
-   */
-  protected function preQuery(array &$call_args, SearchApiQueryInterface $query) {
-  }
-
-  /**
-   * Empty method to allow subclasses to apply custom changes before search results are returned.
-   *
-   * Works exactly like hook_search_api_solr_search_results_alter().
-   *
-   * @param array $results
-   *   The results array that will be returned for the search.
-   * @param SearchApiQueryInterface $query
-   *   The SearchApiQueryInterface object representing the executed search query.
-   * @param Apache_Solr_Response $response
-   *   The response object returned by Solr.
-   */
-  protected function postQuery(array &$results, SearchApiQueryInterface $query, Apache_Solr_Response $response) {
-  }
-
-  //
-  // Autocompletion feature
-  //
-
-  /**
-   * Get autocompletion suggestions for some user input.
-   *
-   * @param SearchApiQueryInterface $query
-   *   A query representing the completed user input so far.
-   * @param SearchApiAutocompleteSearch $search
-   *   An object containing details about the search the user is on, and
-   *   settings for the autocompletion.
-   * @param string $incomplete_key
-   *   The start of another fulltext keyword for the search, which should be
-   *   completed.
-   * @param string $user_input
-   *   The complete user input for the fulltext search keywords so far.
-   *
-   * @return array
-   *   An array of suggestion. Each suggestion is either a simple string
-   *   containing the whole suggested keywords, or an array containing the
-   *   following keys:
-   *   - prefix: For special suggestions, some kind of prefix describing them.
-   *   - suggestion_prefix: A suggested prefix for the entered input.
-   *   - user_input: The input entered by the user. Defaults to $user_input.
-   *   - suggestion_suffix: A suggested suffix for the entered input.
-   *   - results: If available, the estimated number of results for these keys.
-   */
-  // Largely copied from the apachesolr_autocomplete module.
-  public function getAutocompleteSuggestions(SearchApiQueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) {
-    $suggestions = array();
-    // Reset request handler
-    $this->request_handler = NULL;
-    // Turn inputs to lower case, otherwise we get case sensivity problems.
-    $incomp = drupal_strtolower($incomplete_key);
-
-    $index = $query->getIndex();
-    $fields = $this->getFieldNames($index);
-    $complete = $query->getOriginalKeys();
-
-    // Extract keys
-    $keys = $query->getKeys();
-    if (is_array($keys)) {
-      $keys_array = array();
-      while ($keys) {
-        reset($keys);
-        if (!element_child(key($keys))) {
-          array_shift($keys);
-          continue;
-        }
-        $key = array_shift($keys);
-        if (is_array($key)) {
-          $keys = array_merge($keys, $key);
-        }
-        else {
-          $keys_array[$key] = $key;
-        }
-      }
-      $keys = $this->flattenKeys($query->getKeys());
-    }
-    else {
-      $keys_array = drupal_map_assoc(preg_split('/[-\s():{}\[\]\\\\"]+/', $keys, -1, PREG_SPLIT_NO_EMPTY));
-    }
-    if (!$keys) {
-      $keys = NULL;
-    }
-
-    // Set searched fields
-    $options = $query->getOptions();
-    $search_fields = $query->getFields();
-    $qf = array();
-    foreach ($search_fields as $f) {
-      $qf[] = $fields[$f];
-    }
-
-    // Extract filters
-    $fq = $this->createFilterQueries($query->getFilter(), $fields, $index->options['fields']);
-    $fq[] = 'index_id:' . $index->machine_name;
-
-    // Autocomplete magic
-    $facet_fields = array();
-    foreach ($search_fields as $f) {
-      $facet_fields[] = $fields[$f];
-    }
-
-    $limit = $query->getOption('limit', 10);
-
-    $params = array(
-      'qf' => $qf,
-      'fq' => $fq,
-      'facet' => 'true',
-      'facet.field' => $facet_fields,
-      'facet.prefix' => $incomp,
-      'facet.limit' => $limit * 5,
-      'facet.mincount' => 1,
-      'spellcheck' => (!isset($this->options['autocorrect_spell']) || $this->options['autocorrect_spell']) ? 'true' : 'false',
-      'spellcheck.count' => 1,
-    );
-    $call_args = array(
-      'query'  => &$keys,
-      'offset' => 0,
-      'limit'  => 0,
-      'params' => &$params,
-    );
-    if ($this->request_handler) {
-      $this->setRequestHandler($this->request_handler, $call_args);
-    }
-    $second_pass = !isset($this->options['autocorrect_suggest_words']) || $this->options['autocorrect_suggest_words'];
-    for ($i = 0; $i < ($second_pass ? 2 : 1); ++$i) {
-      try {
-        // Send search request
-        $this->connect();
-        drupal_alter('search_api_solr_query', $call_args, $query);
-        $this->preQuery($call_args, $query);
-        $response = $this->solr->search($keys, 0, 0, $params);
-
-        if ($response->getHttpStatus() != 200) {
-          watchdog('search_api_solr', 'The Solr server responded with status code @status: @msg.', array('@status' => $response->getHttpStatus(), '@msg' => $response->getHttpStatusMessage()), WATCHDOG_WARNING, 'admin/config/search/search_api/server/' . $this->server->machine_name);
-          return array();
-        }
-
-        if (!empty($response->spellcheck->suggestions)) {
-          $replace = array();
-          foreach ($response->spellcheck->suggestions as $word => $data) {
-            $replace[$word] = $data->suggestion[0];
-          }
-          $corrected = str_ireplace(array_keys($replace), array_values($replace), $user_input);
-          if ($corrected != $user_input) {
-            array_unshift($suggestions, array(
-              'prefix' => t('Did you mean') . ':',
-              'user_input' => $corrected,
-            ));
-          }
-        }
-
-        $matches = array();
-        if (isset($response->facet_counts->facet_fields)) {
-          foreach ($response->facet_counts->facet_fields as $terms) {
-            foreach ($terms as $term => $count) {
-              if (isset($matches[$term])) {
-                // If we just add the result counts, we can easily get over the
-                // total number of results if terms appear in multiple fields.
-                // Therefore, we just take the highest value from any field.
-                $matches[$term] = max($matches[$term], $count);
-              }
-              else {
-                $matches[$term] = $count;
-              }
-            }
-          }
-
-          if ($matches) {
-            // Eliminate suggestions that are too short or already in the query.
-            foreach ($matches as $term => $count) {
-              if (strlen($term) < 3 || isset($keys_array[$term])) {
-                unset($matches[$term]);
-              }
-            }
-
-            // Don't suggest terms that are too frequent (by default in more
-            // than 90% of results).
-            $result_count = $response->response->numFound;
-            $max_occurrences = $result_count * variable_get('search_api_solr_autocomplete_max_occurrences', 0.9);
-            if (($max_occurrences >= 1 || $i > 0) && $max_occurrences < $result_count) {
-              foreach ($matches as $match => $count) {
-                if ($count > $max_occurrences) {
-                  unset($matches[$match]);
-                }
-              }
-            }
-
-            // The $count in this array is actually a score. We want the
-            // highest ones first.
-            arsort($matches);
-
-            // Shorten the array to the right ones.
-            $additional_matches = array_slice($matches, $limit - count($suggestions), NULL, TRUE);
-            $matches = array_slice($matches, 0, $limit, TRUE);
-
-            // Build suggestions using returned facets
-            $incomp_length = strlen($incomp);
-            foreach ($matches as $term => $count) {
-              if (drupal_strtolower(substr($term, 0, $incomp_length)) == $incomp) {
-                $suggestions[] = array(
-                  'suggestion_suffix' => substr($term, $incomp_length),
-                  'results' => $count,
-                );
-              }
-              else {
-                $suggestions[] = array(
-                  'suggestion_suffix' => ' ' . $term,
-                  'results' => $count,
-                );
-              }
-            }
-          }
-        }
-      }
-      catch (Exception $e) {
-        watchdog_exception('search_api_solr', $e, "%type during autocomplete Solr query: !message in %function (line %line of %file).", array(), WATCHDOG_WARNING);
-      }
-
-      if (count($suggestions) >= $limit) {
-        break;
-      }
-      // Change parameters for second query.
-      unset($params['facet.prefix']);
-      $keys = trim ($keys . ' ' . $incomplete_key);
-    }
-
-    return $suggestions;
-  }
-
-  //
-  // SearchApiMultiServiceInterface methods
-  //
-
-  /**
-   * Create a query object for searching on this server.
-   *
-   * @param $options
-   *   Associative array of options configuring this query. See
-   *   SearchApiMultiQueryInterface::__construct().
-   *
-   * @throws SearchApiException
-   *   If the server is currently disabled.
-   *
-   * @return SearchApiMultiQueryInterface
-   *   An object for searching this server.
-   */
-  public function queryMultiple(array $options = array()) {
-    return new SearchApiMultiQuery($this->server, $options);
-  }
-
-  /**
-   * Executes a search on the server represented by this object.
-   *
-   * @param SearchApiMultiQueryInterface $query
-   *   The search query to execute.
-   *
-   * @throws SearchApiException
-   *   If an error prevented the search from completing.
-   *
-   * @return array
-   *   An associative array containing the search results, as required by
-   *   SearchApiMultiQueryInterface::execute().
-   */
-  public function searchMultiple(SearchApiMultiQueryInterface $query) {
-    $time_method_called = microtime(TRUE);
-    // Get field information
-    $solr_fields = array(
-      'search_api_id' => 'ss_search_api_id',
-      'search_api_relevance' => 'score',
-      'search_api_multi_index' => 'index_id',
-    );
-    $fields = array(
-      'search_api_multi_index' => array(
-        'type' => 'string',
-      ),
-    );
-    foreach ($query->getIndexes() as $index_id => $index) {
-      if (empty($index->options['fields'])) {
-        continue;
-      }
-      $prefix = $index_id . ':';
-      foreach ($this->getFieldNames($index) as $field => $key) {
-        if (!isset($solr_fields[$field])) {
-          $solr_fields[$prefix . $field] = $key;
-        }
-      }
-      foreach ($index->options['fields'] as $field => $info) {
-        $fields[$prefix . $field] = $info;
-      }
-    }
-
-    // Extract keys
-    $keys = $query->getKeys();
-    if (is_array($keys)) {
-      $keys = $this->flattenKeys($keys);
-    }
-
-    // Set searched fields
-    $search_fields = $query->getFields();
-    $qf = array();
-    foreach ($search_fields as $f) {
-      $qf[] = $solr_fields[$f];
-    }
-
-    // Extract filters
-    $filter = $query->getFilter();
-    $fq = $this->createFilterQueries($filter, $solr_fields, $fields);
-
-    // Restrict search to searched indexes.
-    $index_filter = array();
-    foreach ($query->getIndexes() as $index_id => $index) {
-      $index_filter[] = 'index_id:' . SearchApiSolrConnection::phrase($index_id);
-    }
-    $fq[] = implode(' OR ', $index_filter);
-
-    // Extract sort
-    $sort = array();
-    foreach ($query->getSort() as $f => $order) {
-      $f = $solr_fields[$f];
-      if (substr($f, 0, 3) == 'ss_') {
-        $f = 'sort_' . substr($f, 3);
-      }
-      $order = strtolower($order);
-      $sort[] = "$f $order";
-    }
-
-    // Get facet fields
-    $facets = $query->getOption('search_api_facets') ? $query->getOption('search_api_facets') : array();
-    $facet_params = $this->getFacetParams($facets, $solr_fields);
-
-    // Set defaults
-    if (!$keys) {
-      $keys = NULL;
-    }
-    $options = $query->getOptions();
-    $offset = isset($options['offset']) ? $options['offset'] : 0;
-    $limit = isset($options['limit']) ? $options['limit'] : 1000000;
-
-    // Collect parameters
-    $params = array(
-      'qf' => $qf,
-      'fl' => 'item_id,index_id,score',
-      'fq' => $fq,
-    );
-    if ($sort) {
-      $params['sort'] = implode(', ', $sort);
-    }
-    if (!empty($facet_params['facet.field'])) {
-      $params += $facet_params;
-    }
-    try {
-      // Send search request
-      $time_processing_done = microtime(TRUE);
-      $this->connect();
-      $call_args = array(
-        'query'  => &$keys,
-        'offset' => &$offset,
-        'limit'  => &$limit,
-        'params' => &$params,
-      );
-      drupal_alter('search_api_solr_multi_query', $call_args, $query);
-
-      // Retrieve http method from server options.
-      $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : Apache_Solr_Service::METHOD_POST;
-      $response = $this->solr->search($keys, $offset, $limit, $params, $http_method);
-      $time_query_done = microtime(TRUE);
-
-      if ($response->getHttpStatus() != 200) {
-        throw new SearchApiException(t('The Solr server responded with status code @status: @msg.',
-            array('@status' => $response->getHttpStatus(), '@msg' => $response->getHttpStatusMessage())));
-      }
-
-      // Extract results
-      $results = array();
-      $results['result count'] = $response->response->numFound;
-      $results['results'] = array();
-      $tmp = array();
-      foreach ($response->response->docs as $id => $doc) {
-        $result = array(
-          'id' => $doc->item_id,
-          'index_id' => $doc->index_id,
-          'score' => $doc->score,
-        );
-        $excerpt = $this->getExcerpt($response, $id, $tmp, array());
-        if ($excerpt) {
-          $result['excerpt'] = $excerpt;
-        }
-        $results['results'][$id] = $result;
-      }
-
-      // 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 = $this->getFacetField($solr_fields[$info['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);
-              }
-            }
-            foreach ($terms as $term => $count) {
-              if ($count >= $min_count) {
-                $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]);
-            }
-          }
-        }
-      }
-
-      // Compute performance
-      $time_end = microtime(TRUE);
-      $results['performance'] = array(
-        'complete' => $time_end - $time_method_called,
-        'preprocessing' => $time_processing_done - $time_method_called,
-        'execution' => $time_query_done - $time_processing_done,
-        'postprocessing' => $time_end - $time_query_done,
-      );
-
-      return $results;
-    }
-    catch (Exception $e) {
-      throw new SearchApiException($e->getMessage());
-    }
-  }
-
-  //
-  // Additional methods that might be used when knowing the service class.
-  //
-
-  /**
-   * Ping the Solr server to tell whether it can be accessed.
-   *
-   * Uses the admin/ping request handler.
-   */
-  public function ping() {
-    $this->connect();
-    return $this->solr->ping();
-  }
-
-  /**
-   * Sends a commit command to the Solr server.
-   */
-  public function commit() {
-    try {
-      $this->connect();
-      return $this->solr->commit(FALSE, FALSE, FALSE);
-    }
-    catch (Exception $e) {
-      watchdog('search_api_solr', 'A commit operation for server @name failed: @msg.',
-          array('@name' => $this->server->machine_name, '@msg' => $e->getMessage()), WATCHDOG_WARNING);
-    }
-  }
-
-  /**
-   * Schedules a commit operation for this server.
-   *
-   * The commit will be sent at the end of the current page request. Multiple
-   * calls to this method will still only result in one commit operation.
-   */
-  public function scheduleCommit() {
-    if (!$this->commitScheduled) {
-      $this->commitScheduled = TRUE;
-      drupal_register_shutdown_function(array($this, 'commit'));
-    }
-  }
-
-  /**
-   * @return SearchApiSolrConnection
-   *   The solr connection object used by this server.
-   */
-  public function getSolrConnection() {
-    $this->connect();
-    return $this->solr;
-  }
-
-  /**
-   * Get metadata about fields in the Solr/Lucene index.
-   *
-   * @param boolean $reset
-   *   Reload the cached data?
-   */
-  public function getFields($reset = FALSE) {
-    $cid = 'search_api_solr:fields:' . $this->server->machine_name;
-
-    // If the data hasn't been retrieved before and we aren't refreshing it, try
-    // to get data from the cache.
-    if (!isset($this->fields) && !$reset) {
-      $cache = cache_get($cid);
-      if (isset($cache->data) && !$reset) {
-        $this->fields = $cache->data;
-      }
-    }
-
-    // If there was no data in the cache, or if we're refreshing the data,
-    // connect to the Solr server, retrieve schema information, and cache it.
-    if (!isset($this->fields) || $reset) {
-      $this->connect();
-      $this->fields = array();
-      foreach ($this->solr->getFields() as $name => $info) {
-        $this->fields[$name] = new SearchApiSolrField($info);
-      }
-      cache_set($cid, $this->fields);
-    }
-
-    return $this->fields;
-  }
-
-}
diff --git a/solr_connection.inc b/solr_connection.inc
deleted file mode 100644
index 770a7b6..0000000
--- a/solr_connection.inc
+++ /dev/null
@@ -1,259 +0,0 @@
-<?php
-
-/**
- * A few custom rewrites to the Apache_Solr_Service class, to allow providing
- * HTTP authentication and using this module without turning "allow_url_fopen"
- * on.
- *
- * Stolen from the apachesolr module for the most part.
- */
-class SearchApiSolrConnection extends Apache_Solr_Service {
-
-  /**
-   * Authentication string (username + password) for HTTP authentication.
-   */
-  protected $http_auth;
-
-  /**
-   * Additional servlet mapping. Allows us to use the LukeRequestHandler Solr
-   * service.
-   */
-  const LUKE_SERVLET = 'admin/luke';
-
-  /**
-   * Lucene index schema information.
-   *
-   * @var Apache_Solr_Response
-   */
-  protected $luke;
-
-  /**
-   * Identifies which version of the SolrPhpClient this uses, "old" or "new".
-   *
-   * @var bool
-   */
-  protected $newClient = FALSE;
-
-  /**
-   * Constructs a Solr connection with an optional HTTP user and password.
-   *
-   * @param array $options
-   *   An array containing construction arguments.
-   */
-  public function __construct(array $options) {
-    $options += array(
-      'host' => 'localhost',
-      'port' => 8983,
-      'path' => '',
-      'http_user' => NULL,
-      'http_pass' => NULL,
-      'default_field'=>'id',
-    );
-    parent::__construct($options['host'], $options['port'], $options['path']);
-    if ($options['http_user'] && $options['http_pass']) {
-      $this->http_auth = 'Basic ' . base64_encode($options['http_user'] . ':' . $options['http_pass']);
-    }
-    // Since /ping otherwise complains about missing default field.
-    $this->_pingUrl .= '?q=' . $options['default_field'] . ':1';
-
-    // As of July 2011, the newest release is r60, with Service.php having
-    // revision 59. Revision 40 is just anything between 22 (old) and that.
-    $this->newClient = trim(parent::SVN_REVISION, '$ :A..Za..z') > 40;
-    if ($this->newClient) {
-      $this->_httpTransport = new SearchApiSolrHttpTransport($this->http_auth);
-    }
-  }
-
-  /**
-   * Central method for making a get operation against this Solr Server.
-   *
-   * @see Apache_Solr_Service::_sendRawGet()
-   */
-  protected function _sendRawGet($url, $timeout = FALSE) {
-    // Little "hack" to allow filter-only queries
-    // Since "*:*" doesn't work with the dismax query handler, we mustn't set
-    // "q", to let "q.alt" kick in. However, Apache_Solr_Service::search() will
-    // always add "q", even if it is empty. Therefore, we delete empty "q"
-    // parameters here.
-    $url = preg_replace('/([?&])q=(&|$)/', '$1', $url);
-
-    if ($this->newClient) {
-      return parent::_sendRawGet($url, $timeout);
-    }
-
-    list($data, $headers) = $this->_makeHttpRequest($url, 'GET', array(), '', $timeout);
-    $response = new Apache_Solr_Response($data, $headers, $this->_createDocuments, $this->_collapseSingleValueArrays);
-    $code = (int) $response->getHttpStatus();
-    if ($code != 200) {
-      $message = $response->getHttpStatusMessage();
-      if ($code >= 400 && $code != 403 && $code != 404) {
-        // Add details, like Solr's exception message.
-        $message .= $response->getRawResponse();
-      }
-      throw new Exception('"' . $code . '" Status: ' . $message);
-    }
-    return $response;
-  }
-
-  /**
-   * Central method for making a post operation against this Solr Server.
-   *
-   * @see Apache_Solr_Service::_sendRawPost()
-   */
-  protected function _sendRawPost($url, $rawPost, $timeout = FALSE, $contentType = 'text/xml; charset=UTF-8') {
-    if ($this->newClient) {
-      return parent::_sendRawPost($url, $rawPost, $timeout, $contentType);
-    }
-
-    $request_headers = array('Content-Type' => $contentType);
-    list($data, $headers) = $this->_makeHttpRequest($url, 'POST', $request_headers, $rawPost, $timeout);
-    $response = new Apache_Solr_Response($data, $headers, $this->_createDocuments, $this->_collapseSingleValueArrays);
-    $code = (int) $response->getHttpStatus();
-    if ($code != 200) {
-      $message = $response->getHttpStatusMessage();
-      if ($code >= 400 && $code != 403 && $code != 404) {
-        // Add details, like Solr's exception message.
-        $message .= $response->getRawResponse();
-      }
-      throw new Exception('"' . $code . '" Status: ' . $message);
-    }
-    return $response;
-  }
-
-
-  /**
-   * Call the /admin/ping servlet, to test the connection to the server.
-   *
-   * @param $timeout
-   *   maximum time to wait for ping in seconds, -1 for unlimited (default 2).
-   * @return
-   *   (float) seconds taken to ping the server, FALSE if timeout occurs.
-   */
-  public function ping($timeout = 2) {
-    if ($this->newClient) {
-      return parent::ping($timeout);
-    }
-    $start = microtime(TRUE);
-
-    if ($timeout <= 0.0) {
-      $timeout = -1;
-    }
-    // Attempt a HEAD request to the solr ping url.
-    list($data, $headers) = $this->_makeHttpRequest($this->_pingUrl, 'HEAD', array(), NULL, $timeout);
-    $response = new Apache_Solr_Response($data, $headers);
-
-    if ($response->getHttpStatus() == 200) {
-      // Add 0.1 ms to the ping time so we never return 0.0.
-      return microtime(TRUE) - $start + 0.0001;
-    }
-    else {
-      return FALSE;
-    }
-  }
-
-  /**
-   * Helper method for making an HTTP request, without using stupid stuff like
-   * file_get_contents().
-   */
-  protected function _makeHttpRequest($url, $method = 'GET', $headers = array(), $content = '', $timeout = FALSE) {
-    $options = array(
-      'headers' => $headers,
-      'method' => $method,
-      'data' => $content,
-    );
-
-    if ($this->http_auth) {
-      $options['headers']['Authorization'] = $this->http_auth;
-    }
-    if ($timeout) {
-      $options['timeout'] = $timeout;
-    }
-
-    $result = drupal_http_request($url, $options);
-
-    if (!isset($result->code) || $result->code < 0) {
-      $result->code = 0;
-      $result->status_message = 'Request failed';
-      $result->protocol = 'HTTP/1.0';
-    }
-    // Additional information may be in the error property.
-    if (isset($result->error)) {
-      $result->status_message .= ': ' . check_plain($result->error);
-    }
-
-    if (!isset($result->data)) {
-      $result->data = '';
-    }
-    // The headers have to be reformatted for the response class.
-    $headers[] = "{$result->protocol} {$result->code} {$result->status_message}";
-    if (isset($result->headers)) {
-      foreach ($result->headers as $name => $value) {
-        $headers[] = "$name: $value";
-      }
-    }
-    return array($result->data, $headers);
-  }
-
-  /**
-   * Convenience function for escaping a field name.
-   *
-   * Since field names can only contain one special character, ":", there is no
-   * need to use the complete escape() method.
-   *
-   * @param string $value
-   *   The field name to escape.
-   *
-   * @return string
-   *   An escaped string suitable for passing to Solr.
-   */
-  static public function escapeFieldName($value) {
-    $value = str_replace(':', '\:', $value);
-    return $value;
-  }
-
-  /**
-   * Convenience function for creating phrase syntax from a value.
-   *
-   * @param string $value
-   *   The string to convert into a Solr phrase value.
-   *
-   * @return string
-   *   A quoted string suitable for passing to Solr.
-   */
-  static public function phrase($value) {
-    $value = str_replace("\\", "\\\\", $value);
-    $value = str_replace('"', '\"', $value);
-    return '"' . $value . '"';
-  }
-
-  /**
-   * Get metadata about the Lucene index.
-   *
-   * @param int $num_terms
-   *   Number of 'top terms' to return.
-   *
-   * @return Apache_Solr_Response
-   *   A response object containing schema information.
-   */
-  public function getLuke($num_terms = 0) {
-    if (!isset($this->luke[$num_terms])) {
-      $url = $this->_constructUrl(self::LUKE_SERVLET, array('numTerms' => "$num_terms", 'wt' => self::SOLR_WRITER));
-      $this->luke[$num_terms] = $this->_sendRawGet($url);
-    }
-    return $this->luke[$num_terms];
-  }
-
-  /**
-   * Get metadata about fields in the Lucene index.
-   *
-   * @return array
-   *   An array of objects, keyed by field name, describing fields on the index.
-   *
-   * @see SearchApiSolrConnection::getLuke()
-   * @see http://wiki.apache.org/solr/LukeRequestHandler
-   */
-  public function getFields() {
-    return (array) $this->getLuke()->fields;
-  }
-
-}
diff --git a/solr_field.inc b/solr_field.inc
deleted file mode 100644
index 6eea13d..0000000
--- a/solr_field.inc
+++ /dev/null
@@ -1,185 +0,0 @@
-<?php
-
-/**
- * Logic around Solr field schema information.
- */
-class SearchApiSolrField {
-
-  /**
-   * @var array
-   *   Human-readable labels for Solr schema properties.
-   */
-  public static $schemaLabels = array(
-    'I' => 'Indexed',
-    'T' => 'Tokenized',
-    'S' => 'Stored',
-    'M' => 'Multivalued',
-    'V' => 'TermVector Stored',
-    'o' => 'Store Offset With TermVector',
-    'p' => 'Store Position With TermVector',
-    'O' => 'Omit Norms',
-    'L' => 'Lazy',
-    'B' => 'Binary',
-    'C' => 'Compressed',
-    'f' => 'Sort Missing First',
-    'l' => 'Sort Missing Last',
-  );
-
-  /**
-   * @var stdclass
-   *   The original field object.
-   */
-  protected $field;
-
-  /**
-   * @var array
-   *   An array of schema properties for this field. This will be a subset of
-   *   the SearchApiSolrField::schemaLabels array.
-   */
-  protected $schema;
-
-  /**
-   * Constructor.
-   *
-   * @param stdClass $field
-   *   A field object from Solr's "Luke" servlet.
-   */
-  public function __construct($field) {
-    $this->field = $field;
-  }
-
-  /**
-   * Get the type of the Solr field, according to the Solr schema.
-   *
-   * Note that field types like "text", "boolean", and "date" are conventions,
-   * but their presence and behavior are entirely determined by the particular
-   * schema.xml file used by a Solr core.
-   *
-   * @return string
-   *   The type of the Solr field.
-   */
-  public function getType() {
-    return $this->field->type;
-  }
-
-  /**
-   * Get an array of field properties.
-   *
-   * @return array
-   *   An array of properties describing the solr schema. The array keys are
-   *   single-character codes, and the values are human-readable labels. This
-   *   will be a subset of the SearchApiSolrField::schemaLabels array.
-   */
-  public function getSchema() {
-    if (!isset($this->schema)) {
-      foreach (str_split(str_replace('-', '', $this->field->schema)) as $key) {
-        $this->schema[$key] = isset(self::$schemaLabels[$key]) ? self::$schemaLabels[$key] : $key;
-      }
-    }
-    return $this->schema;
-  }
-
-  /**
-   * Get the "dynamic base" of this field.
-   *
-   * This typically looks like 'ss_*, and is used to aggregate fields based on
-   * "hungarian" naming conventions.
-   *
-   * @return string
-   *   The mask describing the solr aggregate field, if there is one.
-   */
-  public function getDynamicBase() {
-    return isset($this->field->dynamicBase) ? $this->field->dynamicBase : NULL;
-  }
-
-  /**
-   * Determine whether this field may be suitable for use as a key field.
-   *
-   * Unfortunately, it seems like the best way to find an actual uniqueKey field
-   * according to Solr is to examine the Solr core's schema.xml.
-   *
-   * @return boolean
-   *   Whether the field is suitable for use as a key.
-   */
-  public function isPossibleKey() {
-    return !$this->getDynamicBase()
-      && !in_array($this->getType(), array('boolean', 'date', 'text'))
-      && $this->isStored()
-      && !$this->isMultivalued();
-  }
-
-  /**
-   * Determine whether a field is suitable for sorting.
-   *
-   * In order for a field to yield useful sorted results in Solr, it must be
-   * indexed, not multivalued, and not tokenized. It's ok if a field is
-   * tokenized and yields only one token, but there's no general way to check
-   * for that.
-   *
-   * @return boolean
-   *   Whether the field is suitable for sorting.
-   */
-  public function isSortable() {
-    return $this->isIndexed()
-      && !$this->isMultivalued()
-      && !$this->isTokenized();
-  }
-
-  /**
-   * The following functions return information about specific properties of this field.
-   *
-   * @return boolean
-   */
-  public function isIndexed() {
-    $this->getSchema();
-    return isset($this->schema['I']);
-  }
-  public function isTokenized() {
-    $this->getSchema();
-    return isset($this->schema['T']);
-  }
-  public function isStored() {
-    $this->getSchema();
-    return isset($this->schema['S']);
-  }
-  public function isMultivalued() {
-    $this->getSchema();
-    return isset($this->schema['M']);
-  }
-  public function isTermVectorStored() {
-    $this->getSchema();
-    return isset($this->schema['V']);
-  }
-  public function isStoreOffsetWithTermVector() {
-    $this->getSchema();
-    return isset($this->schema['o']);
-  }
-  public function isStorePositionWithTermVector() {
-    $this->getSchema();
-    return isset($this->schema['p']);
-  }
-  public function isOmitNorms() {
-    $this->getSchema();
-    return isset($this->schema['O']);
-  }
-  public function isLazy() {
-    $this->getSchema();
-    return isset($this->schema['L']);
-  }
-  public function isBinary() {
-    $this->getSchema();
-    return isset($this->schema['B']);
-  }
-  public function isCompressed() {
-    $this->getSchema();
-    return isset($this->schema['C']);
-  }
-  public function isSortMissingFirst() {
-    $this->getSchema();
-    return isset($this->schema['f']);
-  }
-  public function isSortMissingLast() {
-    $this->getSchema();
-    return isset($this->schema['l']);
-  }
-}
