diff --git a/config/schema/search_api.views.schema.yml b/config/schema/search_api.views.schema.yml index 93a2cdf..0fb0f47 100644 --- a/config/schema/search_api.views.schema.yml +++ b/config/schema/search_api.views.schema.yml @@ -5,6 +5,17 @@ views.query.search_api_query: search_api_bypass_access: type: boolean label: If the underlying search index has access checks enabled, this option allows you to disable them for this view. + parse_mode: + type: string + label: Chooses how the search keys will be parsed. + +views.query.search_api_entity_query: + type: views_query + label: 'Search API entity query' + mapping: + search_api_bypass_access: + type: boolean + label: If the underlying search index has access checks enabled, this option allows you to disable them for this view. entity_access: type: boolean label: Execute an access check for all result entities. diff --git a/search_api.views.inc b/search_api.views.inc index fa4a810..3098f8c 100644 --- a/search_api.views.inc +++ b/search_api.views.inc @@ -10,16 +10,16 @@ use Drupal\search_api\Entity\Index; use Drupal\search_api\Item\FieldInterface; use Drupal\search_api\SearchApiException; use Drupal\search_api\Utility; +use Drupal\search_api\IndexInterface; /** * Implements hook_views_data(). */ function search_api_views_data() { $data = array(); - try { - /** @var \Drupal\search_api\IndexInterface $index */ - foreach (Index::loadMultiple() as $index) { - // Fill in base data. + + foreach (Index::loadMultiple() as $index) { + try { $key = 'search_api_index_' . $index->id(); $table = &$data[$key]; $table['table']['group'] = t('Index @name', array('@name' => $index->label())); @@ -27,79 +27,51 @@ function search_api_views_data() { 'field' => 'search_api_id', 'index' => $index->id(), 'title' => t('Index @name', array('@name' => $index->label())), - 'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->label())), + 'help' => t('Use the @name search index for filtering and retrieving data.', array('@name' => $index->label())), 'query_id' => 'search_api_query', ); + _search_api_views_data_special_fields($table, $index); + _search_api_views_data_searchapi_fields($table, $index); - /** @var \Drupal\search_api\Item\FieldInterface $field */ - foreach ($index->getFields() as $field_id => $field) { - $field_alias = _search_api_views_find_field_alias($field_id, $table); - $field_definition = _search_api_views_get_handlers($field); - if ($field_definition) { - $field_definition += array( - 'title' => $field->getLabel(), - 'help' => $field->getDescription(), - ); - if ($datasource = $field->getDatasource()) { - $field_definition['group'] = $datasource->label(); - } - if ($definition = $field->getDataDefinition()) { - $field_definition['title short'] = $definition->getLabel(); - } - if ($field_id != $field_alias) { - $field_definition['real field'] = $field_id; - } - $table[$field_alias] = $field_definition; + $i = 0; + foreach ($index->getDatasources() as $datasource) { + if (!$entity_type_id = $datasource->getEntityTypeId()) { + continue; } - } + $entity_manager = \Drupal::entityManager(); + if (!$entity_type = $entity_manager->getDefinition($entity_type_id)) { + continue; + } + $storage = $entity_manager->getStorage($entity_type_id); + if (!$base_table = $entity_type->getBaseTable()) { + continue; + } + $entity_tables = array($base_table => $entity_type_id); + // Some entities may not have a data table. + $data_table = $entity_type->getDataTable(); + if ($data_table) { + $entity_tables[$data_table] = $entity_type_id; + } + // Description of the field tables. + // @todo This is the same code as views itself uses, issue about the sql + // assumptions https://www.drupal.org/node/2079019. + $table_mapping = $storage->getTableMapping(); - if (isset($table['search_api_language']['filter']['id'])) { - $table['search_api_language']['filter']['id'] = 'search_api_language'; - $table['search_api_language']['filter']['allow empty'] = FALSE; + foreach ($entity_tables as $base_table => $entity_type_id) { + $table[$base_table]['relationship'] = array( + 'label' => $base_table, + 'title' => $base_table, + 'id' => 'search_api_datasource_entity', + 'base' => $base_table, + ); + } } - - // Add handlers for special fields. - $table['search_api_id']['title'] = t('Entity ID'); - $table['search_api_id']['help'] = t("The entity's ID"); - $table['search_api_id']['field']['id'] = 'numeric'; - $table['search_api_id']['sort']['id'] = 'search_api'; - - $table['search_api_datasource']['title'] = t('Datasource'); - $table['search_api_datasource']['help'] = t("The data source ID"); - $table['search_api_datasource']['field']['id'] = 'standard'; - $table['search_api_datasource']['filter']['id'] = 'search_api_datasource'; - $table['search_api_datasource']['sort']['id'] = 'search_api'; - - $table['search_api_relevance']['group'] = t('Search'); - $table['search_api_relevance']['title'] = t('Relevance'); - $table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query'); - $table['search_api_relevance']['field']['type'] = 'decimal'; - $table['search_api_relevance']['field']['id'] = 'numeric'; - $table['search_api_relevance']['field']['click sortable'] = TRUE; - $table['search_api_relevance']['sort']['id'] = 'search_api'; - - $table['search_api_excerpt']['group'] = t('Search'); - $table['search_api_excerpt']['title'] = t('Excerpt'); - $table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms'); - $table['search_api_excerpt']['field']['id'] = 'search_api_excerpt'; - - $table['search_api_fulltext']['group'] = t('Search'); - $table['search_api_fulltext']['title'] = t('Fulltext search'); - $table['search_api_fulltext']['help'] = t('Search several or all fulltext fields at once.'); - $table['search_api_fulltext']['filter']['id'] = 'search_api_fulltext'; - $table['search_api_fulltext']['argument']['id'] = 'search_api_fulltext'; - - $table['search_api_more_like_this']['group'] = t('Search'); - $table['search_api_more_like_this']['title'] = t('More like this'); - $table['search_api_more_like_this']['help'] = t('Find similar content.'); - $table['search_api_more_like_this']['argument']['id'] = 'search_api_more_like_this'; - - // @todo Add an "All taxonomy terms" contextual filter (if applicable). } + catch (\Exception $e) { + watchdog_exception('search_api', $e); + } } - catch (Exception $e) { - watchdog_exception('search_api', $e); - } + return $data; } @@ -112,6 +84,10 @@ function search_api_views_plugins_cache_alter(array &$plugins) { /** @var \Drupal\search_api\IndexInterface $index */ foreach (Index::loadMultiple() as $index) { $bases[] = 'search_api_index_' . $index->id(); + $i = 0; + foreach ($index->getDatasources() as $datasource) { + $bases[] = 'search_api_index_entity_' . sprintf("%02d", ++$i) . '_' . $index->id(); + } } $plugins['search_api']['base'] = $bases; } @@ -125,6 +101,10 @@ function search_api_views_plugins_row_alter(array &$plugins) { /** @var \Drupal\search_api\IndexInterface $index */ foreach (Index::loadMultiple() as $index) { $bases[] = 'search_api_index_' . $index->id(); + $i = 0; + foreach ($index->getDatasources() as $datasource) { + $bases[] = 'search_api_index_entity_' . sprintf("%02d", ++$i) . '_' . $index->id(); + } } $plugins['search_api']['base'] = $bases; } @@ -321,3 +301,108 @@ function _search_api_views_handler_mapping() { return $mapping; } + +/** + * Define the special fields for this index. + * + * @param array $table + * An array describing the fields. + * @param \Drupal\search_api\IndexInterface $index + * The index we're adding fields for. + */ +function _search_api_views_data_special_fields(&$table, IndexInterface $index) { + // Add handlers for special fields. + $table['search_api_id']['title'] = t('Search API ID'); + $table['search_api_id']['help'] = t("The entity's encoded Search API ID"); + $table['search_api_id']['field']['id'] = 'standard'; + $table['search_api_id']['sort']['id'] = 'search_api_sort'; + + $table['search_api_datasource']['title'] = t('Datasource'); + $table['search_api_datasource']['help'] = t("The data source ID"); + $table['search_api_datasource']['field']['id'] = 'standard'; + // @todo Enable filtering on datasource. + // $table['search_api_datasource']['sort']['id'] = 'search_api_sort'; + + $table['search_api_relevance']['group'] = t('Search'); + $table['search_api_relevance']['title'] = t('Relevance'); + $table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query'); + $table['search_api_relevance']['field']['type'] = 'decimal'; + $table['search_api_relevance']['field']['id'] = 'numeric'; + $table['search_api_relevance']['field']['click sortable'] = TRUE; + $table['search_api_relevance']['sort']['id'] = 'search_api_sort'; + + $table['search_api_excerpt']['group'] = t('Search'); + $table['search_api_excerpt']['title'] = t('Excerpt'); + $table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms'); + $table['search_api_excerpt']['field']['id'] = 'search_api_excerpt'; + + $table['search_api_fulltext']['group'] = t('Search'); + $table['search_api_fulltext']['title'] = t('Fulltext search'); + $table['search_api_fulltext']['help'] = t('Search several or all fulltext fields at once.'); + $table['search_api_fulltext']['filter']['id'] = 'search_api_fulltext'; + $table['search_api_fulltext']['argument']['id'] = 'search_api_fulltext'; + + $table['search_api_more_like_this']['group'] = t('Search'); + $table['search_api_more_like_this']['title'] = t('More like this'); + $table['search_api_more_like_this']['help'] = t('Find similar content.'); + $table['search_api_more_like_this']['argument']['id'] = 'search_api_more_like_this'; +} + +/** + * Add the fields for this index as views fields. + * + * @param $table + * An array describing the fields. + * @param \Drupal\search_api\IndexInterface $index + * The index we're adding fields for. + */ +function _search_api_views_data_searchapi_fields(&$table, IndexInterface $index) { + foreach ($index->getFields() as $field_id => $field) { + // Fields specifically + // + // $field->propertyPath could be run through to make sure + // key does not already exist if it can possibly be duplicated. + // + $id = $field->getType(); + switch ($id) { + case 'integer': + $type = 'integer'; + $id = 'numeric'; + break; + case 'text': + $type = ''; + $id = 'markup'; + // $format REQUIRED + break; + case 'date': + $type = ''; + $id = 'date'; + break; + case 'string': + default: + $type = ''; + $id = 'standard'; + break; + } + + // @todo + // We need something to flag up if a field is going to be + // single or multiple. + if ($field->getPropertyPath() == 'field_tags:entity:name') { + $id = 'standard_list'; + } + // ENDS + + $table['search_api_item_' . $field->getPropertyPath()] = array( + 'title' => $field->getLabel(), + 'help' => $field->getDescription(), + 'field' => array( + 'id' => $id, + 'real field' => $field_id, + ), + ); + if ($type) { + $table['search_api_item_' . $field->getPropertyPath()]['type'] = $type; + } + } +} diff --git a/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml b/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml index 9404bd0..14154b7 100644 --- a/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml +++ b/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml @@ -33,7 +33,6 @@ display: type: search_api_query options: search_api_bypass_access: false - entity_access: false parse_mode: terms exposed_form: type: basic diff --git a/src/Plugin/views/field/StandardList.php b/src/Plugin/views/field/StandardList.php new file mode 100644 index 0000000..c2e775e --- /dev/null +++ b/src/Plugin/views/field/StandardList.php @@ -0,0 +1,33 @@ +getStorage('search_api_index')->load($index_id); + } + return Index::load($index_id); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + protected function addResults(array $results, ViewExecutable $view) { + $rows = $this->retrieveItemResults($results); + $this->loadResultEntities($rows); + $view->result = array_values($rows); + } + + /** + * {@inheritdoc} + */ + protected function loadResultEntities(&$results) { + foreach ($results as $item_id => $result) { + try { + $entity = $this->index->loadItem($result->search_api_id)->getValue(); + if (!empty($this->options['entity_access'])) { + if (!$entity->access('view')) { + continue; + } + } + $results[$item_id]->_entity = $entity; + } + catch (\Exception $e) { + // No entity so no wrapper to getValue(). + } + } + } +} + + diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php index da6e85f..50c3028 100644 --- a/src/Plugin/views/query/SearchApiQuery.php +++ b/src/Plugin/views/query/SearchApiQuery.php @@ -143,7 +143,7 @@ class SearchApiQuery extends QueryPluginBase { try { parent::init($view, $display, $options); $this->retrievedProperties = array(); - $this->index = self::getIndexFromTable($view->storage->get('base_table')); + $this->index = static::getIndexFromTable($view->storage->get('base_table')); if (!$this->index) { $this->abort(new FormattableMarkup('View %view is not based on Search API but tries to use its query plugin.', array('%view' => $view->storage->label()))); } @@ -167,12 +167,40 @@ class SearchApiQuery extends QueryPluginBase { * * @return $this */ - public function addField($combined_property_path) { + public function addRetrievedProperty($combined_property_path) { $this->retrievedProperties[$combined_property_path] = TRUE; return $this; } /** + * Adds a field to the table. + * + * This replicates the interface of Views' default SQL backend to simplify + * the Views integration of the Search API. If you are writing Search + * API-specific Views code, you should better use the addRetrievedProperty() + * method. + * + * @param string|null $table + * Ignored. + * @param string $field + * The combined property path of the property that should be retrieved. + * @param string $alias + * (optional) Ignored. + * @param array $params + * (optional) Ignored. + * + * @return string + * The name that this field can be referred to as (always $field). + * + * @see \Drupal\views\Plugin\views\query\Sql::addField() + * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::addField() + */ + public function addField($table, $field, $alias = '', $params = array()) { + $this->retrievedProperties[$field] = TRUE; + return $field; + } + + /** * {@inheritdoc} */ public function defineOptions() { @@ -441,22 +469,142 @@ class SearchApiQuery extends QueryPluginBase { * The executed view. */ protected function addResults(array $results, ViewExecutable $view) { - /** @var \Drupal\views\ResultRow[] $rows */ - $rows = array(); - $missing = array(); - - if (!empty($this->configuration['entity_access'])) { - $items = $this->index->loadItemsMultiple(array_keys($results)); - $results = array_intersect_key($results, $items); - /** @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter $item */ - foreach ($items as $item_id => $item) { - if (!$item->getValue()->access('view')) { + $rows = $this->retrieveItemResults($results); + $entity_information = $this->getEntityTableInfo(); + if (!empty($entity_information)) { + $this->loadResultEntities($rows, $view, $entity_information); + } + $view->result = array_values($rows); + array_walk($view->result, function (ResultRow $row, $index) { + $row->index = $index; + }); + } + + /** + * Add relationship. + * + * @todo This is a method called by standard relationship handlers. + * So it's called when a chained entity relationship is added. + * This is probably good enough to add data to load the entity + * when executed. + */ + public function addRelationship($alias, JoinPluginBase $join, $base, $link_point = NULL) { + // @todo What does this do? + if (empty($link_point)) { + $link_point = $this->view->storage->get('base_table'); + } + elseif (!array_key_exists($link_point, $this->relationships)) { + return FALSE; + } + + // Make sure $alias isn't already used; if it, start adding stuff. + $alias_base = $alias; + $count = 1; + while (!empty($this->relationships[$alias])) { + $alias = $alias_base . '_' . $count++; + } + + $this->relationships[$alias] = array( + 'link' => $link_point, + 'table' => $join->table, + 'base' => $base, + ); + + return $alias; + } + + /** + * Adds a query tag. + * + * @todo This is a method called by standard relationship handlers. + */ + public function addTag($tag) { + $this->tags[] = $tag; + } + + /** + * Load the views results as entities. + * + * @param array $results + * An array of results. + * @param \Drupal\views\ViewExecutable $view + * The view where these results are shown. + * @param array $entity_information + */ + protected function loadResultEntities(&$results, ViewExecutable $view, $entity_information) { + // List entities types to load from the relationships on the view. + $entities_to_load = []; + foreach ($entity_information as $join) { + $entities_to_load[$join['entity_type']][] = $join; + } + + foreach ($results as $item_id => $result) { + // Load the entity if it is one of the relationship entities. + list($datasource_id, $raw_id) = Utility::splitCombinedId($item_id); + if (isset($entities_to_load[$this->index->getDatasource($datasource_id)->getEntityTypeId()])) { + $this->loadResultEntity($results, $result, $view->relationship, $entities_to_load); + } + } + } + + /** + * Load the views result as an entity. + * + * @param array $results + * An array of results. + * @param $result + * One item out of the results set. + * @param $relationships + * @param $entities_to_load + */ + private function loadResultEntity(&$results, $result, $relationships, $entities_to_load) { + $item = $this->index->loadItem($result->search_api_id); + if ($item) { + $item_id = $result->search_api_id; + $entity = $item->getValue(); + // Check access to the entity if access check on. + foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { + if ($relationships[$join['base']]->options['entity_access'] && !$entity->access('view')) { + unset($results[$item_id]); + continue; + } + } + // Add entity to all relationships that use it. + // Multiple example: node and node_fields. + foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { + $results[$item_id]->_relationship_entities[$join['relationship_id']] = $entity; + } + } + else { + // No entity. If it is required remove the row. + foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { + if ($relationships[$join['base']]->options['required']) { unset($results[$item_id]); + continue; } } } + } + + /** + * Retrieves items from the results. + * + * @param array $results + * An array of results + * + * @return \Drupal\views\ResultRow[] + * The results as views resultRow objects. + */ + protected function retrieveItemResults(array $results) { + /** @var \Drupal\views\ResultRow[] $rows */ + $rows = array(); - // First off, we try to gather as much property values as possible without + // Views \Drupal\views\Plugin\views\style\StylePluginBase::renderFields() + // uses a numeric results index to key the rendered results. + // The Row::index is the key then used to retrieve these. + $count = 0; + + // First off, we try to gather as many property values as possible without // loading any items. foreach ($results as $item_id => $result) { $datasource_id = $result->getDatasourceId(); @@ -473,57 +621,24 @@ class SearchApiQuery extends QueryPluginBase { $values['search_api_excerpt'] = $result->getExcerpt() ?: ''; // Gather any fields from the search results. - foreach ($result->getFields(FALSE) as $field) { + foreach ($result->getFields() as $field_id => $field) { if ($field->getValues()) { + // @todo so results from fields here are always arrays; even when single. + // What are the different handling in views when something _can_ be multiple, + // and when it should be single? $combined_id = Utility::createCombinedId($field->getDatasourceId(), $field->getPropertyPath()); - $values[$combined_id] = $field->getValues(); + $values[$combined_id] = $field->getValues()[0]; } } - // Check whether we need to extract any properties from the result item. - $missing_fields = array_diff_key($this->retrievedProperties, $values); - if ($missing_fields) { - $missing[$item_id] = array_keys($missing_fields); - if (!is_object($values['_item'])) { - $item_ids[] = $item_id; - } - } + $values['index'] = $count++; - // Save the row values for adding them to the Views result afterwards. // @todo Use a custom sub-class here to also pass the result item object, // or other information? $rows[$item_id] = new ResultRow($values); } - // Load items of those rows which haven't got all field values, yet. - if (!empty($item_ids)) { - foreach ($this->index->loadItemsMultiple($item_ids) as $item_id => $object) { - $results[$item_id]->setOriginalObject($object); - $rows[$item_id]->_item = $object; - } - } - - foreach ($missing as $item_id => $missing_fields) { - /** @var \Drupal\search_api\Item\FieldInterface[] $fields_to_extract */ - $fields_to_extract = array(); - foreach ($missing_fields as $combined_id) { - list($datasource_id, $property_path) = Utility::splitCombinedId($combined_id); - if ($datasource_id == $results[$item_id]->getDatasourceId()) { - $fields_to_extract[$property_path] = Utility::createField($this->index, $combined_id); - } - } - Utility::extractFields($results[$item_id]->getOriginalObject(), $fields_to_extract); - foreach ($fields_to_extract as $field) { - $combined_id = $field->getFieldIdentifier(); - $rows[$item_id]->$combined_id = $field->getValues(); - } - } - - // Finally, add all rows to the Views result set. - $view->result = array_values($rows); - array_walk($view->result, function (ResultRow $row, $index) { - $row->index = $index; - }); + return $rows; } /** @@ -1119,6 +1234,7 @@ class SearchApiQuery extends QueryPluginBase { * concept of "tables", this method implementation does nothing. If you are * writing Search API-specific Views code, there is therefore no reason at all * to call this method. + * See https://www.drupal.org/node/2484565 for more information. * * @return string * An empty string. diff --git a/src/Plugin/views/relationship/DatasourceEntity.php b/src/Plugin/views/relationship/DatasourceEntity.php new file mode 100644 index 0000000..6435483 --- /dev/null +++ b/src/Plugin/views/relationship/DatasourceEntity.php @@ -0,0 +1,62 @@ + 'checkbox', + '#title' => $this->t('Check view access permissions'), + '#description' => $this->t('Enable to check user has permission to view the entity. This prevents users from seeing inappropriate content when the index contains stale data, or doesn\'t provide access checks. However, result counts, paging and other things won\'t work correctly if results are eliminated in this way, so only use this as a last ressort (and in addition to other checks, if possible).'), + '#default_value' => !empty($this->options['required']), + ); + } + + /** + * We don't need to add anything to the query, because this is SQL-specific. + */ + public function query() { + return; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + // @todo: Add the entity as a dependency if there is an entity. + } + +} diff --git a/src/Plugin/views/row/SearchApiRow.php b/src/Plugin/views/row/SearchApiRow.php index b0ba304..8d3ceba 100644 --- a/src/Plugin/views/row/SearchApiRow.php +++ b/src/Plugin/views/row/SearchApiRow.php @@ -24,7 +24,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * * @ViewsRow( * id = "search_api", - * title = @Translation("Rendered Search API item"), + * title = @Translation("Rendered entity"), * help = @Translation("Displays entity of the matching search API item"), * ) */ @@ -118,7 +118,6 @@ class SearchApiRow extends RowPluginBase { */ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { parent::init($view, $display, $options); - $base_table = $view->storage->get('base_table'); $this->index = SearchApiQuery::getIndexFromTable($base_table, $this->getEntityTypeManager()); if (!$this->index) { @@ -248,7 +247,7 @@ class SearchApiRow extends RowPluginBase { public function query() { parent::query(); // @todo Find a better way to ensure that the item is loaded. - $this->view->query->addField('_magic'); + $this->view->query->addRetrievedProperty('_magic'); } } diff --git a/src/Tests/ViewsTest.php b/src/Tests/ViewsTest.php index e89c56c..40befe4 100644 --- a/src/Tests/ViewsTest.php +++ b/src/Tests/ViewsTest.php @@ -24,7 +24,7 @@ class ViewsTest extends WebTestBase { * * @var string[] */ - public static $modules = array('search_api_test_views'); + public static $modules = array('search_api_test_views', 'views_ui'); /** * A search index ID. @@ -182,4 +182,19 @@ class ViewsTest extends WebTestBase { } } + /** + * Test views admin. + */ + public function testViewsAdmin() { + $admin_user = $this->drupalCreateUser(array( + 'administer search_api', + 'access administration pages', + 'administer views', + )); + $this->drupalLogin($admin_user); + $this->insertExampleContent(); + + $this->drupalGet('admin/structure/views/view/search_api_test_views_fulltext'); + } + } diff --git a/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml b/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml index 0031593..a397f64 100644 --- a/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml +++ b/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml @@ -20,7 +20,6 @@ display: type: search_api_query options: search_api_bypass_access: false - entity_access: false parse_mode: terms exposed_form: type: basic