You are here

public function SearchApiSolrService::search in Search API Solr 7

Executes a search on the server represented by this object.

Parameters

$query: The SearchApiQueryInterface object to execute.

Return value

array An associative array containing the search results, as required by SearchApiQueryInterface::execute().

Throws

SearchApiException If an error prevented the search from completing.

Overrides SearchApiServiceInterface::search

File

includes/service.inc, line 887

Class

SearchApiSolrService
Search service class using Solr server.

Code

public function search(SearchApiQueryInterface $query) {
  $time_method_called = microtime(TRUE);

  // Reset request handler.
  $this->request_handler = NULL;

  // Get field information.
  $index = $query
    ->getIndex();
  $index_id = $this
    ->getIndexId($index->machine_name);
  $fields = $this
    ->getFieldNames($index);

  // Get Solr connection.
  $this
    ->connect();
  $version = $this->solr
    ->getSolrVersion();

  // Extract keys.
  $keys = $query
    ->getKeys();
  if (is_array($keys)) {
    $keys = $this
      ->flattenKeys($keys);
  }

  // Set searched fields.
  $options = $query
    ->getOptions();
  $search_fields = $this
    ->getQueryFields($query);

  // Get the index fields to be able to retrieve boosts.
  $index_fields = $index
    ->getFields() + array(
    'search_api_relevance' => array(
      'type' => 'decimal',
      'indexed' => TRUE,
    ),
    'search_api_id' => array(
      'type' => 'integer',
      'indexed' => TRUE,
    ),
  );
  $qf = array();
  foreach ($search_fields as $f) {
    $boost = isset($index_fields[$f]['boost']) ? '^' . $index_fields[$f]['boost'] : '';
    $qf[] = $fields[$f] . $boost;
  }

  // Extract filters.
  $filter = $query
    ->getFilter();
  $fq = $this
    ->createFilterQueries($filter, $fields, $index->options['fields']);
  $fq[] = 'index_id:' . call_user_func(array(
    $this
      ->getConnectionClass(),
    'phrase',
  ), $index_id);
  if (!empty($this->options['site_hash'])) {

    // We don't need to escape the site hash, as that consists only of
    // alphanumeric characters.
    $fq[] = 'hash:' . search_api_solr_site_hash();
  }

  // Extract sort.
  $sort = array();
  foreach ($query
    ->getSort() as $field => $order) {
    $f = $fields[$field];
    if (substr($f, 0, 3) == 'ss_') {
      $f = 'sort_' . substr($f, 3);
    }

    // The default Solr schema provides a virtual field named "random_SEED"
    // that can be used to randomly sort the results; the field is available
    // only at query-time.
    if ($field == 'search_api_random') {
      $params = $query
        ->getOption('search_api_random_sort', array());

      // Random seed: getting the value from parameters or computing a new one.
      $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
      $f = 'random_' . $seed;
    }
    $order = strtolower($order);
    $sort[$field] = "{$f} {$order}";
  }

  // Get facet fields.
  $facets = $query
    ->getOption('search_api_facets', array());
  $facet_params = $this
    ->getFacetParams($facets, $fields, $fq);

  // Handle highlighting.
  $highlight_params = $this
    ->getHighlightParams($query);

  // Handle More Like This query.
  $mlt = $query
    ->getOption('search_api_mlt');
  if ($mlt) {
    $mlt_params['qt'] = 'mlt';

    // The fields to look for similarities in.
    $mlt_fl = array();

    // Solr 4 (before 4.6) has a bug which results in numeric fields not being
    // supported in MLT queries.
    $mlt_no_numeric_fields = FALSE;
    if ($version == 4) {
      $system_info = $this->solr
        ->getSystemInfo();
      $mlt_no_numeric_fields = !isset($system_info->lucene->{'solr-spec-version'}) || version_compare($system_info->lucene->{'solr-spec-version'}, '4.6.0', '<');
    }
    foreach ($mlt['fields'] as $f) {

      // Date fields don't seem to be supported at all.
      if ($fields[$f][0] === 'd' || $mlt_no_numeric_fields && in_array($fields[$f][0], array(
        'i',
        'f',
      ))) {
        continue;
      }
      $mlt_fl[] = $fields[$f];

      // For non-text fields, set minimum word length to 0.
      if (isset($index->options['fields'][$f]['type']) && !search_api_is_text_type($index->options['fields'][$f]['type'])) {
        $mlt_params['f.' . $fields[$f] . '.mlt.minwl'] = 0;
      }
    }
    $mlt_params['mlt.fl'] = implode(',', $mlt_fl);
    $id = $this
      ->createId($index_id, $mlt['id']);
    $id = call_user_func(array(
      $this
        ->getConnectionClass(),
      'phrase',
    ), $id);
    $keys = 'id:' . $id;

    // In (early versions of) Solr 5, facets aren't supported with MLT.
    if ($version >= 5) {
      $facet_params = array();
    }
  }

  // Handle spatial filters.
  if ($spatials = $query
    ->getOption('search_api_location')) {
    foreach ($spatials as $i => $spatial) {

      // Spatial options all need a field to do anything.
      if (!isset($spatial['field'])) {
        continue;
      }
      unset($radius);
      $field = $fields[$spatial['field']];
      $escaped_field = call_user_func(array(
        $this
          ->getConnectionClass(),
        'escapeFieldName',
      ), $field);

      // If proper bbox coordinates were given use them to filter.
      if (isset($spatial['bbox'])) {
        if ($version >= 4) {
          $bbox = $spatial['bbox'];
          $fq[] = $escaped_field . ':[' . (double) $bbox['bottom'] . ',' . (double) $bbox['left'] . ' TO ' . (double) $bbox['top'] . ',' . (double) $bbox['right'] . ']';
        }
        else {
          $warnings[] = t('Filtering by a bounding box is not supported in Solr versions below 4.');
        }
      }

      // Everything other than a bounding box filter requires a point, so stop
      // here (for this option) if "lat" and "lon" aren't both set.
      if (!isset($spatial['lat']) || !isset($spatial['lon'])) {
        continue;
      }
      $point = (double) $spatial['lat'] . ',' . (double) $spatial['lon'];

      // Prepare the filter settings.
      if (isset($spatial['radius'])) {
        $radius = (double) $spatial['radius'];
      }
      $spatial_method = 'geofilt';
      if (isset($spatial['method']) && in_array($spatial['method'], array(
        'geofilt',
        'bbox',
      ))) {
        $spatial_method = $spatial['method'];
      }

      // Change the fq facet ranges to the correct fq.
      foreach ($fq as $key => $value) {

        // If the fq consists only of a filter on this field, replace it with
        // a range.
        $preg_field = preg_quote($escaped_field, '/');
        if (preg_match('/^(?:\\{!tag[^}]+\\})?' . $preg_field . ':\\["?(\\*|\\d+(?:\\.\\d+)?)"? TO "?(\\*|\\d+(?:\\.\\d+)?)"?\\]$/', $value, $m)) {
          unset($fq[$key]);
          if ($m[1] && is_numeric($m[1])) {
            $min_radius = isset($min_radius) ? max($min_radius, $m[1]) : $m[1];
          }
          if (is_numeric($m[2])) {

            // Make the radius tighter accordingly.
            $radius = isset($radius) ? min($radius, $m[2]) : $m[2];
          }
        }
      }

      // If either a radius was given in the option, or a filter was
      // encountered, set a filter for the lowest value. If a lower boundary
      // was set (too), we can only set a filter for that if the field name
      // doesn't contains any colons.
      if (isset($min_radius) && strpos($field, ':') === FALSE) {
        $upper = isset($radius) ? " u={$radius}" : '';
        $fq[] = "{!frange l={$min_radius}{$upper}}geodist({$field},{$point})";
      }
      elseif (isset($radius)) {
        $fq[] = "{!{$spatial_method} pt={$point} sfield={$field} d={$radius}}";
      }

      // Change sort on the field, if set (and not already changed).
      if (isset($sort[$spatial['field']]) && substr($sort[$spatial['field']], 0, strlen($field)) === $field) {
        if (strpos($field, ':') === FALSE) {
          $sort[$spatial['field']] = str_replace($field, "geodist({$field},{$point})", $sort[$spatial['field']]);
        }
        else {
          $link = l(t('edit server'), 'admin/config/search/search_api/server/' . $this->server->machine_name . '/edit');
          watchdog('search_api_solr', 'Location sort on field @field had to be ignored because unclean field identifiers are used.', array(
            '@field' => $spatial['field'],
          ), WATCHDOG_WARNING, $link);
        }
      }

      // Add parameters to fetch distance, if requested.
      if (!empty($spatial['distance']) && $version >= 4) {
        if (strpos($field, ':') === FALSE) {

          // Add pseudofield with the distance to the result items.
          $location_fields[] = '_' . $field . '_distance_:geodist(' . $field . ',' . $point . ')';
        }
        else {
          $link = l(t('edit server'), 'admin/config/search/search_api/server/' . $this->server->machine_name . '/edit');
          watchdog('search_api_solr', "Location distance information can't be added because unclean field identifiers are used.", array(), WATCHDOG_WARNING, $link);
        }
      }

      // Change the facet parameters for spatial fields to return distance
      // facets.
      if (!empty($facets)) {
        if (!empty($facet_params['facet.field'])) {
          $facet_params['facet.field'] = array_diff($facet_params['facet.field'], array(
            $field,
          ));
        }
        foreach ($facets as $delta => $facet) {
          if ($facet['field'] != $spatial['field']) {
            continue;
          }
          $steps = $facet['limit'] > 0 ? $facet['limit'] : 5;
          $step = (isset($radius) ? $radius : 100) / $steps;
          for ($k = $steps - 1; $k > 0; --$k) {
            $distance = $step * $k;
            $key = "spatial-{$delta}-{$distance}";
            $facet_params['facet.query'][] = "{!{$spatial_method} pt={$point} sfield={$field} d={$distance} key={$key}}";
          }
          foreach (array(
            'limit',
            'mincount',
            'missing',
          ) as $setting) {
            unset($facet_params["f.{$field}.facet.{$setting}"]);
          }
        }
      }
    }
  }

  // Normal sorting on location fields isn't possible.
  foreach ($sort as $field => $sort_param) {
    if (substr($sort_param, 0, 3) === 'loc') {
      unset($sort[$field]);
    }
  }

  // Handle field collapsing / grouping.
  $grouping = $query
    ->getOption('search_api_grouping');
  if (!empty($grouping['use_grouping'])) {
    $group_params['group'] = 'true';

    // We always want the number of groups returned so that we get pagers done
    // right.
    $group_params['group.ngroups'] = 'true';
    if (!empty($grouping['truncate'])) {
      $group_params['group.truncate'] = 'true';
    }
    if (!empty($grouping['group_facet'])) {
      $group_params['group.facet'] = 'true';
    }
    foreach ($grouping['fields'] as $collapse_field) {
      $type = $index_fields[$collapse_field]['type'];

      // Only single-valued fields are supported.
      if ($version < 4) {

        // For Solr 3.x, only string and boolean fields are supported.
        if (search_api_is_list_type($type) || !search_api_is_text_type($type, array(
          'string',
          'boolean',
          'uri',
        ))) {
          $warnings[] = t('Grouping is not supported for field @field. ' . 'Only single-valued fields of type "String", "Boolean" or "URI" are supported.', array(
            '@field' => $index_fields[$collapse_field]['name'],
          ));
          continue;
        }
      }
      else {
        if (search_api_is_list_type($type) || search_api_is_text_type($type)) {
          $warnings[] = t('Grouping is not supported for field @field. ' . 'Only single-valued fields not indexed as "Fulltext" are supported.', array(
            '@field' => $index_fields[$collapse_field]['name'],
          ));
          continue;
        }
      }
      $group_params['group.field'][] = $fields[$collapse_field];
    }
    if (empty($group_params['group.field'])) {
      unset($group_params);
    }
    else {
      if (!empty($grouping['group_sort'])) {
        foreach ($grouping['group_sort'] as $group_sort_field => $order) {
          if (isset($fields[$group_sort_field])) {
            $f = $fields[$group_sort_field];
            if (substr($f, 0, 3) == 'ss_') {
              $f = 'sort_' . substr($f, 3);
            }

            // The default Solr schema provides a virtual field named
            // "random_SEED" that can be used to randomly sort the results;
            // the field is available only at query-time.
            if ($group_sort_field == 'search_api_random') {
              $params = $query
                ->getOption('search_api_random_sort', array());

              // Random seed: getting the value from parameters or computing a
              // new one.
              $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
              $f = 'random_' . $seed;
            }
            $order = strtolower($order);
            $group_params['group.sort'][] = $f . ' ' . $order;
          }
        }
        if (!empty($group_params['group.sort'])) {
          $group_params['group.sort'] = implode(', ', $group_params['group.sort']);
        }
      }
      if (!empty($grouping['group_limit']) && $grouping['group_limit'] != 1) {
        $group_params['group.limit'] = $grouping['group_limit'];
      }
    }
  }

  // Set defaults.
  if (!$keys) {
    $keys = NULL;
  }

  // Collect parameters.
  $params = array(
    'fl' => 'item_id,score',
    'qf' => $qf,
    'fq' => $fq,
  );
  if (isset($options['offset'])) {
    $params['start'] = $options['offset'];
  }
  $params['rows'] = isset($options['limit']) ? $options['limit'] : 1000000;
  if ($sort) {
    $params['sort'] = implode(', ', $sort);
  }
  if (!empty($facet_params['facet.field'])) {
    $params += $facet_params;
  }
  if (!empty($highlight_params)) {
    $params += $highlight_params;
  }
  if (!empty($options['search_api_spellcheck'])) {
    $params['spellcheck'] = 'true';
  }
  if (!empty($mlt_params['mlt.fl'])) {
    $params += $mlt_params;
  }
  if (!empty($group_params)) {
    $params += $group_params;
  }
  if (!empty($this->options['retrieve_data'])) {
    $params['fl'] = '*,score';
  }
  if (!empty($location_fields)) {
    $params['fl'] .= ',' . implode(',', $location_fields);
  }

  // Retrieve http method from server options.
  $http_method = !empty($this->options['http_method']) ? $this->options['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);
  }
  try {

    // Send search request.
    $time_processing_done = microtime(TRUE);
    drupal_alter('search_api_solr_query', $call_args, $query);
    $this
      ->preQuery($call_args, $query);
    $response = $this->solr
      ->search($keys, $params, $http_method);
    $time_query_done = microtime(TRUE);

    // Extract results.
    $results = $this
      ->extractResults($query, $response);

    // Add warnings, if present.
    if (!empty($warnings)) {
      $results['warnings'] = isset($results['warnings']) ? array_merge($warnings, $results['warnings']) : $warnings;
    }

    // Extract facets.
    if ($facets = $this
      ->extractFacets($query, $response)) {
      $results['search_api_facets'] = $facets;
    }
    drupal_alter('search_api_solr_search_results', $results, $query, $response);
    $this
      ->postQuery($results, $query, $response);

    // Compute performance.
    $time_end = microtime(TRUE);
    $results['performance'] = array(
      'complete' => $time_end - $time_method_called,
      'preprocessing' => $time_processing_done - $time_method_called,
      'execution' => $time_query_done - $time_processing_done,
      'postprocessing' => $time_end - $time_query_done,
    );
    return $results;
  } catch (SearchApiException $e) {
    throw new SearchApiException(t('An error occurred while trying to search with Solr: @msg.', array(
      '@msg' => $e
        ->getMessage(),
    )));
  }
}