/** * {@inheritdoc} */ public function execute() { /** @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */ $facet_source = $this->facet->getFacetSource(); $query_info = $facet_source->getQueryInfo($this->facet); /** @var \Drupal\core_search_facets\FacetsQuery $facet_query */ $facet_query = $facet_source->getFacetQueryExtender(); $tables_joined = []; // Add the filter to the query if there are active values. $active_items = $this->facet->getActiveItems(); foreach ($active_items as $item) { foreach ($query_info['fields'] as $field_info) { // Adds join to the facet query. $facet_query->addFacetJoin($query_info, $field_info['table_alias']); // Adds join to search query, makes sure it is only added once. if (isset($query_info['joins'][$field_info['table_alias']])) { if (!isset($tables_joined[$field_info['table_alias']])) { $tables_joined[$field_info['table_alias']] = TRUE; $join_info = $query_info['joins'][$field_info['table_alias']]; $this->query->join($join_info['table'], $join_info['alias'], $join_info['condition']); } } // Adds facet conditions to the queries. $field = $field_info['table_alias'] . '.' . $field_info['field']; $this->query->condition($field, $item); $facet_query->condition($field, $item); } } }
/** * Lets modules alter the Solarium select query before executing it. * * @param \Solarium\QueryType\Select\Query\Query $solarium_query * The Solarium query object, as generated from the Search API query. * @param \Drupal\search_api\Query\QueryInterface $query * The Search API query object representing the executed search query. */ function hook_search_api_solr_query_alter(\Solarium\QueryType\Select\Query\Query $solarium_query, \Drupal\search_api\Query\QueryInterface $query) { if ($query->getOption('foobar')) { // If the Search API query has a 'foobar' option, remove all sorting options // from the Solarium query. $solarium_query->clearSorts(); } }
/** * Retrieves all options set for this search query. * * The return value is a reference to the options so they can also be altered * this way. * * @return array * An associative array of query options. * * @see \Drupal\search_api\Query\QueryInterface::getOptions() */ public function &getOptions() { if (!$this->shouldAbort()) { return $this->query->getOptions(); } $ret = NULL; return $ret; }
/** * {@inheritdoc} */ public function execute() { /** @var \Drupal\facets\Utility\FacetsDateHandler $date_handler */ $date_handler = \Drupal::getContainer()->get('facets.utility.date_handler'); /** @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */ $facet_source = $this->facet->getFacetSource(); // Gets the last active date, bails if there isn't one. $active_items = $this->facet->getActiveItems(); if (!($active_item = end($active_items))) { return; } // Gets facet query and this facet's query info. /** @var \Drupal\core_search_facets\FacetsQuery $facet_query */ $facet_query = $facet_source->getFacetQueryExtender(); $query_info = $facet_source->getQueryInfo($this->facet); $tables_joined = []; $active_item = $date_handler->extractActiveItems($active_item); foreach ($query_info['fields'] as $field_info) { // Adds join to the facet query. $facet_query->addFacetJoin($query_info, $field_info['table_alias']); // Adds join to search query, makes sure it is only added once. if (isset($query_info['joins'][$field_info['table_alias']])) { if (!isset($tables_joined[$field_info['table_alias']])) { $tables_joined[$field_info['table_alias']] = TRUE; $join_info = $query_info['joins'][$field_info['table_alias']]; $this->query->join($join_info['table'], $join_info['alias'], $join_info['condition']); } } // Adds field conditions to the facet and search query. $field = $field_info['table_alias'] . '.' . $field_info['field']; $this->query->condition($field, $active_item['start']['timestamp'], '>='); $this->query->condition($field, $active_item['end']['timestamp'], '<'); $facet_query->condition($field, $active_item['start']['timestamp'], '>='); $facet_query->condition($field, $active_item['end']['timestamp'], '<'); } }
/** * Extracts the positive keywords used in a search query. * * @param \Drupal\search_api\Query\QueryInterface $query * The query from which to extract the keywords. * * @return string[] * An array of all unique positive keywords used in the query. */ protected function getKeywords(QueryInterface $query) { $keys = $query->getKeys(); if (!$keys) { return array(); } if (is_array($keys)) { return $this->flattenKeysArray($keys); } $keywords_in = preg_split(self::$split, $keys); // Assure there are no duplicates. (This is actually faster than // array_unique() by a factor of 3 to 4.) // Remove quotes from keywords. $keywords = array(); foreach (array_filter($keywords_in) as $keyword) { if ($keyword = trim($keyword, "'\"")) { $keywords[$keyword] = $keyword; } } return $keywords; }
/** * 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; }
/** * Adds a node access filter to a search query, if applicable. * * @param \Drupal\search_api\Query\QueryInterface $query * The query to which a node access filter should be added, if applicable. * @param \Drupal\Core\Session\AccountInterface $account * The user for whom the search is executed. * * @throws \Drupal\search_api\SearchApiException * Thrown if not all necessary fields are indexed on the index. */ protected function addNodeAccess(QueryInterface $query, AccountInterface $account) { // Don't do anything if the user can access all content. if ($account->hasPermission('bypass node access')) { return; } // Gather the affected datasources, grouped by entity type, as well as the // unaffected ones. $affected_datasources = array(); $unaffected_datasources = array(); foreach ($this->index->getDatasources() as $datasource_id => $datasource) { $entity_type = $datasource->getEntityTypeId(); if (in_array($entity_type, array('node', 'comment'))) { $affected_datasources[$entity_type][] = $datasource_id; } else { $unaffected_datasources[] = $datasource_id; } } // The filter structure we want looks like this: // [belongs to other datasource] // OR // ( // [is enabled (or was created by the user, if applicable)] // AND // [grants view access to one of the user's gid/realm combinations] // ) // If there are no "other" datasources, we don't need the nested OR, // however, and can add the "ADD" // @todo Add a filter tag, once they are implemented. if ($unaffected_datasources) { $outer_conditions = $query->createConditionGroup('OR', array('content_access')); $query->addConditionGroup($outer_conditions); foreach ($unaffected_datasources as $datasource_id) { $outer_conditions->addCondition('search_api_datasource', $datasource_id); } $access_conditions = $query->createConditionGroup('AND'); $outer_conditions->addConditionGroup($access_conditions); } else { $access_conditions = $query; } if (!$account->hasPermission('access content')) { unset($affected_datasources['node']); } if (!$account->hasPermission('access comments')) { unset($affected_datasources['comment']); } // If the user does not have the permission to see any content at all, deny // access to all items from affected datasources. if (!$affected_datasources) { // If there were "other" datasources, the existing filter will already // remove all results of node or comment datasources. Otherwise, we should // not return any results at all. if (!$unaffected_datasources) { // @todo More elegant way to return no results? // @todo Now that field IDs can be picked freely, this can theoretically // even fail! Needs to be fixed! $query->addCondition('search_api_language', ''); } return; } // Collect all the required fields that need to be part of the index. $unpublished_own = $account->hasPermission('view own unpublished content'); $enabled_conditions = $query->createConditionGroup('OR', array('content_access_enabled')); foreach ($affected_datasources as $entity_type => $datasources) { foreach ($datasources as $datasource_id) { // If this is a comment datasource, or users cannot view their own // unpublished nodes, a simple filter on "status" is enough. Otherwise, // it's a bit more complicated. $status_field = $this->findField($datasource_id, 'status', 'boolean'); if ($status_field) { $enabled_conditions->addCondition($status_field->getFieldIdentifier(), TRUE); } if ($entity_type == 'node' && $unpublished_own) { $author_field = $this->findField($datasource_id, 'uid', 'integer'); if ($author_field) { $enabled_conditions->addCondition($author_field->getFieldIdentifier(), $account->id()); } } } } $access_conditions->addConditionGroup($enabled_conditions); // Filter by the user's node access grants. $node_grants_field = $this->findField(NULL, 'search_api_node_grants', 'string'); if (!$node_grants_field) { return; } $node_grants_field_id = $node_grants_field->getFieldIdentifier(); $grants_conditions = $query->createConditionGroup('OR', array('content_access_grants')); $grants = node_access_grants('view', $account); foreach ($grants as $realm => $gids) { foreach ($gids as $gid) { $grants_conditions->addCondition($node_grants_field_id, "node_access_{$realm}:{$gid}"); } } // Also add items that are accessible for everyone by checking the "access // all" pseudo grant. $grants_conditions->addCondition($node_grants_field_id, 'node_access__all'); $access_conditions->addConditionGroup($grants_conditions); }
/** * {@inheritdoc} */ public function preprocessSearchQuery(QueryInterface $query) { $keys =& $query->getKeys(); if (isset($keys)) { $this->processKeys($keys); } $filter = $query->getFilter(); $filters =& $filter->getFilters(); $this->processFilters($filters); }
/** * Retrieves the effective fulltext fields from the query. * * Automatically translates a NULL value in the query object to all fulltext * fields in the search index. * * @param \Drupal\search_api\Query\QueryInterface $query * The search query. * * @return string[] * The fulltext fields in which to search for the search keys. * * @see \Drupal\search_api\Query\QueryInterface::getFulltextFields() */ protected function getQueryFulltextFields(QueryInterface $query) { $fulltext_fields = $query->getFulltextFields(); return $fulltext_fields === NULL ? $query->getIndex()->getFulltextFields() : $fulltext_fields; }
public function setSorts(Query $solarium_query, QueryInterface $query, $field_names_single_value = array()) { foreach ($query->getSorts() as $field => $order) { $f = $field_names_single_value[$field]; if (substr($f, 0, 3) == 'ss_') { $f = 'sort_' . substr($f, 3); } $solarium_query->addSort($f, strtolower($order)); } }
/** * Alter the query before executing the query. * * @param \Drupal\views\ViewExecutable $view * The view object about to be processed. * @param \Drupal\search_api\Query\QueryInterface $query * The Search API Views query to be altered. * * @deprecated Use hook_views_query_alter() instead. * * @see hook_views_query_alter() * * @todo Possibly remove this, since hook_views_query_alter() works just as well * (with instanceof check and $query->getSearchApiQuery()). */ function hook_search_api_views_query_alter(\Drupal\views\ViewExecutable $view, \Drupal\search_api\Query\QueryInterface &$query) { if ($view->getPath() === 'search') { $query->setOption('custom_do_magic', TRUE); } }
public function getAutocompleteSuggestions(QueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) { $suggestions = array(); // Reset request handler $this->request_handler = NULL; // Turn inputs to lower case, otherwise we get case sensivity problems. $incomp = Unicode::strtolower($incomplete_key); $index = $query->getIndex(); $field_names = $this->getFieldNames($index); $complete = $query->getOriginalKeys(); // Extract keys $keys = $query->getKeys(); if (is_array($keys)) { $keys_array = array(); while ($keys) { reset($keys); if (!element_child(key($keys))) { array_shift($keys); continue; } $key = array_shift($keys); if (is_array($key)) { $keys = array_merge($keys, $key); } else { $keys_array[$key] = $key; } } $keys = $this->getSolrHelper()->flattenKeys($query->getKeys()); } else { $keys_array = preg_split('/[-\\s():{}\\[\\]\\\\"]+/', $keys, -1, PREG_SPLIT_NO_EMPTY); $keys_array = array_combine($keys_array, $keys_array); } if (!$keys) { $keys = NULL; } // Set searched fields $options = $query->getOptions(); $search_fields = $query->getFulltextFields(); $qf = array(); foreach ($search_fields as $f) { $qf[] = $field_names[$f]; } // Extract filters $fq = $this->createFilterQueries($query->getFilter(), $field_names, $index->getOption('fields', array())); $index_id = $this->getIndexId($index->id()); $fq[] = 'index_id:' . $this->getQueryHelper()->escapePhrase($index_id); if ($this->configuration['site_hash']) { $site_hash = $this->getQueryHelper()->escapePhrase(SearchApiSolrUtility::getSiteHash()); $fq[] = 'hash:' . $site_hash; } // Autocomplete magic $facet_fields = array(); foreach ($search_fields as $f) { $facet_fields[] = $field_names[$f]; } $limit = $query->getOption('limit', 10); $params = array('qf' => $qf, 'fq' => $fq, 'rows' => 0, 'facet' => 'true', 'facet.field' => $facet_fields, 'facet.prefix' => $incomp, 'facet.limit' => $limit * 5, 'facet.mincount' => 1, 'spellcheck' => !isset($this->configuration['autocorrect_spell']) || $this->configuration['autocorrect_spell'] ? 'true' : 'false', 'spellcheck.count' => 1); // Retrieve http method from server options. $http_method = !empty($this->configuration['http_method']) ? $this->configuration['http_method'] : 'AUTO'; $call_args = array('query' => &$keys, 'params' => &$params, 'http_method' => &$http_method); if ($this->request_handler) { $this->setRequestHandler($this->request_handler, $call_args); } $second_pass = !isset($this->configuration['autocorrect_suggest_words']) || $this->configuration['autocorrect_suggest_words']; for ($i = 0; $i < ($second_pass ? 2 : 1); ++$i) { try { // Send search request $this->connect(); $this->moduleHandler->alter('search_api_solr_query', $call_args, $query); $this->preQuery($call_args, $query); $response = $this->solr->search($keys, $params, $http_method); if (!empty($response->spellcheck->suggestions)) { $replace = array(); foreach ($response->spellcheck->suggestions as $word => $data) { $replace[$word] = $data->suggestion[0]; } $corrected = str_ireplace(array_keys($replace), array_values($replace), $user_input); if ($corrected != $user_input) { array_unshift($suggestions, array('prefix' => $this->t('Did you mean') . ':', 'user_input' => $corrected)); } } $matches = array(); if (isset($response->facet_counts->facet_fields)) { foreach ($response->facet_counts->facet_fields as $terms) { foreach ($terms as $term => $count) { if (isset($matches[$term])) { // If we just add the result counts, we can easily get over the // total number of results if terms appear in multiple fields. // Therefore, we just take the highest value from any field. $matches[$term] = max($matches[$term], $count); } else { $matches[$term] = $count; } } } if ($matches) { // Eliminate suggestions that are too short or already in the query. foreach ($matches as $term => $count) { if (strlen($term) < 3 || isset($keys_array[$term])) { unset($matches[$term]); } } // Don't suggest terms that are too frequent (by default in more // than 90% of results). $result_count = $response->response->numFound; $max_occurrences = $result_count * $this->searchApiSolrSettings->get('autocomplete_max_occurrences'); if (($max_occurrences >= 1 || $i > 0) && $max_occurrences < $result_count) { foreach ($matches as $match => $count) { if ($count > $max_occurrences) { unset($matches[$match]); } } } // The $count in this array is actually a score. We want the // highest ones first. arsort($matches); // Shorten the array to the right ones. $additional_matches = array_slice($matches, $limit - count($suggestions), NULL, TRUE); $matches = array_slice($matches, 0, $limit, TRUE); // Build suggestions using returned facets $incomp_length = strlen($incomp); foreach ($matches as $term => $count) { if (Unicode::strtolower(substr($term, 0, $incomp_length)) == $incomp) { $suggestions[] = array('suggestion_suffix' => substr($term, $incomp_length), 'term' => $term, 'results' => $count); } else { $suggestions[] = array('suggestion_suffix' => ' ' . $term, 'term' => $term, 'results' => $count); } } } } } catch (SearchApiException $e) { watchdog_exception('search_api_solr', $e, "%type during autocomplete Solr query: !message in %function (line %line of %file).", array(), WATCHDOG_WARNING); } if (count($suggestions) >= $limit) { break; } // Change parameters for second query. unset($params['facet.prefix']); $keys = trim($keys . ' ' . $incomplete_key); } return $suggestions; }
/** * Alter a search query with a specific tag before it gets executed. * * The hook is invoked after all enabled processors have preprocessed the query. * * @param \Drupal\search_api\Query\QueryInterface $query * The query that will be executed. */ function hook_search_api_query_TAG_alter(\Drupal\search_api\Query\QueryInterface &$query) { // Exclude the node with ID 10 from the search results. $fields = $query->getIndex()->getFields(); foreach ($query->getIndex()->getDatasources() as $datasource_id => $datasource) { if ($datasource->getEntityTypeId() == 'node') { if (isset($fields['nid'])) { $query->addCondition('nid', 10, '<>'); } } } }
/** * {@inheritdoc} */ public function preprocessSearchQuery(QueryInterface $query) { $keys =& $query->getKeys(); if (isset($keys)) { $this->processKeys($keys); } $conditions = $query->getConditionGroup(); $this->processConditions($conditions->getConditions()); }
/** * {@inheritdoc} */ public function search(QueryInterface $query) { // This plugin does not support searching and we therefore just return an empty search result. $results = $query->getResults(); $results->setResultItems(array()); $results->setResultCount(0); return $results; }