diff --git a/search_api.module b/search_api.module
index 4caef001..ccbd9990 100644
--- a/search_api.module
+++ b/search_api.module
@@ -8,6 +8,7 @@
use Drupal\comment\Entity\Comment;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
@@ -25,6 +26,7 @@
use Drupal\search_api\Task\IndexTaskManager;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutable;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
/**
* Implements hook_help().
@@ -731,3 +733,44 @@ function search_api_form_views_exposed_form_alter(&$form, FormStateInterface $fo
}
}
}
+
+/**
+ * Implements hook_entity_extra_field_info().
+ */
+function search_api_entity_extra_field_info() {
+ $extra = [];
+
+ // Add an extra "excerpt" field to every content entity.
+ $entity_types = \Drupal::entityTypeManager()->getDefinitions();
+ $bundle_info = \Drupal::getContainer()->get('entity_type.bundle.info');
+ foreach ($entity_types as $entity_type_id => $entity_type) {
+ if ($entity_type instanceof ContentEntityType) {
+ $bundles = $bundle_info->getBundleInfo($entity_type_id);
+ foreach ($bundles as $bundle => $data) {
+ $extra[$entity_type_id][$bundle]['display']['search_api_excerpt'] = [
+ 'label' => t('Search result excerpt'),
+ 'description' => t('An excerpt provided by Search API when rendered in a search.'),
+ 'weight' => 100,
+ 'visible' => FALSE,
+ ];
+ }
+ }
+ }
+ return $extra;
+}
+
+/**
+ * Implements hook_entity_view().
+ */
+function search_api_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
+ $excerpt_component = $display->getComponent('search_api_excerpt');
+ if ($excerpt_component !== NULL && isset($build['#search_api_excerpt'])) {
+ $build['search_api_excerpt'] = [
+ '#type' => 'markup',
+ '#markup' => $build['#search_api_excerpt'],
+ '#cache' => [
+ 'contexts' => ['url.query_args']
+ ],
+ ];
+ }
+}
diff --git a/src/Plugin/search_api/processor/RenderedItem.php b/src/Plugin/search_api/processor/RenderedItem.php
index 1f003888..e03978e8 100644
--- a/src/Plugin/search_api/processor/RenderedItem.php
+++ b/src/Plugin/search_api/processor/RenderedItem.php
@@ -269,6 +269,10 @@ public function addFieldValues(ItemInterface $item) {
try {
$build = $datasource->viewItem($item->getOriginalObject(), $view_mode);
+ // Add the excerpt to the render array to allow adding it to view modes.
+ if ($item->getExcerpt()) {
+ $build['#search_api_excerpt'] = $item->getExcerpt();
+ }
$value = (string) $this->getRenderer()->renderPlain($build);
if ($value) {
$field->addValue($value);
diff --git a/src/Plugin/views/row/SearchApiRow.php b/src/Plugin/views/row/SearchApiRow.php
index 42e72159..d583e632 100644
--- a/src/Plugin/views/row/SearchApiRow.php
+++ b/src/Plugin/views/row/SearchApiRow.php
@@ -198,7 +198,14 @@ public function render($row) {
}
try {
- return $this->index->getDatasource($datasource_id)->viewItem($row->_object, $view_mode);
+ $build = $this->index->getDatasource($datasource_id)
+ ->viewItem($row->_object, $view_mode);
+ // Add the excerpt to the render array to allow adding it to view modes.
+ if (isset($row->search_api_excerpt)) {
+ $build['#search_api_excerpt'] = $row->search_api_excerpt;
+ }
+
+ return $build;
}
catch (SearchApiException $e) {
$this->logException($e);
diff --git a/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.article.search_result.yml b/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.article.search_result.yml
new file mode 100644
index 00000000..691f2bf8
--- /dev/null
+++ b/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.article.search_result.yml
@@ -0,0 +1,15 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - core.entity_view_mode.entity_test_mulrev_changed.search_result
+id: entity_test_mulrev_changed.article.search_result
+targetEntityType: entity_test_mulrev_changed
+bundle: article
+mode: search_result
+content:
+ search_api_excerpt:
+ weight: 0
+ region: content
+ settings: { }
+ third_party_settings: { }
diff --git a/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.item.search_result.yml b/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.item.search_result.yml
new file mode 100644
index 00000000..8b2eaecc
--- /dev/null
+++ b/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.item.search_result.yml
@@ -0,0 +1,15 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - core.entity_view_mode.entity_test_mulrev_changed.search_result
+id: entity_test_mulrev_changed.item.search_result
+targetEntityType: entity_test_mulrev_changed
+bundle: item
+mode: search_result
+content:
+ search_api_excerpt:
+ weight: 0
+ region: content
+ settings: { }
+ third_party_settings: { }
diff --git a/tests/search_api_test_excerpt_field/config/install/core.entity_view_mode.entity_test_mulrev_changed.search_result.yml b/tests/search_api_test_excerpt_field/config/install/core.entity_view_mode.entity_test_mulrev_changed.search_result.yml
new file mode 100644
index 00000000..2a2d0a3b
--- /dev/null
+++ b/tests/search_api_test_excerpt_field/config/install/core.entity_view_mode.entity_test_mulrev_changed.search_result.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - entity_test
+id: entity_test_mulrev_changed.search_result
+label: 'Search result'
+targetEntityType: entity_test_mulrev_changed
+cache: true
diff --git a/tests/search_api_test_excerpt_field/config/install/views.view.search_api_test_excerpt_field.yml b/tests/search_api_test_excerpt_field/config/install/views.view.search_api_test_excerpt_field.yml
new file mode 100644
index 00000000..b19e1871
--- /dev/null
+++ b/tests/search_api_test_excerpt_field/config/install/views.view.search_api_test_excerpt_field.yml
@@ -0,0 +1,119 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - search_api.index.database_search_index
+ module:
+ - search_api
+id: search_api_test_excerpt_field
+label: 'Search API Test excerpt field'
+module: views
+description: ''
+tag: ''
+base_table: search_api_index_database_search_index
+base_field: search_api_id
+core: 8.x
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: none
+ options: { }
+ cache:
+ type: none
+ options: { }
+ query:
+ type: search_api_query
+ options:
+ bypass_access: true
+ skip_access: true
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Apply
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: mini
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: ‹‹
+ next: ››
+ style:
+ type: default
+ options:
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: search_api
+ options:
+ view_modes:
+ 'entity:entity_test_mulrev_changed':
+ article: search_result
+ item: search_result
+ fields: { }
+ filters: { }
+ sorts: { }
+ title: 'Search API Test search view caching'
+ header:
+ result:
+ id: result
+ table: views
+ field: result
+ relationship: none
+ group_type: group
+ admin_label: ''
+ empty: true
+ content: 'Displaying @total search results FOO BAR BAZ'
+ plugin_id: result
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ tags: { }
+ page:
+ display_plugin: page
+ id: page
+ display_title: Disabled
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: search-api-test-excerpt-field
+ display_description: ''
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ tags: { }
diff --git a/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml b/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml
new file mode 100644
index 00000000..7c3cf4dd
--- /dev/null
+++ b/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml
@@ -0,0 +1,10 @@
+name: 'Search API Excerpt Field Test'
+type: module
+description: 'Support module for testing the Search API Excerpt field'
+package: Testing
+dependencies:
+ - search_api:search_api
+ - search_api:search_api_test_db
+ - drupal:views
+core: 8.x
+hidden: true
diff --git a/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.module b/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.module
new file mode 100644
index 00000000..1ad14577
--- /dev/null
+++ b/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.module
@@ -0,0 +1,20 @@
+get('search_api_test_excerpt_field', $excerptTemplate);
+ foreach ($results->getResultItems() as $itemId => $item) {
+ $item->setExcerpt(str_replace('{{item_id}}', $itemId, $excerptTemplate));
+ }
+}
diff --git a/tests/src/Functional/ExcerptFieldTest.php b/tests/src/Functional/ExcerptFieldTest.php
new file mode 100644
index 00000000..0c3d89fe
--- /dev/null
+++ b/tests/src/Functional/ExcerptFieldTest.php
@@ -0,0 +1,83 @@
+get('search_api.index_task_manager')
+ ->addItemsAll(Index::load($this->indexId));
+ $this->insertExampleContent();
+ $this->indexItems($this->indexId);
+
+ // Do not use a batch for tracking the initial items after creating an
+ // index when running the tests via the GUI. Otherwise, it seems Drupal's
+ // Batch API gets confused and the test fails.
+ if (!Utility::isRunningInCli()) {
+ \Drupal::state()->set('search_api_use_tracking_batch', FALSE);
+ }
+ }
+
+ /**
+ * Tests that the "Search excerpt" field in entity displays works correctly.
+ */
+ public function testSearchExcerptField() {
+ $assertSession = $this->assertSession();
+
+ $path = '/search-api-test-excerpt-field';
+ $this->drupalGet($path);
+ foreach ($this->ids as $itemId) {
+ $assertSession->pageTextContains("Item $itemId test excerpt");
+ }
+
+ // Visiting the same page a second time retrieves the rendered node from
+ // cache, not using the updated test excerpt template.
+ $stateKey = 'search_api_test_excerpt_field';
+ \Drupal::state()->set($stateKey, 'test--{{item_id}}--excerpt');
+ $this->drupalGet($path);
+ foreach ($this->ids as $itemId) {
+ $assertSession->pageTextContains("Item $itemId test excerpt");
+ $assertSession->pageTextNotContains("test--$itemId--excerpt");
+ }
+
+ // Changing the GET parameters does skip the render cache for the nodes.
+ $this->drupalGet($path, ['query' => ['foo' => 'bar']]);
+ foreach ($this->ids as $itemId) {
+ $assertSession->pageTextContains("test--$itemId--excerpt");
+ $assertSession->pageTextNotContains("Item $itemId test excerpt");
+ }
+ }
+
+}
diff --git a/tests/src/Functional/SearchApiBrowserTestBase.php b/tests/src/Functional/SearchApiBrowserTestBase.php
index 125b3018..e0ad8dd9 100644
--- a/tests/src/Functional/SearchApiBrowserTestBase.php
+++ b/tests/src/Functional/SearchApiBrowserTestBase.php
@@ -7,6 +7,7 @@
use Drupal\search_api\Entity\Server;
use Drupal\search_api\Utility\Utility;
use Drupal\Tests\BrowserTestBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the base class for web tests for Search API.
@@ -24,6 +25,13 @@ abstract class SearchApiBrowserTestBase extends BrowserTestBase {
'search_api_test',
];
+ /**
+ * Set this to TRUE to include "item" and "article" bundles for test entities.
+ *
+ * @var bool
+ */
+ protected static $additionalBundles = FALSE;
+
/**
* An admin user used for this test.
*
@@ -194,4 +202,27 @@ protected function executeTasks() {
$this->assertEquals(0, $task_manager->getTasksCount(), 'No more pending tasks.');
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function initConfig(ContainerInterface $container) {
+ parent::initConfig($container);
+
+ if (!static::$additionalBundles) {
+ return;
+ }
+
+ // This will just set the Drupal state to include the necessary bundles for
+ // our test entity type. Otherwise, fields from those bundles won't be found
+ // and thus removed from the test index. (We can't do it in setUp(), before
+ // calling the parent method, since the container isn't set up at that
+ // point.)
+ $bundles = [
+ 'entity_test_mulrev_changed' => ['label' => 'Entity Test Bundle'],
+ 'item' => ['label' => 'item'],
+ 'article' => ['label' => 'article'],
+ ];
+ \Drupal::state()->set('entity_test_mulrev_changed.bundles', $bundles);
+ }
+
}
diff --git a/tests/src/Functional/ViewsTest.php b/tests/src/Functional/ViewsTest.php
index 0e5146ef..f17bff6a 100644
--- a/tests/src/Functional/ViewsTest.php
+++ b/tests/src/Functional/ViewsTest.php
@@ -12,7 +12,6 @@
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Utility\Utility;
use Drupal\views\Entity\View;
-use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Tests the Views integration of the Search API.
@@ -35,6 +34,11 @@ class ViewsTest extends SearchApiBrowserTestBase {
'views_ui',
];
+ /**
+ * {@inheritdoc}
+ */
+ protected static $additionalBundles = TRUE;
+
/**
* {@inheritdoc}
*/
@@ -997,25 +1001,10 @@ public function testHighlighting() {
$options['query']['search_api_fulltext'] = 'foo';
$this->drupalGet($path, $options);
$this->assertSession()->responseContains('foo bar baz');
- }
- /**
- * {@inheritdoc}
- */
- protected function initConfig(ContainerInterface $container) {
- parent::initConfig($container);
-
- // This will just set the Drupal state to include the necessary bundles for
- // our test entity type. Otherwise, fields from those bundles won't be found
- // and thus removed from the test index. (We can't do it in setUp(), before
- // calling the parent method, since the container isn't set up at that
- // point.)
- $bundles = [
- 'entity_test_mulrev_changed' => ['label' => 'Entity Test Bundle'],
- 'item' => ['label' => 'item'],
- 'article' => ['label' => 'article'],
- ];
- \Drupal::state()->set('entity_test_mulrev_changed.bundles', $bundles);
+ $options['query']['search_api_fulltext'] = 'bar';
+ $this->drupalGet($path, $options);
+ $this->assertSession()->responseContains('foo bar baz');
}
}
diff --git a/tests/src/Kernel/Processor/ProcessorTestBase.php b/tests/src/Kernel/Processor/ProcessorTestBase.php
index af6a68a5..1a77c0bd 100644
--- a/tests/src/Kernel/Processor/ProcessorTestBase.php
+++ b/tests/src/Kernel/Processor/ProcessorTestBase.php
@@ -153,29 +153,49 @@ public function setUp($processor = NULL) {
* @return \Drupal\search_api\Item\ItemInterface[]
* The generated test items.
*/
- public function generateItems(array $items) {
+ protected function generateItems(array $items) {
/** @var \Drupal\search_api\Item\ItemInterface[] $extracted_items */
$extracted_items = [];
- foreach ($items as $item) {
- $id = Utility::createCombinedId($item['datasource'], $item['item_id']);
- $extracted_items[$id] = \Drupal::getContainer()
- ->get('search_api.fields_helper')
- ->createItemFromObject($this->index, $item['item'], $id);
- foreach ([NULL, $item['datasource']] as $datasource_id) {
- foreach ($this->index->getFieldsByDatasource($datasource_id) as $key => $field) {
- /** @var \Drupal\search_api\Item\FieldInterface $field */
- $field = clone $field;
- if (isset($item[$field->getPropertyPath()])) {
- $field->addValue($item[$field->getPropertyPath()]);
- }
- $extracted_items[$id]->setField($key, $field);
- }
- }
+ foreach ($items as $values) {
+ $item = $this->generateItem($values);
+ $extracted_items[$item->getId()] = $item;
}
return $extracted_items;
}
+ /**
+ * Generates a single test item.
+ *
+ * @param array $values
+ * An associative array with the following keys:
+ * - datasource: The datasource plugin ID.
+ * - item: The item object to be indexed.
+ * - item_id: The datasource-specific raw item ID.
+ * - *: Any other keys will be treated as property paths, and their values
+ * as a single value for a field with that property path.
+ *
+ * @return \Drupal\search_api\Item\Item|\Drupal\search_api\Item\ItemInterface
+ * The generated test item.
+ */
+ protected function generateItem(array $values) {
+ $id = Utility::createCombinedId($values['datasource'], $values['item_id']);
+ $item = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createItemFromObject($this->index, $values['item'], $id);
+ foreach ([NULL, $values['datasource']] as $datasource_id) {
+ foreach ($this->index->getFieldsByDatasource($datasource_id) as $key => $field) {
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ $field = clone $field;
+ if (isset($values[$field->getPropertyPath()])) {
+ $field->addValue($values[$field->getPropertyPath()]);
+ }
+ $item->setField($key, $field);
+ }
+ }
+ return $item;
+ }
+
/**
* Indexes all (unindexed) items.
*
diff --git a/tests/src/Kernel/Processor/RenderedItemTest.php b/tests/src/Kernel/Processor/RenderedItemTest.php
index c48d2715..9ae867e0 100644
--- a/tests/src/Kernel/Processor/RenderedItemTest.php
+++ b/tests/src/Kernel/Processor/RenderedItemTest.php
@@ -5,6 +5,7 @@
use Drupal\comment\CommentInterface;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Entity\CommentType;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
@@ -14,6 +15,7 @@
use Drupal\node\Entity\NodeType;
use Drupal\node\NodeInterface;
use Drupal\search_api\Entity\Index;
+use Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface;
use Drupal\search_api\Utility\Utility;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
@@ -256,7 +258,6 @@ public function testAddFieldValues() {
default:
$this->assertTrue(FALSE);
}
-
}
}
@@ -354,6 +355,40 @@ public function testHideRenderedItem() {
}
}
+ /**
+ * Tests that the "Search excerpt" field in entity displays works correctly.
+ */
+ public function testSearchExcerptField() {
+ \Drupal::getContainer()->get('module_installer')
+ ->install(['search_api_test_excerpt_field']);
+ $this->installEntitySchema('entity_view_mode');
+
+ $view_mode = EntityViewDisplay::load('node.article.teaser');
+ $view_mode->set('content', [
+ 'search_api_excerpt' => [
+ 'weight' => 0,
+ 'region' => 'content',
+ ],
+ ]);
+ $view_mode->save();
+
+ $item = $this->generateItem([
+ 'datasource' => 'entity:node',
+ 'item' => $this->nodes[3]->getTypedData(),
+ 'item_id' => 3,
+ ]);
+ $test_value = 'This is the test excerpt value';
+ $item->setExcerpt($test_value);
+
+ $this->processor->addFieldValues($item);
+ $rendered_item = $item->getField('rendered_item');
+
+ $values = $rendered_item->getValues();
+ $this->assertCount(1, $values);
+ $this->assertInstanceOf(TextValueInterface::class, $values[0]);
+ $this->assertContains($test_value, (string) $values[0]);
+ }
+
/**
* Tests whether the property is correctly added by the processor.
*/