diff --git a/config/schema/search_api.processor.schema.yml b/config/schema/search_api.processor.schema.yml index 7fd47a1..92f5224 100644 --- a/config/schema/search_api.processor.schema.yml +++ b/config/schema/search_api.processor.schema.yml @@ -64,6 +64,9 @@ plugin.plugin_configuration.search_api_processor.highlight: highlight: type: string label: 'Defines whether returned fields should be highlighted (always/if returned/never).' + highlight_partial: + type: boolean + label: 'Whether matches in parts of words should be highlighted' plugin.plugin_configuration.search_api_processor.html_filter: type: search_api.fields_processor_configuration diff --git a/src/Plugin/search_api/processor/Highlight.php b/src/Plugin/search_api/processor/Highlight.php index 9d45a73..feebcc7 100644 --- a/src/Plugin/search_api/processor/Highlight.php +++ b/src/Plugin/search_api/processor/Highlight.php @@ -98,6 +98,7 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface { 'excerpt' => TRUE, 'excerpt_length' => 256, 'highlight' => 'always', + 'highlight_partial' => FALSE, 'exclude_fields' => array(), ); } @@ -126,6 +127,12 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface { ), '#default_value' => $this->configuration['highlight'], ); + $form['highlight_partial'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Highlight partial matches'), + '#description' => $this->t('When enabled, matches in parts of words will be highlighted as well.'), + '#default_value' => $this->configuration['highlight_partial'], + ); $form['excerpt'] = array( '#type' => 'checkbox', '#title' => $this->t('Create excerpt'), @@ -437,8 +444,18 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface { // we are requiring a match on a word boundary, make sure $text starts // and ends with a space. $matches = array(); - if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) { - $found_position = $matches[0][1]; + + $found_position = FALSE; + if (!$this->configuration['highlight_partial']) { + $regex = '/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu'; + if (preg_match($regex, ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) { + $found_position = $matches[0][1]; + } + } + else { + $found_position = stripos($text, $key, $look_start[$key]); + } + if ($found_position !== FALSE) { $look_start[$key] = $found_position + 1; // Keep track of which keys we found this time, in case we need to // pass through again to find more text. @@ -552,9 +569,13 @@ class Highlight extends ProcessorPluginBase implements PluginFormInterface { } return implode('', $texts); } - $replace = $this->configuration['prefix'] . '\0' . $this->configuration['suffix']; $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/'))); - $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' '); + // If "Highlight partial matches" is disabled, we only want to highlight + // matches that are complete words. Otherwise, we want all of them. + $boundary = !$this->configuration['highlight_partial'] ? self::$boundary : ''; + $regex = '/' . $boundary . '(?:' . $keys . ')' . $boundary . '/iu'; + $replace = $this->configuration['prefix'] . '\0' . $this->configuration['suffix']; + $text = preg_replace($regex, $replace, ' ' . $text . ' '); return trim($text); } diff --git a/tests/src/Unit/Plugin/Processor/HighlightTest.php b/tests/src/Unit/Plugin/Processor/HighlightTest.php index 42fac9b..ee16954 100644 --- a/tests/src/Unit/Plugin/Processor/HighlightTest.php +++ b/tests/src/Unit/Plugin/Processor/HighlightTest.php @@ -292,6 +292,49 @@ class HighlightTest extends UnitTestCase { } /** + * Tests highlighting of partial matches. + */ + public function testPostprocessSearchResultsHighlightPartial() { + $this->processor->setConfiguration(array('highlight_partial' => TRUE)); + + $query = $this->getMock('Drupal\search_api\Query\QueryInterface'); + $query->expects($this->once()) + ->method('getProcessingLevel') + ->willReturn(QueryInterface::PROCESSING_FULL); + $query->expects($this->atLeastOnce()) + ->method('getKeys') + ->will($this->returnValue(array('#conjunction' => 'AND', 'partial'))); + /** @var \Drupal\search_api\Query\QueryInterface $query */ + + $field = $this->createTestField('body', 'entity:node/body'); + + $this->index->expects($this->atLeastOnce()) + ->method('getFields') + ->will($this->returnValue(array('body' => $field))); + + $this->processor->setIndex($this->index); + + $body_values = array('Some longwordtoshowpartialmatching value'); + $fields = array( + 'entity:node/body' => array( + 'type' => 'text', + 'values' => $body_values, + ), + ); + + $items = $this->createItems($this->index, 1, $fields); + + $results = new ResultSet($query); + $results->setResultItems($items); + $results->setResultCount(1); + + $this->processor->postprocessSearchResults($results); + + $output = $results->getExtraData('highlighted_fields'); + $this->assertEquals('Some longwordtoshowpartialmatching value', $output[$this->itemIds[0]]['body'][0], 'Highlighting is correctly applied to a partial match.'); + } + + /** * Tests field highlighting when previous highlighting is present. */ public function testPostprocessSearchResultsWithPreviousHighlighting() {