/**
  * {@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();
    }
}
Beispiel #3
0
 /**
  * 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'], '<');
     }
 }
Beispiel #5
0
 /**
  * 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;
 }
Beispiel #6
0
  /**
   * 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;
  }
Beispiel #7
0
 /**
  * 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);
 }
Beispiel #9
0
 /**
  * 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;
 }
Beispiel #10
0
 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));
     }
 }
Beispiel #11
0
/**
 * 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;
 }
Beispiel #13
0
/**
 * 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;
 }