/** * Adds a property to be retrieved. * * Currently doesn't serve any purpose, but might be added to the search query * in the future to help backends that support returning fields determine * which of the fields should actually be returned. * * @param string $combined_property_path * The combined property path of the property that should be retrieved. * * @return $this */ public function addRetrievedProperty($combined_property_path) { list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path); $this->retrievedProperties[$datasource_id][$property_path] = $combined_property_path; return $this; }
/** * Form submission handler for adding a new field to the index. * * @param array $form * An associative array containing the structure of the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. */ public function addField(array $form, FormStateInterface $form_state) { $button = $form_state->getTriggeringElement(); if (!$button) { return; } /** @var \Drupal\Core\TypedData\DataDefinitionInterface $property */ $property = $button['#property']; list($datasource_id, $property_path) = Utility::splitCombinedId($button['#name']); $field = Utility::createFieldFromProperty($this->entity, $property, $datasource_id, $property_path, NULL, $button['#data_type']); $field->setLabel($button['#prefixed_label']); $this->entity->addField($field); $args['%label'] = $field->getLabel(); drupal_set_message($this->t('Field %label was added to the index.', $args)); }
/** * {@inheritdoc} */ public function trackItemsInserted(array $ids) { $transaction = $this->getDatabaseConnection()->startTransaction(); try { $index_id = $this->getIndex()->id(); // Process the IDs in chunks so we don't create an overly large INSERT // statement. foreach (array_chunk($ids, 1000) as $ids_chunk) { $insert = $this->createInsertStatement(); foreach ($ids_chunk as $item_id) { list($datasource_id) = Utility::splitCombinedId($item_id); $insert->values(array('index_id' => $index_id, 'datasource' => $datasource_id, 'item_id' => $item_id, 'changed' => REQUEST_TIME, 'status' => $this::STATUS_NOT_INDEXED)); } $insert->execute(); } } catch (\Exception $e) { watchdog_exception('search_api', $e); $transaction->rollback(); } }
/** * Creates a certain number of test items. * * @param \Drupal\search_api\IndexInterface $index * The index that should be used for the items. * @param int $count * The number of items to create. * @param array[] $fields * The fields to create on the items, with keys being field IDs and values * being arrays with the following information: * - type: The type to set for the field. * - values: (optional) The values to set for the field. * @param \Drupal\Core\TypedData\ComplexDataInterface|null $object * (optional) The object to set on each item as the "original object". * @param array|null $datasource_ids * (optional) An array of datasource IDs to use for the items, in that order * (starting again from the front if necessary). Defaults to using * "entity:node" for all items. * * @return \Drupal\search_api\Item\ItemInterface[] * An array containing the requested test items. */ public function createItems(IndexInterface $index, $count, array $fields, ComplexDataInterface $object = NULL, array $datasource_ids = NULL) { if (!isset($datasource_ids)) { $datasource_ids = array('entity:node'); } $datasource_count = count($datasource_ids); $items = array(); for ($i = 0; $i < $count; ++$i) { $datasource_id = $datasource_ids[$i % $datasource_count]; $this->itemIds[$i] = $item_id = Utility::createCombinedId($datasource_id, $i + 1 . ':en'); $item = Utility::createItem($index, $item_id); if (isset($object)) { $item->setOriginalObject($object); } foreach ($fields as $field_id => $field_info) { // Only add fields of the right datasource. list($field_datasource_id) = Utility::splitCombinedId($field_id); if (isset($field_datasource_id) && $field_datasource_id != $datasource_id) { continue; } $field = Utility::createField($index, $field_id)->setType($field_info['type']); if (isset($field_info['values'])) { $field->setValues($field_info['values']); } $item->setField($field_id, $field); } $item->setFieldsExtracted(TRUE); $items[$item_id] = $item; } return $items; }
/** * Tests whether the rendered_item field is correctly filled by the processor. */ public function testPreprocessIndexItems() { $items = array(); foreach ($this->nodes as $node) { $items[] = array('datasource' => 'entity:node', 'item' => $node->getTypedData(), 'item_id' => $node->id(), 'text' => 'node text' . $node->id()); } $items = $this->generateItems($items); $this->processor->preprocessIndexItems($items); foreach ($items as $key => $item) { list(, $nid) = Utility::splitCombinedId($key); $field = $item->getField('rendered_item'); $this->assertEquals('text', $field->getType(), 'Node item ' . $nid . ' rendered value is identified as text.'); $values = $field->getValues(); // Test that the value is a string (not, e.g., a SafeString object). $this->assertTrue(is_string($values[0]), 'Node item ' . $nid . ' rendered value is a string.'); $this->assertEquals(1, count($values), 'Node item ' . $nid . ' rendered value is a single value.'); // These tests rely on the template not changing. However, if we'd only // check whether the field values themselves are included, there could // easier be false positives. For example the title text was present even // when the processor was broken, because the schema metadata was also // adding it to the output. $this->assertTrue(substr_count($values[0], 'view-mode-full') > 0, 'Node item ' . $nid . ' rendered in view-mode "full".'); $this->assertTrue(substr_count($values[0], 'field--name-title') > 0, 'Node item ' . $nid . ' has a rendered title field.'); $this->assertTrue(substr_count($values[0], '>' . $this->nodes[$nid]->label() . '<') > 0, 'Node item ' . $nid . ' has a rendered title inside HTML-Tags.'); $this->assertTrue(substr_count($values[0], '>Member for<') > 0, 'Node item ' . $nid . ' has rendered member information HTML-Tags.'); $this->assertTrue(substr_count($values[0], '>' . $this->nodes[$nid]->get('body')->getValue()[0]['value'] . '<') > 0, 'Node item ' . $nid . ' has rendered content inside HTML-Tags.'); } }
/** * {@inheritdoc} */ public function loadItemsMultiple(array $item_ids, $group_by_datasource = FALSE) { $items_by_datasource = array(); foreach ($item_ids as $item_id) { list($datasource_id, $raw_id) = Utility::splitCombinedId($item_id); $items_by_datasource[$datasource_id][$item_id] = $raw_id; } $items = array(); foreach ($items_by_datasource as $datasource_id => $raw_ids) { try { foreach ($this->getDatasource($datasource_id)->loadMultiple($raw_ids) as $raw_id => $item) { $id = Utility::createCombinedId($datasource_id, $raw_id); if ($group_by_datasource) { $items[$datasource_id][$id] = $item; } else { $items[$id] = $item; } } } catch (SearchApiException $e) { watchdog_exception('search_api', $e); } } return $items; }
/** * Creates a description for an aggregated field. * * @param array $field_definition * The settings of the aggregated field. * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties * All available properties on the index, keyed by combined ID. * @param string[] $datasource_label_prefixes * The label prefixes for all datasources. * * @return string * A description for the given aggregated field. */ protected function fieldDescription(array $field_definition, array $properties, array $datasource_label_prefixes) { $fields = array(); foreach ($field_definition['fields'] as $combined_id) { list($datasource_id, $property_path) = Utility::splitCombinedId($combined_id); $label = $property_path; if (isset($properties[$combined_id])) { $label = $properties[$combined_id]->getLabel(); } $fields[] = $datasource_label_prefixes[$datasource_id] . $label; } $type = $this->getTypes()[$field_definition['type']]; return $this->t('A @type aggregation of the following fields: @fields.', array('@type' => $type, '@fields' => implode(', ', $fields))); }
/** * Retrieves the property path of the parent property. * * @return string|null * The property path of the parent property. */ protected function getParentPath() { if (!isset($this->parentPath)) { $combined_property_path = $this->getCombinedPropertyPath(); list(, $property_path) = Utility::splitCombinedId($combined_property_path); list($this->parentPath) = Utility::splitPropertyPath($property_path); } return $this->parentPath; }
/** * {@inheritdoc} */ public function getDatasourceId() { if (!isset($this->datasourceId)) { list($this->datasourceId) = Utility::splitCombinedId($this->id); } return $this->datasourceId; }
/** * Creates a certain number of test items. * * @param \Drupal\search_api\IndexInterface $index * The index that should be used for the items. * @param int $count * The number of items to create. * @param array[] $fields * The fields to create on the items, with keys being combined property * paths and values being arrays with properties to set on the field. * @param \Drupal\Core\TypedData\ComplexDataInterface|null $object * (optional) The object to set on each item as the "original object". * @param array|null $datasource_ids * (optional) An array of datasource IDs to use for the items, in that order * (starting again from the front if necessary). * * @return \Drupal\search_api\Item\ItemInterface[] * An array containing the requested test items. */ public function createItems(IndexInterface $index, $count, array $fields, ComplexDataInterface $object = NULL, array $datasource_ids = array('entity:node')) { $datasource_count = count($datasource_ids); $items = array(); for ($i = 0; $i < $count; ++$i) { $datasource_id = $datasource_ids[$i % $datasource_count]; $this->itemIds[$i] = $item_id = Utility::createCombinedId($datasource_id, $i + 1 . ':en'); $item = Utility::createItem($index, $item_id); if (isset($object)) { $item->setOriginalObject($object); } foreach ($fields as $combined_property_path => $field_info) { list($field_info['datasource_id'], $field_info['property_path']) = Utility::splitCombinedId($combined_property_path); // Only add fields of the right datasource. if (isset($field_info['datasource_id']) && $field_info['datasource_id'] != $datasource_id) { continue; } $field_id = Utility::getNewFieldId($index, $field_info['property_path']); $field = Utility::createField($index, $field_id, $field_info); $item->setField($field_id, $field); } $item->setFieldsExtracted(TRUE); $items[$item_id] = $item; } return $items; }
/** * Allows you to log or alter the items that are indexed. * * Please be aware that generally preventing the indexing of certain items is * deprecated. This is better done with processors, which can easily be * configured and only added to indexes where this behaviour is wanted. * If your module will use this hook to reject certain items from indexing, * please document this clearly to avoid confusion. * * @param \Drupal\search_api\IndexInterface $index * The search index on which items will be indexed. * @param \Drupal\search_api\Item\ItemInterface[] $items * The items that will be indexed. */ function hook_search_api_index_items_alter(\Drupal\search_api\IndexInterface $index, array &$items) { foreach ($items as $item_id => $item) { list(, $raw_id) = \Drupal\search_api\Utility::splitCombinedId($item->getId()); if ($raw_id % 5 == 0) { unset($items[$item_id]); } } drupal_set_message(t('Indexing items on index %index with the following IDs: @ids', array('%index' => $index->label(), '@ids' => implode(', ', array_keys($items))))); }
/** * Constructs a FieldTrait object. * * @param \Drupal\search_api\IndexInterface $index * The field's index. * @param string $field_identifier * The field's combined identifier, with datasource prefix if applicable. */ public function __construct(IndexInterface $index, $field_identifier) { $this->index = $index; $this->fieldIdentifier = $field_identifier; list($this->datasourceId, $this->propertyPath) = Utility::splitCombinedId($field_identifier); }
/** * Tests whether the properties are correctly altered. * * @see \Drupal\search_api\Plugin\search_api\processor\AggregatedField::alterPropertyDefinitions() */ public function testAlterPropertyDefinitions() { $fields = array( 'entity:test1/foo', 'entity:test1/foo:bar', 'entity:test2/foobaz:bla', ); $index_fields = array(); foreach ($fields as $field_id) { $field_object = Utility::createField($this->index, $field_id); list($prefix, $label) = str_replace(':', ' » ', Utility::splitCombinedId($field_id)); $field_object->setLabelPrefix($prefix . ' » '); $field_object->setLabel($label); $index_fields[$field_id] = $field_object; } $this->index->expects($this->any()) ->method('getFields') ->will($this->returnValue($index_fields)); $configuration['fields'] = array( 'search_api_aggregation_1' => array( 'label' => 'Field 1', 'type' => 'union', 'fields' => array( 'entity:test1/foo', 'entity:test1/foo:bar', 'entity:test2/foobaz:bla', ), ), 'search_api_aggregation_2' => array( 'label' => 'Field 2', 'type' => 'max', 'fields' => array( 'entity:test1/foo:bar', ), ), ); $this->processor->setConfiguration($configuration); /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */ $translation = $this->getStringTranslationStub(); $this->processor->setStringTranslation($translation); // Check for modified properties when no data source is given. $properties = array(); $this->processor->alterPropertyDefinitions($properties, NULL); $property_added = array_key_exists('search_api_aggregation_1', $properties); $this->assertTrue($property_added, 'The "search_api_aggregation_1" property was added to the properties.'); if ($property_added) { $this->assertInstanceOf('Drupal\Core\TypedData\DataDefinitionInterface', $properties['search_api_aggregation_1'], 'The "search_api_aggregation_1" property contains a valid data definition.'); if ($properties['search_api_aggregation_1'] instanceof DataDefinitionInterface) { $this->assertEquals('string', $properties['search_api_aggregation_1']->getDataType(), 'Correct data type set in the data definition.'); $this->assertEquals('Field 1', $properties['search_api_aggregation_1']->getLabel(), 'Correct label set in the data definition.'); $description = $translation->translate('A @type aggregation of the following fields: @fields.', array('@type' => 'Union', '@fields' => 'entity » test1 » foo, entity » test1 » foo » bar, entity » test2 » foobaz » bla'));; $this->assertEquals($description, $properties['search_api_aggregation_1']->getDescription(), 'Correct description set in the data definition.'); } } $property_added = array_key_exists('search_api_aggregation_2', $properties); $this->assertTrue($property_added, 'The "search_api_aggregation_2" property was added to the properties.'); if ($property_added) { $this->assertInstanceOf('Drupal\Core\TypedData\DataDefinitionInterface', $properties['search_api_aggregation_2'], 'The "search_api_aggregation_2" property contains a valid data definition.'); if ($properties['search_api_aggregation_2'] instanceof DataDefinitionInterface) { $this->assertEquals('integer', $properties['search_api_aggregation_2']->getDataType(), 'Correct data type set in the data definition.'); $this->assertEquals('Field 2', $properties['search_api_aggregation_2']->getLabel(), 'Correct label set in the data definition.'); $description = $translation->translate('A @type aggregation of the following fields: @fields.', array('@type' => 'Maximum', '@fields' => 'entity » test1 » foo » bar'));; $this->assertEquals($description, $properties['search_api_aggregation_2']->getDescription(), 'Correct description set in the data definition.'); } } // Test whether the properties of specific datasources stay untouched. $properties = array(); /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */ $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface'); $this->processor->alterPropertyDefinitions($properties, $datasource); $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.'); }
/** * {@inheritdoc} */ public function preprocessIndexItems(array &$items) { if (!$items) { return; } if (isset($this->configuration['fields'])) { /** @var \Drupal\search_api\Item\ItemInterface[] $items */ foreach ($items as $item) { foreach ($this->configuration['fields'] as $aggregated_field_id => $aggregated_field) { if ($aggregated_field['label']) { if (!$item->getField($aggregated_field_id, FALSE)) { continue; } // Extract the selected fields to aggregate from the settings. $required_fields = array(); // @todo Don't do this once for every item, compute fields per // datasource right away. foreach ($aggregated_field['fields'] as $field_id_to_aggregate) { // Only include valid and selected fields to aggregate. if (!isset($required_fields[$field_id_to_aggregate]) && !empty($field_id_to_aggregate)) { // Make sure we only get fields from the datasource of the // current item. list($datasource_id) = Utility::splitCombinedId($field_id_to_aggregate); if (!$datasource_id || $datasource_id == $item->getDatasourceId()) { $required_fields[$field_id_to_aggregate] = $field_id_to_aggregate; } } } // Get all the available field values. $given_fields = array(); foreach ($required_fields as $required_field_id) { $field = $item->getField($required_field_id); if ($field && $field->getValues()) { $given_fields[$required_field_id] = $field; unset($required_fields[$required_field_id]); } } $missing_fields = array(); // Get all the missing field values. foreach ($required_fields as $required_field_id) { $field = Utility::createField($this->index, $required_field_id); $missing_fields[$field->getPropertyPath()] = $field; } // Get the value from the original objects in to the fields if ($missing_fields) { Utility::extractFields($item->getOriginalObject(), $missing_fields); } $fields = array_merge($given_fields, $missing_fields); $values = array(); /** @var \Drupal\search_api\Item\FieldInterface[] $fields */ foreach ($fields as $field) { $values = array_merge($values, $field->getValues()); } switch ($aggregated_field['type']) { case 'concat': $values = array(implode("\n\n", $values)); break; case 'sum': $values = array(array_sum($values)); break; case 'count': $values = array(count($values)); break; case 'max': $values = array(max($values)); break; case 'min': $values = array(min($values)); break; case 'first': if ($values) { $values = array(reset($values)); } break; } $item->getField($aggregated_field_id)->setValues($values); } } } } }