You are here

public function SearchApiSolrBackend::search in Search API Solr 8.3

Same name and namespace in other branches
  1. 8 src/Plugin/search_api/backend/SearchApiSolrBackend.php \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend::search()
  2. 8.2 src/Plugin/search_api/backend/SearchApiSolrBackend.php \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend::search()
  3. 4.x src/Plugin/search_api/backend/SearchApiSolrBackend.php \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend::search()

Options on $query prefixed by 'solr_param_' will be passed natively to Solr as query parameter without the prefix. For example you can set the "Minimum Should Match" parameter 'mm' to '75%' like this:

$query
  ->setOption('solr_param_mm', '75%');

Throws

\Drupal\Component\Plugin\Exception\PluginException

\Drupal\search_api_solr\SearchApiSolrException

Overrides BackendSpecificInterface::search

File

src/Plugin/search_api/backend/SearchApiSolrBackend.php, line 1320

Class

SearchApiSolrBackend
Apache Solr backend for search api.

Namespace

Drupal\search_api_solr\Plugin\search_api\backend

Code

public function search(QueryInterface $query) {

  /** @var \Drupal\search_api\Entity\Index $index */
  $index = $query
    ->getIndex();
  $this
    ->finalizeIndex($index);
  if ($query
    ->getOption('solr_streaming_expression', FALSE)) {
    if ($solarium_result = $this
      ->executeStreamingExpression($query)) {

      // Extract results.
      $search_api_result_set = $this
        ->extractResults($query, $solarium_result);
      $this->moduleHandler
        ->alter('search_api_solr_search_results', $search_api_result_set, $query, $solarium_result);
      $this
        ->postQuery($search_api_result_set, $query, $solarium_result);
    }
    else {
      throw new SearchApiSolrException('Streaming expression has no result.');
    }
  }
  else {
    $mlt_options = $query
      ->getOption('search_api_mlt');
    if (!empty($mlt_options)) {
      $query
        ->addTag('mlt');
    }

    // Ensure language(s) condition is set.
    $language_ids = $this
      ->ensureLanguageCondition($query);

    // Get field information.
    $connector = $this
      ->getSolrConnector();
    $solarium_query = NULL;
    $edismax = NULL;
    $index_fields = $index
      ->getFields();
    $index_fields += $this
      ->getSpecialFields($index);
    if ($query
      ->hasTag('mlt')) {
      $solarium_query = $this
        ->getMoreLikeThisQuery($query);
    }
    else {

      // Instantiate a Solarium select query.
      $solarium_query = $connector
        ->getSelectQuery();
      $edismax = $solarium_query
        ->getEDisMax();
      $field_names = $this
        ->getSolrFieldNamesKeyedByLanguage($language_ids, $index);

      // Set searched fields.
      $search_fields = $this
        ->getQueryFulltextFields($query);
      $query_fields_boosted = [];
      foreach ($search_fields as $search_field) {

        /** @var \Drupal\search_api\Item\FieldInterface $field */
        $field = $index_fields[$search_field];
        $boost = $field
          ->getBoost() ? '^' . $field
          ->getBoost() : '';
        $names = [];
        $first_name = reset($field_names[$search_field]);
        if (strpos($first_name, 't') === 0) {

          // Add all language-specific field names. This should work for
          // non Drupal Solr Documents as well which contain only a single
          // name.
          $names = array_values($field_names[$search_field]);
        }
        else {
          $names[] = $first_name;
        }
        foreach (array_unique($names) as $name) {
          $query_fields_boosted[] = $name . $boost;
        }
      }
      $edismax
        ->setQueryFields(implode(' ', $query_fields_boosted));
    }
    $options = $query
      ->getOptions();

    // Set basic filters.
    $filter_queries = $this
      ->getFilterQueries($query, $options);
    foreach ($filter_queries as $id => $filter_query) {
      $solarium_query
        ->createFilterQuery('filters_' . $id)
        ->setQuery($filter_query['query'])
        ->addTags($filter_query['tags']);
    }
    if (!Utility::hasIndexJustSolrDocumentDatasource($index)) {

      // Set the Index (and site) filter.
      $solarium_query
        ->createFilterQuery('index_filter')
        ->setQuery($this
        ->getIndexFilterQueryString($index));
    }
    else {

      // Set requestHandler for the query type, if necessary and configured.
      $config = $index
        ->getDatasource('solr_document')
        ->getConfiguration();
      if (!empty($config['request_handler'])) {
        $solarium_query
          ->addParam('qt', $config['request_handler']);
      }

      // Set the default query, if necessary and configured.
      if (!$solarium_query
        ->getQuery() && !empty($config['default_query'])) {
        $solarium_query
          ->setQuery($config['default_query']);
      }

      // The query builder of Search API Solr Search bases on 'OR' which is
      // the default value for solr, too. But a foreign schema could have a
      // non-default config for q.op. Therefore we need to set it explicitly
      // if not set.
      $params = $solarium_query
        ->getParams();
      if (!isset($params['q.op'])) {
        $solarium_query
          ->addParam('q.op', 'OR');
      }
    }
    $unspecific_field_names = $this
      ->getSolrFieldNames($index);

    // For solr_document datasource, search_api_language might not be mapped.
    if (!empty($unspecific_field_names['search_api_language'])) {
      $solarium_query
        ->createFilterQuery('language_filter')
        ->setQuery($this
        ->createFilterQuery($unspecific_field_names['search_api_language'], $language_ids, 'IN', new Field($index, 'search_api_language'), $options));
    }
    if ($query
      ->hasTag('mlt')) {

      // Set the list of fields to retrieve, but avoid highlighting and
      // different overhead.
      $this
        ->setFields($solarium_query, $query
        ->getOption('search_api_retrieved_field_values', []), $query, FALSE);
    }
    else {

      // Set the list of fields to retrieve.
      $this
        ->setFields($solarium_query, $query
        ->getOption('search_api_retrieved_field_values', []), $query);

      // Set sorts.
      $this
        ->setSorts($solarium_query, $query);

      // Set facet fields. setSpatial() might add more facets.
      $this
        ->setFacets($query, $solarium_query);

      // Handle spatial filters.
      if (isset($options['search_api_location'])) {
        $this
          ->setSpatial($solarium_query, $options['search_api_location'], $query);
      }

      // Handle spatial filters.
      if (isset($options['search_api_rpt'])) {
        $this
          ->setRpt($solarium_query, $options['search_api_rpt'], $query);
      }

      // Handle field collapsing / grouping.
      if (isset($options['search_api_grouping'])) {
        $this
          ->setGrouping($solarium_query, $query, $options['search_api_grouping'], $index_fields, $field_names);
      }

      // Handle spellcheck.
      if (isset($options['search_api_spellcheck'])) {
        $this
          ->setSpellcheck($solarium_query, $query, $options['search_api_spellcheck']);
      }
    }
    if (isset($options['offset'])) {
      $solarium_query
        ->setStart($options['offset']);
    }

    // In previous versions we set a high value for rows if no limit was set
    // in the options. The intention was to retrieve "all" results instead of
    // falling back to Solr's default of 10. But for Solr Cloud it turned out
    // that independent from the real number of documents, Solr seems to
    // allocate rows*shards memory for sorting the distributed result. That
    // could lead to out of memory exceptions. The default limit is now
    // configurable as advanced server option.
    $solarium_query
      ->setRows($query
      ->getOption('limit') ?? $this->configuration['rows'] ?? 10);
    foreach ($options as $option => $value) {
      if (strpos($option, 'solr_param_') === 0) {
        $solarium_query
          ->addParam(substr($option, 11), $value);
      }
    }
    $this
      ->applySearchWorkarounds($solarium_query, $query);
    try {

      // Allow modules to alter the solarium query.
      $this->moduleHandler
        ->alter('search_api_solr_query', $solarium_query, $query);
      $this
        ->preQuery($solarium_query, $query);

      // Since Solr 7.2 the edsimax query parser doesn't allow local
      // parameters anymore. But since we don't want to force all modules that
      // implemented our hooks to re-write their code, we transform the query
      // back into a lucene query. flattenKeys() was adjusted accordingly, but
      // in a backward compatible way.
      // @see https://lucene.apache.org/solr/guide/7_2/solr-upgrade-notes.html#solr-7-2
      if ($edismax) {
        $parse_mode_id = $query
          ->getParseMode()
          ->getPluginId();

        /** @var \Solarium\Core\Query\AbstractQuery $solarium_query */
        $params = $solarium_query
          ->getParams();

        // Extract keys.
        $keys = $query
          ->getKeys();
        $query_fields_boosted = $edismax
          ->getQueryFields() ?? '';
        if (isset($params['defType']) && 'edismax' === $params['defType']) {

          // Edismax was forced via API. In case of parse mode 'direct' we get
          // a string we use as it is. In the other cases we just need to
          // escape the keys.
          $flatten_keys = 'direct' === $parse_mode_id ? $keys : Utility::flattenKeys($keys, [], 'keys');
        }
        else {
          $flatten_keys = Utility::flattenKeys($keys, $query_fields_boosted ? explode(' ', $query_fields_boosted) : [], $parse_mode_id);
        }
        if ('direct' !== $parse_mode_id && strpos($flatten_keys, '-(') === 0) {

          // flattenKeys() always wraps the query in parenthesis. If the query
          // is negated we need to extend it by *:* which logically means 'all
          // documents' except the ones that match the flatten keys.
          $flatten_keys = '*:* ' . $flatten_keys;
        }
        $flatten_query = [];
        if (!Utility::hasIndexJustSolrDocumentDatasource($index) && (!isset($params['defType']) || 'edismax' !== $params['defType'])) {

          // Apply term boosts if configured via a Search API processor if
          // sort by search_api_relevance is present.
          $sorts = $solarium_query
            ->getSorts();
          $relevance_field = reset($field_names['search_api_relevance']);
          if (isset($sorts[$relevance_field])) {
            $flatten_query[] = '{!boost b=boost_document}';

            // @todo Remove condition together with search_api_solr_legacy.
            if (version_compare($connector
              ->getSolrMajorVersion(), '6', '>=')) {

              // Since Solr 6 we could use payload_score!
              $flatten_query[] = Utility::flattenKeysToPayloadScore($keys, $parse_mode_id);
            }
          }
        }
        $flatten_query[] = trim($flatten_keys ?: '*:*');
        $solarium_query
          ->setQuery(implode(' ', $flatten_query));
        if (!isset($params['defType']) || 'edismax' !== $params['defType']) {
          $solarium_query
            ->removeComponent(ComponentAwareQueryInterface::COMPONENT_EDISMAX);
        }
        else {

          // Remove defType 'edismax' because the solarium query still has the
          // edismax component which will set defType itself. We should avoid
          // to have this parameter twice.
          $solarium_query
            ->removeParam('defType');
        }
      }

      // Allow modules to alter the converted solarium query.
      $this->moduleHandler
        ->alter('search_api_solr_converted_query', $solarium_query, $query);

      // Send search request.
      $response = $connector
        ->search($solarium_query, $this
        ->getCollectionEndpoint($index));
      $body = $response
        ->getBody();
      if (200 != $response
        ->getStatusCode()) {
        throw new SearchApiSolrException(strip_tags($body), $response
          ->getStatusCode());
      }
      $search_api_response = new Response($body, $response
        ->getHeaders());
      $solarium_result = $connector
        ->createSearchResult($solarium_query, $search_api_response);

      // Extract results.
      $search_api_result_set = $this
        ->extractResults($query, $solarium_result);
      if ($solarium_result instanceof Result) {

        // Extract facets.
        if ($solarium_facet_set = $solarium_result
          ->getFacetSet()) {
          $search_api_result_set
            ->setExtraData('facet_set', $solarium_facet_set);
          if ($search_api_facets = $this
            ->extractFacets($query, $solarium_result)) {
            $search_api_result_set
              ->setExtraData('search_api_facets', $search_api_facets);
          }
        }

        // Extract spellcheck suggestions.
        if (isset($options['search_api_spellcheck'])) {
          $search_api_spellcheck['suggestions'] = $this
            ->extractSpellCheckSuggestions($solarium_result);
          if (!empty($options['search_api_spellcheck']['collate'])) {

            /** @var \Solarium\Component\Result\Spellcheck\Result $spellcheck_result */
            if ($spellcheck_result = $solarium_result
              ->getComponent(ComponentAwareQueryInterface::COMPONENT_SPELLCHECK)) {
              if ($collation = $spellcheck_result
                ->getCollation()) {
                $search_api_spellcheck['collation'] = $collation
                  ->getQuery();
              }
            }
          }
          $search_api_result_set
            ->setExtraData('search_api_spellcheck', $search_api_spellcheck);
        }
      }
      $this->moduleHandler
        ->alter('search_api_solr_search_results', $search_api_result_set, $query, $solarium_result);
      $this
        ->postQuery($search_api_result_set, $query, $solarium_result);
    } catch (\Exception $e) {
      throw new SearchApiSolrException('An error occurred while trying to search with Solr: ' . $e
        ->getMessage(), $e
        ->getCode(), $e);
    }
  }
}