/** * {@inheritdoc} */ public function setUp() { parent::setUp(); $this->installSchema('search_api', array('search_api_item', 'search_api_task')); // Create a test server. $this->server = Server::create(array( 'name' => $this->randomString(), 'id' => $this->randomMachineName(), 'status' => 1, 'backend' => 'search_api_test_backend', )); $this->server->save(); // Create a test index. $this->index = Index::create(array( 'name' => $this->randomString(), 'id' => $this->randomMachineName(), 'status' => 1, 'datasources' => array('entity:' . $this->testEntityTypeId), 'tracker' => 'default', 'server' => $this->server->id(), 'options' => array('index_directly' => FALSE), )); $this->index->save(); Utility::getIndexTaskManager()->addItemsAll($this->index); }
/** * {@inheritdoc} */ public function setUp() { parent::setUp(); $this->setUpExampleStructure(); Utility::getIndexTaskManager()->addItemsAll(Index::load($this->indexId)); }
/** * Returns the item IDs for the given entity IDs. * * @param array $entity_ids * An array of entity IDs. * * @return string[] * An array of item IDs. */ protected function getItemIds(array $entity_ids) { $translate_ids = function ($entity_id) { return Utility::createCombinedId('entity:entity_test', $entity_id . ':en'); }; return array_map($translate_ids, $entity_ids); }
/** * {@inheritdoc} */ public function setUp() { parent::setUp(); $this->installSchema('search_api', array('search_api_item', 'search_api_task')); $this->installSchema('system', array('router')); $this->installSchema('user', array('users_data')); $this->setUpExampleStructure(); $this->installConfig(array('search_api_test_db')); Utility::getIndexTaskManager()->addItemsAll($this->getIndex()); }
/** * 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.'); }
/** * Tests the processor's preprocessSearchQuery() method. */ public function testPreprocessSearchQuery() { $index = $this->getMock('Drupal\\search_api\\IndexInterface'); $index->expects($this->any())->method('status')->will($this->returnValue(TRUE)); /** @var \Drupal\search_api\IndexInterface $index */ $this->processor->setIndex($index); $query = Utility::createQuery($index); $keys = array('#conjunction' => 'AND', 'foo', 'bar', 'bar foo'); $query->keys($keys); $this->processor->setConfiguration(array('stopwords' => array('foobar', 'bar', 'barfoo'))); $this->processor->preprocessSearchQuery($query); unset($keys[1]); $this->assertEquals($keys, $query->getKeys()); $results = Utility::createSearchResultSet($query); $this->processor->postprocessSearchResults($results); $this->assertEquals(array('bar'), $results->getIgnoredSearchKeys()); }
/** * Adds a field to a search index. * * The index will not be saved automatically. * * @param \Drupal\search_api\IndexInterface $index * The search index. * @param string $property_name * The property's name. * @param string $type * (optional) The field type. */ protected function addField(IndexInterface $index, $property_name, $type = 'text') { $field_info = array('label' => $property_name, 'type' => $type, 'datasource_id' => 'entity:entity_test', 'property_path' => $property_name); $field = Utility::createField($index, $property_name, $field_info); $index->addField($field); $index->save(); }
/** * Tests whether indexed items are correctly preprocessed. */ public function testProcessIndexItems() { /** @var \Drupal\node\Entity\Node $node */ $node = $this->getMockBuilder('Drupal\node\Entity\Node') ->disableOriginalConstructor() ->getMock(); $body_value = array('Some text value'); $body_field_id = Utility::createCombinedId('entity:node', 'body'); $fields = array( 'search_api_url' => array( 'type' => 'string' ), $body_field_id => array( 'type' => 'text', 'values' => $body_value, ), ); $items = $this->createItems($this->index, 2, $fields, EntityAdapter::createFromEntity($node)); // Process the items. $this->processor->preprocessIndexItems($items); // Check the valid item. $field = $items[$this->itemIds[0]]->getField('search_api_url'); $this->assertEquals(array('http://www.example.com/node/example'), $field->getValues(), 'Valid URL added as value to the field.'); // Check that no other fields were changed. $field = $items[$this->itemIds[0]]->getField($body_field_id); $this->assertEquals($body_value, $field->getValues(), 'Body field was not changed.'); // Check the second item to be sure that all are processed. $field = $items[$this->itemIds[1]]->getField('search_api_url'); $this->assertEquals(array('http://www.example.com/node/example'), $field->getValues(), 'Valid URL added as value to the field in the second item.'); }
/** * {@inheritdoc} */ protected function testType($type) { return Utility::isTextType($type, array('text', 'tokenized_text')); }
/** * Tokenizes an HTML string according to the HTML elements. * * Assigns boost values to the elements' contents accordingly. * * @param string $text * The HTML string to parse, passed by reference. After the method call, the * variable will contain the portion of the string after the current * element, or an empty string (if there is no current element). * @param string|null $active_tag * (optional) The currently active tag, for which a closing tag has to be * found. Internal use only. * @param float $boost * (optional) The currently active boost value. Internal use only. * * @return array * Tokenized text with appropriate scores. */ protected function parseHtml(&$text, $active_tag = NULL, $boost = 1.0) { $ret = array(); while (($pos = strpos($text, '<')) !== FALSE) { if ($boost && $pos > 0) { $value = $this->normalizeText(substr($text, 0, $pos)); if ($value !== '') { $ret[] = Utility::createTextToken($value, $boost); } } $text = substr($text, $pos + 1); preg_match('#^(/?)([-:_a-zA-Z0-9]+)#', $text, $m); $pos = strpos($text, '>'); $empty_tag = $text[$pos - 1] == '/'; $text = substr($text, $pos + 1); if ($m[1]) { // Closing tag. if ($active_tag && $m[2] == $active_tag) { return $ret; } } elseif (!$empty_tag) { // Opening tag => recursive call. $inner_boost = $boost * (isset($this->configuration['tags'][$m[2]]) ? $this->configuration['tags'][$m[2]] : 1); $ret = array_merge($ret, $this->parseHtml($text, $m[2], $inner_boost)); } } if ($text) { $value = $this->normalizeText($text); if ($value !== '') { $ret[] = Utility::createTextToken($value, $boost); } $text = ''; } return $ret; }
/** * Implements the magic __toString() method to simplify debugging. */ public function __toString() { $out = $this->getLabel() . ' [' . $this->getFieldIdentifier() . ']: '; if ($this->isIndexed()) { $out .= 'indexed as type ' . $this->getType(); if (Utility::isTextType($this->getType())) { $out .= ' (boost ' . $this->getBoost() . ')'; } } else { $out .= 'not indexed'; } if ($this->getValues()) { $out .= "\nValues:"; foreach ($this->getValues() as $value) { $value = str_replace("\n", "\n ", "{$value}"); $out .= "\n- " . $value; } } return $out; }
/** * Checks whether the index switched tracker plugin and reacts accordingly. * * Used as a helper method in postSave(). Should only be called when the index * was enabled before the change and remained so. * * @param \Drupal\search_api\IndexInterface $original * The previous version of the index. */ protected function reactToTrackerSwitch(IndexInterface $original) { if ($this->tracker != $original->getTrackerId()) { $index_task_manager = Utility::getIndexTaskManager(); if ($original->hasValidTracker()) { $index_task_manager->stopTracking($this); } if ($this->hasValidTracker()) { $index_task_manager->startTracking($this); } } }
/** * 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; }
/** * Makes sure that locked processors and fields are actually enabled/indexed. * * @return bool * TRUE if any changes were made to the index as a result of this operation; * FALSE otherwise. */ public function applyLockedConfiguration() { $change = FALSE; // Obviously, we should first check for locked processors, because they // might add new locked properties. foreach ($this->getProcessors(FALSE) as $processor_id => $processor) { if ($processor->isLocked() && !isset($this->options['processors'][$processor_id])) { $this->options['processors'][$processor_id] = array('processor_id' => $processor_id, 'weights' => array(), 'settings' => array()); $change = TRUE; } } if ($change) { $this->resetCaches(FALSE); } $datasource_ids = array_merge(array(NULL), $this->getDatasourceIds()); foreach ($datasource_ids as $datasource_id) { foreach ($this->getPropertyDefinitions($datasource_id) as $key => $property) { if ($property instanceof PropertyInterface && $property->isLocked()) { $settings = $property->getFieldSettings(); if (empty($settings['type'])) { $mapping = Utility::getFieldTypeMapping(); $type = $property->getDataType(); $settings['type'] = !empty($mapping[$type]) ? $mapping[$type] : 'string'; } $this->options['fields'][$key] = $settings; $change = TRUE; } } } return $change; }
/** * Implements SearchApiAutocompleteInterface::getAutocompleteSuggestions(). */ public function getAutocompleteSuggestions(QueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) { $settings = isset($this->configuration['autocomplete']) ? $this->configuration['autocomplete'] : array(); $settings += array( 'suggest_suffix' => TRUE, 'suggest_words' => TRUE, ); // If none of these options is checked, the user apparently chose a very // roundabout way of telling us he doesn't want autocompletion. if (!array_filter($settings)) { return array(); } $index = $query->getIndex(); $db_info = $this->getIndexDbInfo($index); if (empty($db_info['field_tables'])) { throw new SearchApiException(new FormattableMarkup('Unknown index @id.', array('@id' => $index->id()))); } $fields = $this->getFieldInfo($index); $suggestions = array(); $passes = array(); $incomplete_like = NULL; // Make the input lowercase as the indexed data is (usually) also all // lowercase. $incomplete_key = Unicode::strtolower($incomplete_key); $user_input = Unicode::strtolower($user_input); // Decide which methods we want to use. if ($incomplete_key && $settings['suggest_suffix']) { $passes[] = 1; $incomplete_like = $this->database->escapeLike($incomplete_key) . '%'; } if ($settings['suggest_words'] && (!$incomplete_key || strlen($incomplete_key) >= $this->configuration['min_chars'])) { $passes[] = 2; } if (!$passes) { return array(); } // We want about half of the suggestions from each enabled method. $limit = $query->getOption('limit', 10); $limit /= count($passes); $limit = ceil($limit); // Also collect all keywords already contained in the query so we don't // suggest them. $keys = preg_split('/[^\p{L}\p{N}]+/u', $user_input, -1, PREG_SPLIT_NO_EMPTY); $keys = array_combine($keys, $keys); if ($incomplete_key) { $keys[$incomplete_key] = $incomplete_key; } foreach ($passes as $pass) { if ($pass == 2 && $incomplete_key) { $query->keys($user_input); } // To avoid suggesting incomplete words, we have to temporarily disable // the "partial_matches" option. (There should be no way we'll save the // server during the createDbQuery() call, so this should be safe.) $options = $this->options; $this->options['partial_matches'] = FALSE; $db_query = $this->createDbQuery($query, $fields); $this->options = $options; // We need a list of all current results to match the suggestions against. // However, since MySQL doesn't allow using a temporary table multiple // times in one query, we regrettably have to do it this way. $fulltext_fields = $this->getQueryFulltextFields($query); if (count($fulltext_fields) > 1) { $all_results = $db_query->execute()->fetchCol(); // Compute the total number of results so we can later sort out matches // that occur too often. $total = count($all_results); } else { $table = $this->getTemporaryResultsTable($db_query); if (!$table) { return array(); } $all_results = $this->database->select($table, 't') ->fields('t', array('item_id')); $total = $this->database->query("SELECT COUNT(item_id) FROM {{$table}}")->fetchField(); } $max_occurrences = $this->getConfigFactory()->get('search_api_db.settings')->get('autocomplete_max_occurrences'); $max_occurrences = max(1, floor($total * $max_occurrences)); if (!$total) { if ($pass == 1) { return NULL; } continue; } /** @var \Drupal\Core\Database\Query\SelectInterface|null $word_query */ $word_query = NULL; foreach ($fulltext_fields as $field) { if (!isset($fields[$field]) || !Utility::isTextType($fields[$field]['type'])) { continue; } $field_query = $this->database->select($fields[$field]['table'], 't'); $field_query->fields('t', array('word', 'item_id')) ->condition('item_id', $all_results, 'IN'); if ($pass == 1) { $field_query->condition('word', $incomplete_like, 'LIKE') ->condition('word', $keys, 'NOT IN'); } if (!isset($word_query)) { $word_query = $field_query; } else { $word_query->union($field_query); } } if (!$word_query) { return array(); } $db_query = $this->database->select($word_query, 't'); $db_query->addExpression('COUNT(DISTINCT item_id)', 'results'); $db_query->fields('t', array('word')) ->groupBy('word') ->having('results <= :max', array(':max' => $max_occurrences)) ->orderBy('results', 'DESC') ->range(0, $limit); $incomp_len = strlen($incomplete_key); foreach ($db_query->execute() as $row) { $suffix = ($pass == 1) ? substr($row->word, $incomp_len) : ' ' . $row->word; $suggestions[] = array( 'suggestion_suffix' => $suffix, 'results' => $row->results, ); } } return $suggestions; }
/** * 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.'); } }
/** * 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 getFields($extract = TRUE) { if ($extract && !$this->fieldsExtracted) { $data_type_fallback_mapping = Utility::getDataTypeFallbackMapping($this->index); foreach (array(NULL, $this->getDatasourceId()) as $datasource_id) { $fields_by_property_path = array(); foreach ($this->index->getFieldsByDatasource($datasource_id) as $field_id => $field) { // Don't overwrite fields that were previously set. if (empty($this->fields[$field_id])) { $this->fields[$field_id] = clone $field; $field_data_type = $this->fields[$field_id]->getType(); // If the field data type is in the fallback mapping list, then use // the fallback type as field type. if (isset($data_type_fallback_mapping[$field_data_type])) { $this->fields[$field_id]->setType($data_type_fallback_mapping[$field_data_type]); } $fields_by_property_path[$field->getPropertyPath()] = $this->fields[$field_id]; } } if ($datasource_id && $fields_by_property_path) { try { Utility::extractFields($this->getOriginalObject(), $fields_by_property_path); } catch (SearchApiException $e) { // If we couldn't load the object, just log an error and fail // silently to set the values. watchdog_exception('search_api', $e); } } } $this->fieldsExtracted = TRUE; } return $this->fields; }
/** * {@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(); } }
/** * Tests preprocessing search items with an exclusive filter. */ public function testFilterExclusive() { $configuration['roles'] = array('editor' => 'editor'); $configuration['default'] = 1; $this->processor->setConfiguration($configuration); $this->processor->preprocessIndexItems($this->items); $this->assertTrue(empty($this->items[Utility::createCombinedId('entity:user', '1:en')]), 'User with editor role was successfully removed.'); $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:user', '2:en')]), 'User without the editor role was not removed.'); $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:node', '1:en')]), 'Node item was not removed.'); }
/** * Tests whether overriding processFilterValue() works correctly. */ public function testProcessFilterValueOverride() { $override = function (&$value) { if (isset($value)) { $value = ''; } }; $this->processor->setMethodOverride('processFilterValue', $override); $query = Utility::createQuery($this->index); $query->condition('text_field', 'foo'); $query->condition('string_field', NULL, '<>'); $query->condition('integer_field', 'bar'); $this->processor->preprocessSearchQuery($query); $expected = array( array('string_field', NULL, '<>'), array('integer_field', 'bar', '='), ); $this->assertEquals($expected, array_merge($query->getFilter()->getFilters()), 'Filters were preprocessed correctly.'); }
/** * {@inheritdoc} */ public function query(array $options = array()) { if (!$this->status()) { throw new SearchApiException('Cannot search on a disabled index.'); } return Utility::createQuery($this, $options); }
/** * 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; }
/** * Retrieves the fulltext fields of the given result items. * * @param \Drupal\search_api\Item\ItemInterface[] $result_items * The results for which fulltext data should be extracted, keyed by item * ID. * @param bool $load * (optional) If FALSE, only field values already present will be returned. * Otherwise, fields will be loaded if necessary. * * @return \Drupal\search_api\Item\FieldInterface[][] * An two-dimensional array of fulltext fields, keyed first by item ID and * then field ID. */ protected function getFulltextFields(array $result_items, $load = TRUE) { // @todo Add some caching, since this will sometimes be called twice for the // same result set. $items = array(); // All the index's fulltext fields, grouped by datasource. $fulltext_fields = array(); foreach ($this->index->getFields() as $field_id => $field) { if (Utility::isTextType($field->getType())) { $fulltext_fields[$field->getDatasourceId()][$field_id] = $field; } } $needs_extraction = array(); foreach ($result_items as $item_id => $result_item) { $datasource_id = $result_item->getDatasourceId(); // Make sure this datasource even has any indexed fulltext fields. if (empty($fulltext_fields[$datasource_id])) { continue; } /** @var \Drupal\search_api\Item\FieldInterface $field */ foreach ($fulltext_fields[$datasource_id] as $field_id => $field) { if ($result_item->getField($field_id, FALSE)) { $items[$item_id][$field_id] = $result_item->getField($field_id, FALSE); } elseif ($load) { $needs_extraction[$item_id][$field->getPropertyPath()] = clone $field; } } } $needs_load = array(); foreach ($needs_extraction as $item_id => $fields) { if (!$result_items[$item_id]->getOriginalObject(FALSE)) { $needs_load[$item_id] = $item_id; } } if ($needs_load) { foreach ($this->index->loadItemsMultiple($needs_load) as $item_id => $object) { $result_items[$item_id]->setOriginalObject($object); unset($needs_load[$item_id]); } } // Remove the fields for all items that couldn't be loaded. $needs_extraction = array_diff_key($needs_extraction, $needs_load); foreach ($needs_extraction as $item_id => $fields) { try { Utility::extractFields($result_items[$item_id]->getOriginalObject(), $fields); foreach ($fields as $field) { $field_id = $field->getFieldIdentifier(); $result_items[$item_id]->setField($field_id, $field); $items[$item_id][$field_id] = $field; } } catch (SearchApiException $e) { // Missing highlighting will be the least problem in this case – just // ignore it. } } return $items; }
/** * Tests field highlighting and excerpts with two items. */ public function testPostprocessSearchResultsWithTwoItems() { $query = $this->getMock('Drupal\\search_api\\Query\\QueryInterface'); $query->expects($this->atLeastOnce())->method('getKeys')->will($this->returnValue(array('#conjunction' => 'OR', 'foo'))); /** @var \Drupal\search_api\Query\QueryInterface $query */ /** @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject $index */ $index = $this->getMock('Drupal\\search_api\\IndexInterface'); $body_field = Utility::createField($index, 'body'); $body_field->setType('text'); $body_field->setDatasourceId('entity:node'); $body_field->setPropertyPath('body'); $index->expects($this->atLeastOnce())->method('getFields')->will($this->returnValue(array('body' => $body_field))); $this->processor->setIndex($index); $body_values = array('Some foo value', 'foo bar'); $fields = array('entity:node/body' => array('type' => 'text', 'values' => $body_values)); $items = $this->createItems($index, 2, $fields); $items[$this->itemIds[1]]->getField('body')->setValues(array('The second item also contains foo in its body.')); $results = Utility::createSearchResultSet($query); $results->setResultItems($items); $results->setResultCount(1); $this->processor->postprocessSearchResults($results); $output = $results->getExtraData('highlighted_fields'); $this->assertEquals('Some <strong>foo</strong> value', $output[$this->itemIds[0]]['body'][0], 'Highlighting is correctly applied to first body field value.'); $this->assertEquals('<strong>foo</strong> bar', $output[$this->itemIds[0]]['body'][1], 'Highlighting is correctly applied to second body field value.'); $this->assertEquals('The second item also contains <strong>foo</strong> in its body.', $output[$this->itemIds[1]]['body'][0], 'Highlighting is correctly applied to second item.'); $excerpt1 = '… Some <strong>foo</strong> value … <strong>foo</strong> bar …'; $excerpt2 = '… The second item also contains <strong>foo</strong> in its body. …'; $this->assertEquals($excerpt1, $items[$this->itemIds[0]]->getExcerpt(), 'Correct excerpt created from two text fields.'); $this->assertEquals($excerpt2, $items[$this->itemIds[1]]->getExcerpt(), 'Correct excerpt created for second item.'); }
/** * {@inheritdoc} */ protected function processFieldValue(&$value, &$type) { $this->prepare(); $type = 'tokenized_text'; $text = $this->simplifyText($value); // Split on spaces. The configured (or default) delimiters have been // replaced by those already in simplifyText(). $arr = explode(' ', $text); $value = array(); foreach ($arr as $token) { if (is_numeric($token) || Unicode::strlen($token) >= $this->configuration['minimum_word_size']) { $value[] = Utility::createTextToken($token); } } }
/** * {@inheritdoc} */ public function search(QueryInterface $query) { $this->checkError(__FUNCTION__); $results = Utility::createSearchResultSet($query); $result_items = array(); $datasources = $query->getIndex()->getDatasources(); /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */ $datasource = reset($datasources); $datasource_id = $datasource->getPluginId(); if ($query->getKeys() && $query->getKeys()[0] == 'test') { $item_id = Utility::createCombinedId($datasource_id, '1'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(2); $item->setExcerpt('test'); $result_items[$item_id] = $item; } elseif ($query->getOption('search_api_mlt')) { $item_id = Utility::createCombinedId($datasource_id, '2'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(2); $item->setExcerpt('test test'); $result_items[$item_id] = $item; } else { $item_id = Utility::createCombinedId($datasource_id, '1'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(1); $result_items[$item_id] = $item; $item_id = Utility::createCombinedId($datasource_id, '2'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(1); $result_items[$item_id] = $item; } $results->setResultCount(count($result_items)); return $results; }
/** * 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))); }
/** * Tests if unpublished nodes are removed from the items list. */ public function testNodeStatus() { $this->assertCount(2, $this->items, '2 nodes in the index.'); $this->processor->preprocessIndexItems($this->items); $this->assertCount(1, $this->items, 'An item was removed from the items list.'); $published_nid = Utility::createCombinedId('entity:node', '2:en'); $this->assertTrue(isset($this->items[$published_nid]), 'Correct item was removed.'); }
/** * 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; }