diff --git a/README.md b/README.md
new file mode 100644
index 0000000..437cdf6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+1. Add a Search API index for the server you configured in step 2.
+   1. Check "Solr Document" under the list of Data Sources.
+   2. Configure the Solr Document Datasource by specifying the name of a unique ID
+      field from your server's schema.
+   3. Finish configuring the index by selecting the appropriate server.  Check
+      "Read only" under the Index Options.
+2. On the index's Fields tab, add the fields you would like to have shown in Views
+   or any other display.  Fields must be added here first before they can be
+   displayed, despite appearing in the Add fields list in Views.
+3. Create a view or some other display to see the documents from your server.  If
+   you are using Views, set it up as you would for any other Search API
+   datasource.
+   1. In the Add view wizard, select "Index INDEX_NAME" under the View
+      Settings.
+   2. Configure the view to display fields.
+   3. Add the fields that you want to display.
+
+Known Issues
+------------
+* Search API Solr's backend unsets the ID field that you configure in the
+  datasource settings from the results arrays, making it unavailable for display.
diff --git a/config/schema/plugin.plugin_configuration.search_api_datasource.solr_document.yml b/config/schema/plugin.plugin_configuration.search_api_datasource.solr_document.yml
new file mode 100644
index 0000000..c691b27
--- /dev/null
+++ b/config/schema/plugin.plugin_configuration.search_api_datasource.solr_document.yml
@@ -0,0 +1,14 @@
+plugin.plugin_configuration.search_api_datasource.solr_document:
+  type: mapping
+  label: "Solr document datasource configuration"
+  mapping:
+    id_field:
+      type: string
+      label: "ID field"
+    advanced:
+      type: mapping
+      label: "Advanced configuration"
+      mapping:
+        request_handler:
+          type: string
+          label: "Request handler"
diff --git a/search_api_solr.module b/search_api_solr.module
index 8c3f63e..759e21f 100644
--- a/search_api_solr.module
+++ b/search_api_solr.module
@@ -204,6 +204,22 @@ function search_api_solr_search_api_views_handler_mapping_alter(&$mapping) {
 }
 
 /**
+ * Implements hook_views_data_alter().
+ *
+ * Remove fields from solr_document datasources from the views data. Datasource
+ * fields that have been added to the index would be duplicated in the Views Add
+ * fields list. Fields that aren't added to the index can't be displayed.
+ */
+function search_api_solr_views_data_alter(array &$data) {
+  // @todo check for a search_api based view first.
+  foreach ($data as $key => $fields) {
+    if (preg_match('/search_api_datasource_(.+)_solr_document/', $key)) {
+      unset($data[$key]);
+    }
+  }
+}
+
+/**
  * Deletes all Solr Field Type and re-installs them from their yml files.
  */
 function search_api_solr_delete_and_reinstall_all_field_types() {
diff --git a/search_api_solr.services.yml b/search_api_solr.services.yml
index 6ca51bd..50056af 100644
--- a/search_api_solr.services.yml
+++ b/search_api_solr.services.yml
@@ -26,3 +26,11 @@ services:
     arguments: ['@current_user']
     tags:
       - { name: access_check, applies_to: _search_api_solr_local_action_multilingual_access_check }
+
+  solr_document.factory:
+    class: Drupal\search_api_solr\SolrDocumentFactory
+    arguments: ['@typed_data_manager']
+
+  solr_field.manager:
+    class: Drupal\search_api_solr\SolrFieldManager
+    arguments: ['@cache.discovery']
diff --git a/src/Plugin/DataType/SolrDocument.php b/src/Plugin/DataType/SolrDocument.php
new file mode 100644
index 0000000..6854e90
--- /dev/null
+++ b/src/Plugin/DataType/SolrDocument.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Drupal\search_api_solr\Plugin\DataType;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\Exception\MissingDataException;
+use Drupal\Core\TypedData\TypedData;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api_solr\TypedData\SolrDocumentDefinition;
+use Solarium\QueryType\Select\Result\AbstractDocument;
+
+/**
+ * Defines the "Solr document" data type.
+ *
+ * Instances of this class wrap Search API Item objects and allow to deal with
+ * items based upon the Typed Data API.
+ *
+ * @DataType(
+ *   id = "solr_document",
+ *   label = @Translation("Solr document"),
+ *   description = @Translation("Records from a Solr index."),
+ *   definition_class = "\Drupal\search_api_solr\TypedData\SolrDocumentDefinition"
+ * )
+ */
+class SolrDocument extends TypedData implements \IteratorAggregate, ComplexDataInterface {
+
+  /**
+   * The wrapped Search API Item.
+   *
+   * @var \Drupal\search_api\Item\ItemInterface|null
+   */
+  protected $item;
+
+  /**
+   * Creates an instance wrapping the given Item.
+   *
+   * @param \Drupal\search_api\Item\ItemInterface|null $item
+   *   The Item object to wrap.
+   *
+   * @return static
+   */
+  public static function createFromItem(ItemInterface $item) {
+    $server_id = $item->getIndex()->getServerInstance()->id();
+    $definition = SolrDocumentDefinition::create($server_id);
+    $instance = new static($definition);
+    $instance->setValue($item);
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    return $this->item;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($item, $notify = TRUE) {
+    $this->item = $item;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($property_name) {
+    if (!isset($this->item)) {
+      throw new MissingDataException("Unable to get Solr field $property_name as no item has been provided.");
+    }
+
+    // First, verify that this field actually exists in the Solr server. If we
+    // can't get a definition for it, it doesn't exist.
+    /** @var \Drupal\search_api_solr\Plugin\DataType\SolrField $plugin */
+    $plugin = \Drupal::typedDataManager()->getDefinition('solr_field')['class'];
+    $field_manager = \Drupal::getContainer()->get('solr_field.manager');
+    $server_id = $this->item->getIndex()->getServerInstance()->id();
+    $fields = $field_manager->getFieldDefinitions($server_id);
+    if (empty($fields[$property_name])) {
+      throw new \InvalidArgumentException("The Solr field $property_name could not be found on the $server_id server.");
+    }
+    // Create a new typed data object from the item's field data.
+    $property = $plugin::createInstance($fields[$property_name], $property_name, $this);
+
+    // Now that we have the property, try to find its values. We first look at
+    // the field values contained in the result item.
+    $found = FALSE;
+    foreach ($this->item->getFields(FALSE) as $field) {
+      if ($field->getDatasourceId() === 'solr_document'
+          && $field->getPropertyPath() === $property_name) {
+        $property->setValue($field->getValues());
+        $found = TRUE;
+        break;
+      }
+    }
+
+    if (!$found) {
+      // If that didn't work, maybe we can get the field from the Solr document?
+      $document = $this->item->getExtraData('search_api_solr_document');
+      if ($document instanceof AbstractDocument
+          && isset($document[$property_name])) {
+        $property->setValue($document[$property_name]);
+      }
+    }
+
+    return $property;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($property_name, $value, $notify = TRUE) {
+    // Do nothing because we treat Solr documents as read-only.
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProperties($include_computed = FALSE) {
+    // @todo Implement this.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function toArray() {
+    // @todo Implement this.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    return !isset($this->item);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onChange($name) {
+    // Do nothing.  Unlike content entities, Items don't need to be notified of
+    // changes.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    return isset($this->item) ? $this->item->getIterator() : new \ArrayIterator([]);
+  }
+
+}
diff --git a/src/Plugin/DataType/SolrField.php b/src/Plugin/DataType/SolrField.php
new file mode 100644
index 0000000..a99514e
--- /dev/null
+++ b/src/Plugin/DataType/SolrField.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\search_api_solr\Plugin\DataType;
+
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\TypedData\TypedData;
+use Drupal\search_api\Item\FieldInterface;
+
+/**
+ * Defines the "Solr field" data type.
+ *
+ * Instances of this class wrap Search API Field objects and allow to deal with
+ * fields based upon the Typed Data API.
+ *
+ * @DataType(
+ *   id = "solr_field",
+ *   label = @Translation("Solr field"),
+ *   description = @Translation("Fields from a Solr document."),
+ *   definition_class = "\Drupal\search_api_solr\SolrFieldDefinition"
+ * )
+ */
+class SolrField extends TypedData implements \IteratorAggregate, TypedDataInterface {
+
+  /**
+   * The field value(s).
+   *
+   * @var mixed
+   */
+  protected $value;
+
+  /**
+   * Creates an instance wrapping the given Field.
+   *
+   * @param \Drupal\search_api\Item\FieldInterface $field
+   *   The Field object to wrap.
+   * @param string $name
+   *   The name of the wrapped field.
+   * @param \Drupal\Core\TypedData\TypedDataInterface $parent
+   *   The parent object of the wrapped field, which should be a Solr document.
+   *
+   * @return static
+   */
+  public static function createFromField(FieldInterface $field, $name, TypedDataInterface $parent) {
+    // Get the Solr field definition from the SolrFieldManager.
+    /** @var \Drupal\search_api_solr\SolrFieldManagerInterface $field_manager */
+    $field_manager = \Drupal::getContainer()->get('solr_field.manager');
+    $server_id = $field->getIndex()->getServerInstance()->id();
+    $field_id = $field->getPropertyPath();
+    $definition = $field_manager->getFieldDefinitions($server_id)[$field_id];
+    $instance = new static($definition, $name, $parent);
+    $instance->setValue($field->getValues());
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    return new \ArrayIterator((array) $this->value);
+  }
+
+}
diff --git a/src/Plugin/search_api/backend/SearchApiSolrAnySchemaBackend.php b/src/Plugin/search_api/backend/SearchApiSolrAnySchemaBackend.php
new file mode 100644
index 0000000..1e4eaf3
--- /dev/null
+++ b/src/Plugin/search_api/backend/SearchApiSolrAnySchemaBackend.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace Drupal\search_api_solr\Plugin\search_api\backend;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\Query\ResultSetInterface;
+use Solarium\Core\Query\QueryInterface as SolariumQueryInterface;
+
+/**
+ * A read-only backend for any non-drupal schema.
+ *
+ * @SearchApiBackend(
+ *   id = "search_api_solr_any_schema",
+ *   label = @Translation("Any Schema Solr"),
+ *   description = @Translation("Read-only connection to any Solr server.")
+ * )
+ */
+class SearchApiSolrAnySchemaBackend extends SearchApiSolrBackend {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    // @todo force read-only
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function preQuery(SolariumQueryInterface $solarium_query, QueryInterface $query) {
+    parent::preQuery($solarium_query, $query);
+
+    // Do not modify 'Server index status' queries.
+    // @see https://www.drupal.org/node/2668852
+    if ($query->hasTag('server_index_status')) {
+      return;
+    }
+
+    // Do not alter the query if the index does not use the solr_document
+    // datasource.
+    $index = $query->getIndex();
+    if (!$index->isValidDatasource('solr_document')) {
+      return;
+    }
+
+    // Remove the filter queries that limit the results based on site and index.
+    $solarium_query->removeFilterQuery('site_hash');
+    $solarium_query->removeFilterQuery('index_id');
+
+    // Set requestHandler for the query type.
+    $config = $index->getDatasource('solr_document')->getConfiguration();
+    if (!empty($config['request_handler'])) {
+      $solarium_query->addParam('qt', $config['request_handler']);
+    }
+
+    // Set the default query, if necessary and configured.
+    if (!$solarium_query->getQuery() && !empty($config['default_query'])) {
+      $solarium_query->setQuery($config['default_query']);
+    }
+
+    $backend = $index->getServerInstance()->getBackend();
+    if ($backend instanceof SearchApiSolrBackend) {
+      $solr_config = $backend->getConfiguration();
+      // @todo Should we maybe not even check that setting and use this to
+      //   auto-enable fields retrieval from Solr?
+      if (!empty($solr_config['retrieve_data'])) {
+        $fields_list = [];
+        foreach ($backend->getSolrFieldNames($index) as $solr_field_name) {
+          $fields_list[] = $solr_field_name;
+        }
+        $extra_fields = [
+          'language_field',
+          'label_field',
+          'url_field',
+        ];
+        foreach ($extra_fields as $config_key) {
+          if (!empty($config[$config_key])) {
+            $fields_list[] = $config[$config_key];
+          }
+        }
+        $solarium_query->setFields(array_unique($fields_list));
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function postQuery(ResultSetInterface $results, QueryInterface $query, $response) {
+    parent::postQuery($results, $query, $response);
+
+    // Do not alter the results if the index does not use the solr_document
+    // datasource.
+    $datasources = $query->getIndex()->getDatasources();
+    if (!isset($datasources['solr_document'])) {
+      return;
+    }
+
+    /** @var \Drupal\search_api_solr\SolrDocumentFactoryInterface $solr_document_factory */
+    $solr_document_factory = \Drupal::getContainer()->get('solr_document.factory');
+
+    /** @var \Drupal\search_api\Item\Item $item */
+    foreach ($results->getResultItems() as $item) {
+      // Create the typed data object for the Item immediately after the query
+      // has been run. Doing this now can prevent the Search API from having to
+      // query for individual documents later.
+      $item->setOriginalObject($solr_document_factory->create($item));
+
+      // Prepend each item's itemId with the datasource ID. A lot of the Search
+      // API assumes that the item IDs are formatted as
+      // 'datasouce_id/entity_id'. Of course, the ID numbers of external Solr
+      // documents will not have this pattern and the datasource must be added.
+      // Reflect into the class to set the itemId.
+      $reflection = new \ReflectionClass($item);
+      $id_property = $reflection->getProperty('itemId');
+      $id_property->setAccessible(TRUE);
+      $id_property->setValue($item, 'solr_document/' . $item->getId());
+    }
+  }
+
+  /**
+   * Override the default fields that Search API Solr sets up.  In particular,
+   * set the ID field to the one that is configured via the datasource config
+   * form.
+   *
+   * Also, map the index's field names to the original property paths. Search
+   * API Solr adds prefixes to the paths because it assumes that it has done the
+   * indexing according to its schema.xml rules. Of course, in our case it
+   * hasn't and we need it to use the raw paths. Any field machine names that
+   * have been altered in the field list will have their mapping corrected by
+   * this step too.
+   *
+   * @see \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend::getSolrFieldNames()
+   *
+   * {@inheritdoc}
+   */
+  public function getSolrFieldNames(IndexInterface $index, $reset = FALSE) {
+    // @todo The field name mapping should be cached per index because custom
+    //   queries needs to access it on every query. But we need to be aware of
+    //   datasource additions and deletions.
+    if (!isset($this->fieldNames[$index->id()]) || $reset) {
+      parent::getSolrFieldNames($index, $reset);
+
+      // Do not alter mappings if the index does not use the solr_document
+      // datasource.
+      $datasources = $index->getDatasources();
+      if (isset($datasources['solr_document'])) {
+        // Set the ID field.
+        $config = $index->getDatasource('solr_document')->getConfiguration();
+        $this->fieldNames[$index->id()]['search_api_id'] = $config['id_field'];
+
+        /** @var \Drupal\search_api\Item\FieldInterface[] $index_fields */
+        $index_fields = $index->getFields();
+
+        // Re-map the indexed fields.
+        foreach ($this->fieldNames[$index->id()] as $raw => $name) {
+          // Ignore the Search API fields.
+          if (strpos($raw, 'search_api_') === 0
+            || empty($index_fields[$raw])
+            || $index_fields[$raw]->getDatasourceId() !== 'solr_document') {
+            continue;
+          }
+          $this->fieldNames[$index->id()][$raw] = $index_fields[$raw]->getPropertyPath();
+        }
+      }
+    }
+    return $this->fieldNames[$index->id()];
+  }
+
+}
diff --git a/src/Plugin/search_api/datasource/SolrDocument.php b/src/Plugin/search_api/datasource/SolrDocument.php
new file mode 100644
index 0000000..56e41fe
--- /dev/null
+++ b/src/Plugin/search_api/datasource/SolrDocument.php
@@ -0,0 +1,298 @@
+<?php
+
+namespace Drupal\search_api_solr\Plugin\search_api\datasource;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\search_api\Datasource\DatasourcePluginBase;
+use Drupal\search_api\Plugin\PluginFormTrait;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api_solr\SolrDocumentFactoryInterface;
+use Drupal\search_api_solr\SolrFieldManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Represents a datasource which exposes external Solr Documents.
+ *
+ * @SearchApiDatasource(
+ *   id = "solr_document",
+ *   label = @Translation("Solr Document"),
+ *   description = @Translation("Search through external Solr content. (Only works if this index is attached to a Solr-based server.)"),
+ * )
+ */
+class SolrDocument extends DatasourcePluginBase implements PluginFormInterface {
+
+  use PluginFormTrait;
+
+  /**
+   * The Solr document factory.
+   *
+   * @var \Drupal\search_api_solr\SolrDocumentFactoryInterface
+   */
+  protected $solrDocumentFactory;
+
+  /**
+   * The Solr field manager.
+   *
+   * @var \Drupal\search_api_solr\SolrFieldManagerInterface
+   */
+  protected $solrFieldManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    /** @var static $datasource */
+    $datasource = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+
+    $datasource->setSolrDocumentFactory($container->get('solr_document.factory'));
+    $datasource->setSolrFieldManager($container->get('solr_field.manager'));
+
+    return $datasource;
+  }
+
+  /**
+   * Sets the Solr document factory.
+   *
+   * @param \Drupal\search_api_solr\SolrDocumentFactoryInterface $factory
+   *   The new entity field manager.
+   *
+   * @return $this
+   */
+  public function setSolrDocumentFactory(SolrDocumentFactoryInterface $factory) {
+    $this->solrDocumentFactory = $factory;
+    return $this;
+  }
+
+  /**
+   * Returns the Solr document factory.
+   *
+   * @return \Drupal\search_api_solr\SolrDocumentFactoryInterface
+   *   The Solr document factory.
+   */
+  public function getSolrDocumentFactory() {
+    return $this->solrDocumentFactory ?: \Drupal::getContainer()->get('solr_document.factory');
+  }
+
+  /**
+   * Sets the Solr field manager.
+   *
+   * @param \Drupal\search_api_solr\SolrFieldManagerInterface $solr_field_manager
+   *   The new entity field manager.
+   *
+   * @return $this
+   */
+  public function setSolrFieldManager(SolrFieldManagerInterface $solr_field_manager) {
+    $this->solrFieldManager = $solr_field_manager;
+    return $this;
+  }
+
+  /**
+   * Returns the Solr field manager.
+   *
+   * @return \Drupal\search_api_solr\SolrFieldManagerInterface
+   *   The Solr field manager.
+   */
+  public function getSolrFieldManager() {
+    return $this->solrFieldManager ?: \Drupal::getContainer()->get('solr_field.manager');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemId(ComplexDataInterface $item) {
+    return $this->getFieldValue($item, 'id_field');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemLabel(ComplexDataInterface $item) {
+    return $this->getFieldValue($item, 'label_field');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemLanguage(ComplexDataInterface $item) {
+    if ($this->configuration['language_field']) {
+      return $this->getFieldValue($item, 'language_field');
+    }
+    return parent::getItemLanguage($item);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemUrl(ComplexDataInterface $item) {
+    return $this->getFieldValue($item, 'url_field');
+  }
+
+  /**
+   * Retrieves a scalar field value from a result item.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+   *   The result item.
+   * @param string $config_key
+   *   The key in the configuration.
+   *
+   * @return mixed|null
+   *   The scalar value of the specified field (first value for multi-valued
+   *   fields), if it exists; NULL otherwise.
+   */
+  protected function getFieldValue(ComplexDataInterface $item, $config_key) {
+    if (empty($this->configuration[$config_key])) {
+      return NULL;
+    }
+    $values = $item->get($this->configuration[$config_key])->getValue();
+    if (is_array($values)) {
+      $values = $values ? reset($values) : NULL;
+    }
+    return $values ?: NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions() {
+    $fields = [];
+    $server_id = $this->index->getServerId();
+    if ($server_id) {
+      $fields = $this->getSolrFieldManager()->getFieldDefinitions($server_id);
+    }
+    return $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultiple(array $ids) {
+    $documents = [];
+    try {
+      // Query the index for the Solr documents.
+      $results = $this->index->query()
+        ->addCondition('search_api_id', $ids, 'IN')
+        ->execute()
+        ->getResultItems();
+      foreach ($results as $id => $result) {
+        $documents[$id] = $this->solrDocumentFactory->create($result);
+      }
+    }
+    catch (SearchApiException $e) {
+      // Couldn't load items from server, return an empty array.
+    }
+    return $documents;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    $config = [];
+    $config['id_field'] = '';
+    $config['request_handler'] = '';
+    $config['label_field'] = '';
+    $config['language_field'] = '';
+    $config['url_field'] = '';
+    $config['default_query'] = '*:*';
+    return $config;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    // Get the available fields from the server (if a server has already been
+    // set).
+    $fields = $single_valued_fields = [];
+    foreach ($this->getPropertyDefinitions() as $name => $property) {
+      $fields[$name] = $property->getLabel();
+      if (!$property->isMultivalued()) {
+        $single_valued_fields[$name] = $property->getLabel();
+      }
+    }
+
+    $form['id_field'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('ID field'),
+      '#required' => TRUE,
+      '#description' => $this->t('Enter the name of the field from your Solr schema that contains unique ID values.'),
+      '#default_value' => $this->configuration['id_field'],
+    ];
+    // If there is already a valid server, we can transform the text field into
+    // a select box.
+    if ($single_valued_fields) {
+      $form['id_field']['#type'] = 'select';
+      $form['id_field']['#options'] = $single_valued_fields;
+      $form['id_field']['#description'] = $this->t('Select the Solr index field that contains unique ID values.');
+    }
+    $form['advanced'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Advanced configuration'),
+      '#open' => FALSE,
+    ];
+    $form['advanced']['request_handler'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Request handler'),
+      '#description' => $this->t("Enter the name of a requestHandler from the core's solrconfig.xml file.  This should only be necessary if you need to specify a handler to use other than the default."),
+      '#default_value' => $this->configuration['request_handler'],
+    ];
+    $form['advanced']['default_query'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default query'),
+      '#description' => $this->t("Enter a default query parameter. This is only necessary if a default query cannot be specified in the solrconfig.xml file."),
+      '#default_value' => $this->configuration['default_query'],
+    ];
+    $form['advanced']['label_field'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Label field'),
+      '#description' => $this->t('Enter the name of the field from your Solr schema that should be considered the label (if any).'),
+      '#default_value' => $this->configuration['label_field'],
+    ];
+    $form['advanced']['language_field'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Language field'),
+      '#description' => $this->t('Enter the name of the field from your Solr schema that should be considered the label (if any).'),
+      '#default_value' => $this->configuration['language_field'],
+    ];
+    $form['advanced']['url_field'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('URL field'),
+      '#description' => $this->t('Enter the name of the field from your Solr schema that should be considered the label (if any).'),
+      '#default_value' => $this->configuration['url_field'],
+    ];
+    // If there is already a valid server, we can transform the text fields into
+    // select boxes.
+    if ($fields) {
+      $fields = [
+        '' => $this->t('None'),
+      ] + $fields;
+      $form['advanced']['label_field']['#type'] = 'select';
+      $form['advanced']['label_field']['#options'] = $fields;
+      $form['advanced']['label_field']['#description'] = $this->t('Select the Solr index field that should be considered the label (if any).');
+      $form['advanced']['language_field']['#type'] = 'select';
+      $form['advanced']['language_field']['#options'] = $fields;
+      $form['advanced']['language_field']['#description'] = $this->t("Select the Solr index field that contains the document's language code (if any).");
+      $form['advanced']['url_field']['#type'] = 'select';
+      $form['advanced']['url_field']['#options'] = $fields;
+      $form['advanced']['url_field']['#description'] = $this->t("Select the Solr index field that contains the document's URL (if any).");
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // We want the form fields displayed inside an "Advanced configuration"
+    // fieldset, but we don't want them to be actually stored inside a nested
+    // "advanced" key. (This could also be done via "#parents", but that's
+    // pretty tricky to get right in a subform.)
+    $values = &$form_state->getValues();
+    $values += $values['advanced'];
+    unset($values['advanced']);
+  }
+
+}
diff --git a/src/SolrDocumentFactory.php b/src/SolrDocumentFactory.php
new file mode 100644
index 0000000..91aee95
--- /dev/null
+++ b/src/SolrDocumentFactory.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\search_api_solr;
+
+use Drupal\Core\TypedData\TypedDataManagerInterface;
+use Drupal\search_api\Item\ItemInterface;
+
+/**
+ * Defines a class for a Solr Document factory.
+ */
+class SolrDocumentFactory implements SolrDocumentFactoryInterface {
+
+  /**
+   * A typed data manager.
+   *
+   * @var \Drupal\Core\TypedData\TypedDataManagerInterface
+   */
+  protected $typedDataManager;
+
+  /**
+   * Constructs a SolrDocumentFactory object.
+   *
+   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
+   *   A typed data manager.
+   */
+  public function __construct(TypedDataManagerInterface $typedDataManager) {
+    $this->typedDataManager = $typedDataManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function create(ItemInterface $item) {
+    $plugin = $this->typedDataManager->getDefinition('solr_document')['class'];
+    return $plugin::createFromItem($item);
+  }
+
+}
diff --git a/src/SolrDocumentFactoryInterface.php b/src/SolrDocumentFactoryInterface.php
new file mode 100644
index 0000000..f57b109
--- /dev/null
+++ b/src/SolrDocumentFactoryInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\search_api_solr;
+
+use Drupal\search_api\Item\ItemInterface;
+
+/**
+ * Defines an interface for Solr Document factories.
+ */
+interface SolrDocumentFactoryInterface {
+
+  /**
+   * Creates a SolrDocument data type from a Search API result Item.
+   *
+   * @param \Drupal\search_api\Item\ItemInterface $item
+   *   The result item to be wrapped with the data type class.
+   *
+   * @return \Drupal\search_api_solr\Plugin\DataType\SolrDocument
+   *   The wrapped item.
+   */
+  public function create(ItemInterface $item);
+
+}
diff --git a/src/SolrFieldManager.php b/src/SolrFieldManager.php
new file mode 100644
index 0000000..74ffc23
--- /dev/null
+++ b/src/SolrFieldManager.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\search_api_solr;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\UseCacheBackendTrait;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\search_api\Entity\Server;
+use Drupal\search_api_solr\SearchApiSolrException;
+use Drupal\search_api_solr\SolrBackendInterface;
+use Drupal\search_api_solr\TypedData\SolrFieldDefinition;
+
+/**
+ * Manages the discovery of Solr fields.
+ */
+class SolrFieldManager implements SolrFieldManagerInterface {
+
+  use UseCacheBackendTrait;
+  use StringTranslationTrait;
+
+  /**
+   * Static cache of field definitions per Solr server.
+   *
+   * @var array
+   */
+  protected $fieldDefinitions;
+
+  /**
+   * Constructs a new SorFieldManager.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend.
+   */
+  public function __construct(CacheBackendInterface $cache_backend) {
+    $this->cacheBackend = $cache_backend;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldDefinitions($server_id) {
+    if (!isset($this->fieldDefinitions[$server_id])) {
+      // Not prepared, try to load from cache.
+      $cid = 'solr_field_definitions:' . $server_id;
+      if ($cache = $this->cacheGet($cid)) {
+        $field_definitions = $cache->data;
+      }
+      elseif ($field_definitions = $this->buildFieldDefinitions($server_id)) {
+        // Only cache the field definitions if they aren't empty.
+        $this->cacheSet($cid, $field_definitions, Cache::PERMANENT, ['search_api_server' => $server_id]);
+      }
+      $this->fieldDefinitions[$server_id] = $field_definitions;
+    }
+    return $this->fieldDefinitions[$server_id];
+  }
+
+  /**
+   * Builds the field definitions for a Solr server from its Luke handler.
+   *
+   * @param string $server_id
+   *   The server from which we are retrieving field information.
+   *
+   * @return \Drupal\search_api_solr\TypedData\SolrFieldDefinitionInterface[]
+   *   The array of field definitions for the server, keyed by field name.
+   *
+   * @throws \InvalidArgumentException
+   */
+  protected function buildFieldDefinitions($server_id) {
+    // Load the server entity.
+    $server = Server::load($server_id);
+    if ($server === NULL) {
+      throw new \InvalidArgumentException('The Search API server could not be loaded.');
+    }
+    $backend = $server->getBackend();
+    if (!$backend instanceof SolrBackendInterface) {
+      throw new \InvalidArgumentException("The Search API server's backend must be an instance of SolrBackendInterface.");
+    }
+    $fields = [];
+    try {
+      $luke = $backend->getSolrConnector()->getLuke();
+      foreach ($luke['fields'] as $name => $definition) {
+        $field = new SolrFieldDefinition($definition);
+        $label = Unicode::ucfirst(trim(str_replace('_', ' ', $name)));
+        $field->setLabel($label);
+        // The Search API can't deal with arbitrary item types. To make things
+        // easier, just use one of those known to the Search API.
+        if (strpos($field->getDataType(), 'text') !== FALSE) {
+          $field->setDataType('search_api_text');
+        }
+        elseif (strpos($field->getDataType(), 'date') !== FALSE) {
+          $field->setDataType('timestamp');
+        }
+        elseif (strpos($field->getDataType(), 'int') !== FALSE) {
+          $field->setDataType('integer');
+        }
+        elseif (strpos($field->getDataType(), 'long') !== FALSE) {
+          $field->setDataType('integer');
+        }
+        elseif (strpos($field->getDataType(), 'float') !== FALSE) {
+          $field->setDataType('float');
+        }
+        elseif (strpos($field->getDataType(), 'double') !== FALSE) {
+          $field->setDataType('float');
+        }
+        elseif (strpos($field->getDataType(), 'bool') !== FALSE) {
+          $field->setDataType('boolean');
+        }
+        else {
+          $field->setDataType('string');
+        }
+        $fields[$name] = $field;
+      }
+    }
+    catch (SearchApiSolrException $e) {
+      drupal_set_message($this->t('Could not connect to server %server, %message', ['%server' => $server->id(), '%message' => $e->getMessage()]), 'error');
+      // @todo Inject the logger service.
+      \Drupal::logger('search_api_solr')->error('Could not connect to server %server, %message', ['%server' => $server->id(), '%message' => $e->getMessage()]);
+    }
+    return $fields;
+  }
+
+}
diff --git a/src/SolrFieldManagerInterface.php b/src/SolrFieldManagerInterface.php
new file mode 100644
index 0000000..5fc947a
--- /dev/null
+++ b/src/SolrFieldManagerInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\search_api_solr;
+
+/**
+ * Defines an interface for a Solr field manager.
+ */
+interface SolrFieldManagerInterface {
+
+  /**
+   * Gets the field definitions for a Solr server.
+   *
+   * @param string $server_id
+   *   The ID of the Server from which we are retrieving field information.
+   *
+   * @return \Drupal\search_api_solr\TypedData\SolrFieldDefinitionInterface[]
+   *   The array of field definitions for the server, keyed by field name.
+   */
+  public function getFieldDefinitions($server_id);
+
+}
diff --git a/src/TypedData/SolrDocumentDefinition.php b/src/TypedData/SolrDocumentDefinition.php
new file mode 100644
index 0000000..ccf1546
--- /dev/null
+++ b/src/TypedData/SolrDocumentDefinition.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\search_api_solr\TypedData;
+
+use Drupal\Core\TypedData\ComplexDataDefinitionBase;
+
+/**
+ * A typed data definition class for describing Solr documents.
+ */
+class SolrDocumentDefinition extends ComplexDataDefinitionBase implements SolrDocumentDefinitionInterface {
+
+  /**
+   * The Search API server the Solr document definition belongs to.
+   *
+   * @var \Drupal\search_api\ServerInterface
+   */
+  protected $server;
+
+  /**
+   * Creates a new Solr document definition.
+   *
+   * @param string $server_id
+   *   The Search API server the Solr document definition belongs to.
+   *
+   * @return static
+   */
+  public static function create($server_id) {
+    $definition['type'] = 'solr_document:' . $server_id;
+    $document_definition = new static($definition);
+    $document_definition->setServerId($server_id);
+    return $document_definition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createFromDataType($data_type) {
+    // The data type should be in the form of "solr_document:$server_id".
+    $parts = explode(':', $data_type, 2);
+    if ($parts[0] != 'solr_document') {
+      throw new \InvalidArgumentException('Data type must be in the form of "solr_document:SERVER_ID".');
+    }
+    if (empty($parts[1])) {
+      throw new \InvalidArgumentException('A Search API Server must be specified.');
+    }
+
+    return self::create($parts[1]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getServerId() {
+    return isset($this->definition['constraints']['Server']) ? $this->definition['constraints']['Server'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setServerId($server_id) {
+    return $this->addConstraint('Server', $server_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions() {
+    if (!isset($this->propertyDefinitions)) {
+      $this->propertyDefinitions = [];
+      if (!empty($this->getServerId())) {
+        /** @var \Drupal\search_api_solr\SolrFieldManagerInterface $field_manager */
+        $field_manager = \Drupal::getContainer()->get('solr_field.manager');
+        $this->propertyDefinitions = $field_manager->getFieldDefinitions($this->getServerId());
+      }
+    }
+    return $this->propertyDefinitions;
+  }
+
+}
diff --git a/src/TypedData/SolrDocumentDefinitionInterface.php b/src/TypedData/SolrDocumentDefinitionInterface.php
new file mode 100644
index 0000000..c7ede9e
--- /dev/null
+++ b/src/TypedData/SolrDocumentDefinitionInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\search_api_solr\TypedData;
+
+use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
+
+/**
+ * Interface for typed data Solr document definitions.
+ */
+interface SolrDocumentDefinitionInterface extends ComplexDataDefinitionInterface {
+
+  /**
+   * Gets the Search API Server ID.
+   *
+   * @return string|null
+   *   The Server ID, or NULL if the Server is unknown.
+   */
+  public function getServerId();
+
+  /**
+   * Sets the Search API Server ID.
+   *
+   * @param string $server_id
+   *   The Server ID to set.
+   *
+   * @return $this
+   */
+  public function setServerId($server_id);
+
+}
diff --git a/src/TypedData/SolrFieldDefinition.php b/src/TypedData/SolrFieldDefinition.php
new file mode 100644
index 0000000..de42fd1
--- /dev/null
+++ b/src/TypedData/SolrFieldDefinition.php
@@ -0,0 +1,224 @@
+<?php
+
+namespace Drupal\search_api_solr\TypedData;
+
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Defines a class for Solr field definitions.
+ */
+class SolrFieldDefinition extends DataDefinition implements SolrFieldDefinitionInterface {
+
+  /**
+   * Human-readable labels for Solr schema properties.
+   *
+   * @var string[]
+   */
+  protected static $schemaLabels = [
+    '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',
+  ];
+
+  /**
+   * An array of Solr schema properties for this field.
+   *
+   * @var string[]
+   */
+  protected $schema;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isList() {
+    return $this->isMultivalued();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isReadOnly() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isComputed() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRequired() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSchema() {
+    if (!isset($this->schema)) {
+      foreach (str_split(str_replace('-', '', $this->definition['schema'])) as $key) {
+        $this->schema[$key] = isset(self::$schemaLabels[$key]) ? self::$schemaLabels[$key] : $key;
+      }
+    }
+    return $this->schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDynamicBase() {
+    return isset($this->field['dynamicBase']) ? $this->field['dynamicBase'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isIndexed() {
+    $this->getSchema();
+    return isset($this->schema['I']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTokenized() {
+    $this->getSchema();
+    return isset($this->schema['T']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isStored() {
+    $this->getSchema();
+    return isset($this->schema['S']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isMultivalued() {
+    $this->getSchema();
+    return isset($this->schema['M']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTermVectorStored() {
+    $this->getSchema();
+    return isset($this->schema['V']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isStoreOffsetWithTermVector() {
+    $this->getSchema();
+    return isset($this->schema['o']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isStorePositionWithTermVector() {
+    $this->getSchema();
+    return isset($this->schema['p']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isOmitNorms() {
+    $this->getSchema();
+    return isset($this->schema['O']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isLazy() {
+    $this->getSchema();
+    return isset($this->schema['L']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isBinary() {
+    $this->getSchema();
+    return isset($this->schema['B']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCompressed() {
+    $this->getSchema();
+    return isset($this->schema['C']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSortMissingFirst() {
+    $this->getSchema();
+    return isset($this->schema['f']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSortMissingLast() {
+    $this->getSchema();
+    return isset($this->schema['l']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isPossibleKey() {
+    return !$this->getDynamicBase()
+      && $this->isStored()
+      && !$this->isMultivalued();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSortable() {
+    return $this->isIndexed()
+      && !$this->isMultivalued();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isFulltextSearchable() {
+    return $this->isIndexed()
+      && $this->isTokenized();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isFilterable() {
+    return $this->isIndexed()
+      && !$this->isTokenized();
+  }
+
+}
diff --git a/src/TypedData/SolrFieldDefinitionInterface.php b/src/TypedData/SolrFieldDefinitionInterface.php
new file mode 100644
index 0000000..1669258
--- /dev/null
+++ b/src/TypedData/SolrFieldDefinitionInterface.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace Drupal\search_api_solr\TypedData;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+
+/**
+ * Defines an interface for Solr field definitions.
+ *
+ * The methods in this interface were copied from the Sarnia 7.x module's
+ * SarniaSearchApiSolrField class.  It is possible that not all of these methods
+ * are necessary or will be used.  They were copied because isMultivalued()
+ * provides a convenient way to implement DataDefinitionInterface::isList().
+ * The rest of the methods were also included just in case they are needed for
+ * other purposes.
+ *
+ * @todo Remove unused methods before the official release.
+ */
+interface SolrFieldDefinitionInterface extends DataDefinitionInterface {
+
+  /**
+   * Gets an array of field properties.
+   *
+   * @return string[]
+   *   An array of properties describing the solr schema. The array keys are
+   *   single-character codes, and the values are human-readable labels.
+   */
+  public function getSchema();
+
+  /**
+   * 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();
+
+  /**
+   * Determines whether this field is indexed.
+   *
+   * @return bool
+   *   TRUE if the field is indexed, FALSE otherwise.
+   */
+  public function isIndexed();
+
+  /**
+   * Determines whether this field is tokenized.
+   *
+   * @return bool
+   *   TRUE if the field is tokenized, FALSE otherwise.
+   */
+  public function isTokenized();
+
+  /**
+   * Determines whether this field is stored.
+   *
+   * @return bool
+   *   TRUE if the field is stored, FALSE otherwise.
+   */
+  public function isStored();
+
+  /**
+   * Determines whether this field is multi-valued.
+   *
+   * @return bool
+   *   TRUE if the field is multi-valued, FALSE otherwise.
+   */
+  public function isMultivalued();
+
+  /**
+   * Determines whether this field has stored term vectors.
+   *
+   * @return bool
+   *   TRUE if the field has stored term vectors, FALSE otherwise.
+   */
+  public function isTermVectorStored();
+
+  /**
+   * 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();
+
+  /**
+   * 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();
+
+  /**
+   * Determines whether this field omits norms when indexing.
+   *
+   * @return bool
+   *   TRUE if the field omits norms, FALSE otherwise.
+   */
+  public function isOmitNorms();
+
+  /**
+   * Determines whether this field is lazy-loaded.
+   *
+   * @return bool
+   *   TRUE if the field is lazy-loaded, FALSE otherwise.
+   */
+  public function isLazy();
+
+  /**
+   * Determines whether this field is binary.
+   *
+   * @return bool
+   *   TRUE if the field is binary, FALSE otherwise.
+   */
+  public function isBinary();
+
+  /**
+   * Determines whether this field is compressed.
+   *
+   * @return bool
+   *   TRUE if the field is compressed, FALSE otherwise.
+   */
+  public function isCompressed();
+
+  /**
+   * Determines whether this field sorts missing entries first.
+   *
+   * @return bool
+   *   TRUE if the field sorts missing entries first, FALSE otherwise.
+   */
+  public function isSortMissingFirst();
+
+  /**
+   * Determines whether this field sorts missing entries last.
+   *
+   * @return bool
+   *   TRUE if the field sorts missing entries last, FALSE otherwise.
+   */
+  public function isSortMissingLast();
+
+  /**
+   * 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 bool
+   *   Whether the field is suitable for use as a key.
+   */
+  public function isPossibleKey();
+
+  /**
+   * Determine whether a field is suitable for sorting.
+   *
+   * In order for a field to yield useful sorted results in Solr, it must be
+   * indexed and not multivalued. If a sort field is tokenized, the tokenization
+   * must yield only one token; multiple tokens can result in unpredictable sort
+   * ordering. Unfortunately, there's no way to check whether a particular field
+   * contains values with multiple tokens.
+   *
+   * @return bool
+   *   Whether the field might be suitable for sorting.
+   */
+  public function isSortable();
+
+  /**
+   * Determine whether a field is suitable for fulltext search.
+   *
+   * Some fields are tokenized for sort and contain a single, all lowercase
+   * value. These fields are not suitable for fulltext search, but there is no
+   * general way to tell them apart from fields that are tokenized into multiple
+   * terms.
+   *
+   * @return bool
+   *   Whether the field might be suitable for fulltext search.
+   */
+  public function isFulltextSearchable();
+
+  /**
+   * Determine whether a field is suitable for filtering.
+   *
+   * Fields suitable for filtering must be non-fulltext.  A case-sensitive is
+   * used.  When searching on this type of field, only full, exact values will
+   * match.
+   *
+   * @return bool
+   *   Whether the field might be suitable for filtering.
+   */
+  public function isFilterable();
+
+}
