/** * Tests custom data types integration. */ public function testCustomDataTypes() { $original_value = $this->entities[1]->get('name')->value; $original_type = $this->index->getFields()['entity:entity_test/name']->getType(); $item = $this->index->loadItem('entity:entity_test/1:en'); $item = Utility::createItemFromObject($this->index, $item, 'entity:entity_test/1:en'); $name_field = $item->getField('entity:entity_test/name'); $processed_value = $name_field->getValues()[0]; $processed_type = $name_field->getType(); $this->assertEqual($processed_value, $original_value, 'The processed value matches the original value'); $this->assertEqual($processed_type, $original_type, 'The processed type matches the original type.'); // Reset the fields on the item and change to the supported data type. $item->setFieldsExtracted(FALSE); $item->setFields(array()); $this->index->getFields()['entity:entity_test/name']->setType('search_api_test_data_type'); $name_field = $item->getField('entity:entity_test/name'); $processed_value = $name_field->getValues()[0]; $processed_type = $name_field->getType(); $this->assertEqual($processed_value, $original_value, 'The processed value matches the original value'); $this->assertEqual($processed_type, 'search_api_test_data_type', 'The processed type matches the new type.'); // Reset the fields on the item and change to the non-supported data type. $item->setFieldsExtracted(FALSE); $item->setFields(array()); $this->index->getFields()['entity:entity_test/name']->setType('search_api_unsupported_test_data_type'); $name_field = $item->getField('entity:entity_test/name'); $processed_value = $name_field->getValues()[0]; $processed_type = $name_field->getType(); $this->assertEqual($processed_value, $original_value, 'The processed value matches the original value'); $this->assertEqual($processed_type, 'integer', 'The processed type matches the fallback type.'); // Reset the fields on the item and change to the data altering data type. $item->setFieldsExtracted(FALSE); $item->setFields(array()); $this->index->getFields()['entity:entity_test/name']->setType('search_api_altering_test_data_type'); $name_field = $item->getField('entity:entity_test/name'); $processed_value = $name_field->getValues()[0]; $processed_type = $name_field->getType(); $this->assertEqual($processed_value, strlen($original_value), 'The processed value matches the altered original value'); $this->assertEqual($processed_type, 'search_api_altering_test_data_type', 'The processed type matches the defined type.'); }
/** * {@inheritdoc} */ public function getQueryTypesForFacet(FacetInterface $facet) { // Get our Facets Field Identifier, which is equal to the Search API Field // identifier. $field_id = $facet->getFieldIdentifier(); // Get the Search API Server. $server = $this->index->getServerInstance(); // Get the Search API Backend. $backend = $server->getBackend(); $fields = $this->index->getFields(); foreach ($fields as $field) { if ($field->getFieldIdentifier() == $field_id) { return $this->getQueryTypesForDataType($backend, $field->getType()); } } throw new InvalidQueryTypeException($this->t("No available query types were found for facet @facet", ['@facet' => $facet->getName()])); }
/** * Removes a field from a search index. * * @param \Drupal\search_api\IndexInterface $search_api_index * The search index. * @param string $field_id * The ID of the field to remove. * * @return \Symfony\Component\HttpFoundation\Response * The response to send to the browser. * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException * Thrown when the field was not found. */ public function removeField(IndexInterface $search_api_index, $field_id) { $fields = $search_api_index->getFields(); if (isset($fields[$field_id])) { try { $search_api_index->removeField($field_id); $search_api_index->save(); } catch (SearchApiException $e) { $args['%field'] = $fields[$field_id]->getLabel(); drupal_set_message($this->t('The field %field is locked and cannot be removed.', $args), 'error'); } } else { throw new NotFoundHttpException(); } // Redirect to the index's "View" page. $url = $search_api_index->toUrl('fields'); return $this->redirect($url->getRouteName(), $url->getRouteParameters()); }
/** * {@inheritdoc} */ public function addIndex(IndexInterface $index) { try { // Create the denormalized table now. $index_table = $this->findFreeTable('search_api_db_', $index->id()); $this->createFieldTable(NULL, array('table' => $index_table)); $db_info = array(); $db_info['field_tables'] = array(); $db_info['index_table'] = $index_table; $this->getKeyValueStore()->set($index->id(), $db_info); // If there are no fields, we are done now. if (!$index->getFields()) { return; } } // The database operations might throw PDO or other exceptions, so we catch // them all and re-wrap them appropriately. catch (\Exception $e) { throw new SearchApiException($e->getMessage(), $e->getCode(), $e); } // If dealing with features or stale data or whatever, we might already have // settings stored for this index. If we have, we should take care to only // change what is needed, so we don't save the server (potentially setting // it to "Overridden") unnecessarily. // The easiest way to do this is by just pretending the index was already // present, but its fields were updated. $this->fieldsUpdated($index); }
/** * Updates the storage tables when the field configuration changes. * * @param \Drupal\search_api\IndexInterface $index * The search index whose fields (might) have changed. * * @return bool * TRUE if the data needs to be reindexed, FALSE otherwise. * * @throws \Drupal\search_api\SearchApiException * Thrown if any exceptions occur internally, e.g., in the database * layer. */ protected function fieldsUpdated(IndexInterface $index) { try { $fields =& $this->configuration['field_tables'][$index->id()]; $new_fields = $index->getFields(); $reindex = FALSE; $cleared = FALSE; $change = FALSE; $text_table = NULL; $denormalized_table = $this->configuration['index_tables'][$index->id()]; foreach ($fields as $field_id => $field) { if (!isset($text_table) && Utility::isTextType($field['type'])) { // Stash the shared text table name for the index. $text_table = $field['table']; } if (!isset($new_fields[$field_id])) { // The field is no longer in the index, drop the data. $this->removeFieldStorage($field_id, $field, $denormalized_table); unset($fields[$field_id]); $change = TRUE; continue; } $old_type = $field['type']; $new_type = $new_fields[$field_id]->getType(); $fields[$field_id]['type'] = $new_type; $fields[$field_id]['boost'] = $new_fields[$field_id]->getBoost(); if ($old_type != $new_type) { $change = TRUE; if ($old_type == 'text' || $new_type == 'text') { // A change in fulltext status necessitates completely clearing the // index. $reindex = TRUE; if (!$cleared) { $cleared = TRUE; $this->deleteAllIndexItems($index); } $this->removeFieldStorage($field_id, $field, $denormalized_table); // Keep the table in $new_fields to create the new storage. continue; } elseif ($this->sqlType($old_type) != $this->sqlType($new_type)) { // There is a change in SQL type. We don't have to clear the index, // since types can be converted. $this->database->schema()->changeField($field['table'], 'value', 'value', $this->sqlType($new_type) + array('description' => "The field's value for this item")); $this->database->schema()->changeField($denormalized_table, $field['column'], $field['column'], $this->sqlType($new_type) + array('description' => "The field's value for this item")); $reindex = TRUE; } elseif ($old_type == 'date' || $new_type == 'date') { // Even though the SQL type stays the same, we have to reindex since // conversion rules change. $reindex = TRUE; } } elseif ($new_type == 'text' && $field['boost'] != $new_fields[$field_id]->getBoost()) { $change = TRUE; if (!$reindex) { $multiplier = $new_fields[$field_id]->getBoost() / $field['boost']; $this->database->update($text_table)->expression('score', 'score * :mult', array(':mult' => $multiplier))->condition('field_name', self::getTextFieldName($field_id))->execute(); } } // Make sure the table and column now exist. (Especially important when // we actually add the index for the first time.) $storage_exists = $this->database->schema()->tableExists($field['table']) && $this->database->schema()->fieldExists($field['table'], 'value'); $denormalized_storage_exists = $this->database->schema()->tableExists($denormalized_table) && $this->database->schema()->fieldExists($denormalized_table, $field['column']); if (!Utility::isTextType($field['type']) && !$storage_exists) { $db = array('table' => $field['table'], 'column' => 'value'); $this->createFieldTable($new_fields[$field_id], $db); } // Ensure that a column is created in the denormalized storage even for // 'text' fields. if (!$denormalized_storage_exists) { $db = array('table' => $denormalized_table, 'column' => $field['column']); $this->createFieldTable($new_fields[$field_id], $db); } unset($new_fields[$field_id]); } $prefix = 'search_api_db_' . $index->id(); // These are new fields that were previously not indexed. foreach ($new_fields as $field_id => $field) { $reindex = TRUE; if (Utility::isTextType($field->getType())) { if (!isset($text_table)) { // If we have not encountered a text table, assign a name for it. $text_table = $this->findFreeTable($prefix . '_', 'text'); } $fields[$field_id]['table'] = $text_table; } else { $fields[$field_id]['table'] = $this->findFreeTable($prefix . '_', $field_id); $this->createFieldTable($field, $fields[$field_id]); } // Always add a column in the denormalized table. $fields[$field_id]['column'] = $this->findFreeColumn($denormalized_table, $field_id); $this->createFieldTable($field, array('table' => $denormalized_table, 'column' => $fields[$field_id]['column'])); $fields[$field_id]['type'] = $field->getType(); $fields[$field_id]['boost'] = $field->getBoost(); $change = TRUE; } // If needed, make sure the text table exists. if (isset($text_table) && !$this->database->schema()->tableExists($text_table)) { $table = array('name' => $text_table, 'module' => 'search_api_db', 'fields' => array('item_id' => array('type' => 'varchar', 'length' => 50, 'description' => 'The primary identifier of the item', 'not null' => TRUE), 'field_name' => array('description' => "The name of the field in which the token appears, or an MD5 hash of the field", 'not null' => TRUE, 'type' => 'varchar', 'length' => 191), 'word' => array('description' => 'The text of the indexed token', 'type' => 'varchar', 'length' => 50, 'not null' => TRUE), 'score' => array('description' => 'The score associated with this token', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0)), 'indexes' => array('word_field' => array(array('word', 20), 'field_name')), 'primary key' => array('item_id', 'field_name', 'word')); $this->database->schema()->createTable($text_table, $table); // Some DBMSs will need a character encoding and collation set. Since // this largely circumvents Drupal's database layer, but isn't integral // enough to fail completely when it doesn't work, we wrap it in a // try/catch, to be on the safe side. try { switch ($this->database->databaseType()) { case 'mysql': $this->database->query("ALTER TABLE {{$text_table}} CONVERT TO CHARACTER SET 'utf8' COLLATE 'utf8_bin'"); break; case 'pgsql': $this->database->query("ALTER TABLE {{$text_table}} ALTER COLUMN word SET DATA TYPE character varying(50) COLLATE \"C\""); break; // @todo Add fixes for other DBMSs. // @todo Add fixes for other DBMSs. case 'oracle': case 'sqlite': case 'sqlsrv': break; } } catch (\PDOException $e) { $vars['%index'] = $index->label(); watchdog_exception('search_api_db', $e, '%type while trying to change collation for the fulltext table of index %index: !message in %function (line %line of %file).', $vars); } } if ($change) { $this->server->save(); } return $reindex; } catch (\Exception $e) { throw new SearchApiException($e->getMessage(), $e->getCode(), $e); } }
/** * {@inheritdoc} */ public function getFields() { return $this->entity->getFields(); }
/** * Creates a list of all indexed field names mapped to their Solr field names. * * @param \Drupal\search_api\IndexInterface $index * The Search Api index. * @param bool $single_value_name * (optional) Whether to return names for fields which store only the first * value of the field. Defaults to FALSE. * @param bool $reset * (optional) Whether to reset the static cache. * * The special fields "search_api_id" and "search_api_relevance" are also * included. Any Solr fields that exist on search results are mapped back to * to their local field names in the final result set. * * @see SearchApiSolrBackend::search() */ public function getFieldNames(IndexInterface $index, $single_value_name = FALSE, $reset = FALSE) { // @todo The field name mapping should be cached per index because custom // queries needs to access it on every query. $subkey = (int) $single_value_name; if (!isset($this->fieldNames[$index->id()][$subkey]) || $reset) { // This array maps "local property name" => "solr doc property name". $ret = array('search_api_id' => 'item_id', 'search_api_relevance' => 'score'); // Add the names of any fields configured on the index. $fields = $index->getFields(); foreach ($fields as $key => $field) { // Generate a field name; this corresponds with naming conventions in // our schema.xml $type = $field->getType(); $type_info = SearchApiSolrUtility::getDataTypeInfo($type); $pref = isset($type_info['prefix']) ? $type_info['prefix'] : ''; $pref .= $single_value_name ? 's' : 'm'; $name = $pref . '_' . $key; $ret[$key] = SearchApiSolrUtility::encodeSolrDynamicFieldName($name); } // Let modules adjust the field mappings. $hook_name = $single_value_name ? 'search_api_solr_single_value_field_mapping' : 'search_api_solr_field_mapping'; $this->moduleHandler->alter($hook_name, $index, $ret); $this->fieldNames[$index->id()][$subkey] = $ret; } return $this->fieldNames[$index->id()][$subkey]; }