diff --git a/search_api_solr_datasource.module b/search_api_solr_datasource.module index 252b5ac..bf0c481 100644 --- a/search_api_solr_datasource.module +++ b/search_api_solr_datasource.module @@ -8,20 +8,10 @@ use Drupal\search_api\IndexInterface; use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\Query\ResultSetInterface; +use Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend; use Solarium\QueryType\Select\Query\Query as SolariumQuery; use Solarium\QueryType\Select\Result\Result as SolariumResult; -/** - * Implements hook_search_api_field_type_mapping_alter(). - * - * Add mappings for Solr data types to Search API data types. - */ -function search_api_solr_datasource_search_api_field_type_mapping_alter(array &$mapping) { - $mapping['long'] = 'integer'; - $mapping['tdate'] = 'date'; - $mapping['text'] = 'text'; -} - /** * Implements hook_search_api_solr_field_mapping_alter(). * @@ -55,7 +45,9 @@ function search_api_solr_datasource_search_api_solr_field_mapping_alter(IndexInt // Re-map the indexed fields. foreach ($fields as $raw => $name) { // Ignore the Search API fields. - if (strpos($raw, 'search_api_') === 0) { + if (strpos($raw, 'search_api_') === 0 + || empty($index_fields[$raw]) + || $index_fields[$raw]->getDatasourceId() !== 'solr_document') { continue; } $fields[$raw] = $index_fields[$raw]->getPropertyPath(); @@ -68,8 +60,8 @@ function search_api_solr_datasource_search_api_solr_field_mapping_alter(IndexInt function search_api_solr_datasource_search_api_solr_query_alter(SolariumQuery $solarium_query, QueryInterface $query) { // Do not alter the query if the index does not use the solr_document // datasource. - $datasources = $query->getIndex()->getDatasources(); - if (!isset($datasources['solr_document'])) { + $index = $query->getIndex(); + if (!$index->isValidDatasource('solr_document')) { return; } @@ -78,9 +70,38 @@ function search_api_solr_datasource_search_api_solr_query_alter(SolariumQuery $s $solarium_query->removeFilterQuery('index_id'); // Set requestHandler for the query type. - $config = $query->getIndex()->getDatasource('solr_document')->getConfiguration(); - if (!empty($config['advanced']['request_handler'])) { - $solarium_query->addParam('qt', $config['advanced']['request_handler']); + $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)); + } } } diff --git a/src/Plugin/DataType/SolrDocument.php b/src/Plugin/DataType/SolrDocument.php index efa34f6..6a2dbca 100644 --- a/src/Plugin/DataType/SolrDocument.php +++ b/src/Plugin/DataType/SolrDocument.php @@ -3,9 +3,11 @@ namespace Drupal\search_api_solr_datasource\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_datasource\TypedData\SolrDocumentDefinition; +use Solarium\QueryType\Select\Result\AbstractDocument; /** * Defines the "Solr document" data type. @@ -66,14 +68,42 @@ 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."); } - $field = $this->item->getField($property_name); - if ($field === NULL) { - throw new \InvalidArgumentException("The Solr field $property_name has not been configured in the index."); - } - // Create a new typed data object from the item's field data. + + // 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_datasource\Plugin\DataType\SolrField $plugin */ $plugin = \Drupal::typedDataManager()->getDefinition('solr_field')['class']; - return $plugin::createFromField($field, $property_name, $this); + $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; } /** diff --git a/src/Plugin/DataType/SolrField.php b/src/Plugin/DataType/SolrField.php index aa68ef5..7f49ff3 100644 --- a/src/Plugin/DataType/SolrField.php +++ b/src/Plugin/DataType/SolrField.php @@ -22,11 +22,11 @@ class SolrField extends TypedData implements \IteratorAggregate, TypedDataInterface { /** - * The wrapped Search API Field. + * The field value(s). * - * @var \Drupal\search_api\Item\FieldInterface|null + * @var mixed */ - protected $field; + protected $value; /** * Creates an instance wrapping the given Field. @@ -45,36 +45,18 @@ public static function createFromField(FieldInterface $field, $name, TypedDataIn /** @var \Drupal\search_api_solr_datasource\SolrFieldManagerInterface $field_manager */ $field_manager = \Drupal::getContainer()->get('solr_field.manager'); $server_id = $field->getIndex()->getServerInstance()->id(); - $field_id = $field->getFieldIdentifier(); + $field_id = $field->getPropertyPath(); $definition = $field_manager->getFieldDefinitions($server_id)[$field_id]; $instance = new static($definition, $name, $parent); - $instance->setValue($field); + $instance->setValue($field->getValues()); return $instance; } - /** - * {@inheritdoc} - */ - public function getValue() { - return $this->field; - } - - /** - * {@inheritdoc} - */ - public function setValue($field, $notify = TRUE) { - $this->field = $field; - // Notify the parent of any changes. - if ($notify && isset($this->parent)) { - $this->parent->onChange($this->name); - } - } - /** * {@inheritdoc} */ public function getIterator() { - return isset($this->field) ? $this->field->getIterator() : new \ArrayIterator([]); + return new \ArrayIterator((array) $this->value); } } diff --git a/src/Plugin/search_api/datasource/SolrDocument.php b/src/Plugin/search_api/datasource/SolrDocument.php index 3b476a7..5e6f2bf 100644 --- a/src/Plugin/search_api/datasource/SolrDocument.php +++ b/src/Plugin/search_api/datasource/SolrDocument.php @@ -7,6 +7,7 @@ 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_datasource\SolrDocumentFactoryInterface; use Drupal\search_api_solr_datasource\SolrFieldManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -17,7 +18,7 @@ * @SearchApiDatasource( * id = "solr_document", * label = @Translation("Solr Document"), - * description = @Translation("Exposes external Solr Documents as a datasource."), + * 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 { @@ -101,15 +102,65 @@ public function getSolrFieldManager() { * {@inheritdoc} */ public function getItemId(ComplexDataInterface $item) { - // @todo Implement this. + 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() { - // @todo Handle IndexInterface::getServerInstance() returning NULL. - $fields = $this->getSolrFieldManager()->getFieldDefinitions($this->index->getServerInstance()->id()); + $fields = []; + $server_id = $this->index->getServerId(); + if ($server_id) { + $fields = $this->getSolrFieldManager()->getFieldDefinitions($server_id); + } return $fields; } @@ -117,16 +168,19 @@ public function getPropertyDefinitions() { * {@inheritdoc} */ public function loadMultiple(array $ids) { - // Query the index for the Solr documents. - $query = $this->index->query(['limit' => 1]); - foreach ($ids as $id) { - $query->addCondition('search_api_id', $id); - } - $query->execute(); - $results = $query->getResults()->getResultItems(); $documents = []; - foreach ($results as $id => $result) { - $documents[$id] = $this->solrDocumentFactory->create($result); + 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; } @@ -137,8 +191,11 @@ public function loadMultiple(array $ids) { public function defaultConfiguration() { $config = []; $config['id_field'] = ''; - $config['advanced']['request_handler'] = ''; - $config['advanced']['default_query'] = '*:*'; + $config['request_handler'] = ''; + $config['label_field'] = ''; + $config['language_field'] = ''; + $config['url_field'] = ''; + $config['default_query'] = '*:*'; return $config; } @@ -146,13 +203,30 @@ public function defaultConfiguration() { * {@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 a field from your Solr schema that contains unique ID values.'), + '#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'), @@ -162,18 +236,63 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#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['advanced']['request_handler'], + '#default_value' => $this->configuration['request_handler'], ]; - // @todo Figure out if we actually need this setting. It was copied over - // from Sarnia, but it seems like in D8 Search API Solr defaults to - // selecting all records. It may not be necessary. - /*$form['advanced']['default_query'] = [ + $form['advanced']['default_query'] = [ '#type' => 'textfield', '#title' => $this->t('Default query'), - '#description' => $this->t("Enter a default query parameter. This may only be necessary if a default query cannot be specified in the solrconfig.xml."), - '#default_value' => $this->configuration['advanced']['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/SolrFieldManager.php b/src/SolrFieldManager.php index 887a795..1a42c0d 100644 --- a/src/SolrFieldManager.php +++ b/src/SolrFieldManager.php @@ -2,6 +2,7 @@ namespace Drupal\search_api_solr_datasource; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\UseCacheBackendTrait; @@ -59,7 +60,7 @@ public function getFieldDefinitions($server_id) { * Builds the field definitions for a Solr server from its Luke handler. * * @param string $server_id - * The server from which we are retreiving field information. + * The server from which we are retrieving field information. * * @return \Drupal\search_api_solr_datasource\TypedData\SolrFieldDefinitionInterface[] * The array of field definitions for the server, keyed by field name. @@ -72,16 +73,44 @@ protected function buildFieldDefinitions($server_id) { if ($server === NULL) { throw new \InvalidArgumentException('The Search API server could not be loaded.'); } - if (!$server->getBackend() instanceof SolrBackendInterface) { + $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 = $server->getBackend()->getSolrConnector()->getLuke(); - foreach ($luke['fields'] as $label => $definition) { + $luke = $backend->getSolrConnector()->getLuke(); + foreach ($luke['fields'] as $name => $definition) { $field = new SolrFieldDefinition($definition); + $label = Unicode::ucfirst(trim(str_replace('_', ' ', $name))); $field->setLabel($label); - $fields[$label] = $field; + // 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) {