You are here

class SearchApiQuery in Search API 7

Provides a standard implementation of the SearchApiQueryInterface.

Hierarchy

Expanded class hierarchy of SearchApiQuery

File

includes/query.inc, line 374
Contains SearchApiQueryInterface and SearchApiQuery.

View source
class SearchApiQuery implements SearchApiQueryInterface {

  /**
   * The index this query will use.
   *
   * @var SearchApiIndex
   */
  protected $index;

  /**
   * The index's machine name.
   *
   * Used during serialization to avoid serializing the whole index object.
   *
   * @var string
   */
  protected $index_id;

  /**
   * The search keys. If NULL, this will be a filter-only search.
   *
   * @var mixed
   */
  protected $keys;

  /**
   * The unprocessed search keys, as passed to the keys() method.
   *
   * @var mixed
   */
  protected $orig_keys;

  /**
   * The fields that will be searched for the keys.
   *
   * @var array
   */
  protected $fields;

  /**
   * The search filter associated with this query.
   *
   * @var SearchApiQueryFilterInterface
   */
  protected $filter;

  /**
   * The sort associated with this query.
   *
   * @var array
   */
  protected $sort;

  /**
   * Search options configuring this query.
   *
   * @var array
   */
  protected $options;

  /**
   * Flag for whether preExecute() was already called for this query.
   *
   * @var bool
   */
  protected $pre_execute = FALSE;

  /**
   * {@inheritdoc}
   */
  public function __construct(SearchApiIndex $index, array $options = array()) {
    if (empty($index->options['fields'])) {
      throw new SearchApiException(t("Can't search an index which hasn't got any fields defined."));
    }
    if (empty($index->enabled)) {
      throw new SearchApiException(t("Can't search a disabled index."));
    }
    if (isset($options['parse mode'])) {
      $modes = $this
        ->parseModes();
      if (!isset($modes[$options['parse mode']])) {
        throw new SearchApiException(t('Unknown parse mode: @mode.', array(
          '@mode' => $options['parse mode'],
        )));
      }
    }
    $this->index = $index;
    $this->options = $options + array(
      'conjunction' => 'AND',
      'parse mode' => 'terms',
      'filter class' => 'SearchApiQueryFilter',
      'search id' => __CLASS__,
    );
    $this->filter = $this
      ->createFilter('AND');
    $this->sort = array();
  }

  /**
   * {@inheritdoc}
   */
  public function parseModes() {
    $modes['direct'] = array(
      'name' => t('Direct query'),
      'description' => t("Don't parse the query, just hand it to the search server unaltered. " . "Might fail if the query contains syntax errors in regard to the specific server's query syntax."),
    );
    $modes['single'] = array(
      'name' => t('Single term'),
      'description' => t('The query is interpreted as a single keyword, maybe containing spaces or special characters.'),
    );
    $modes['terms'] = array(
      'name' => t('Multiple terms'),
      'description' => t('The query is interpreted as multiple keywords separated by spaces. ' . 'Keywords containing spaces may be "quoted". Quoted keywords must still be separated by spaces.'),
    );

    // @todo Add fourth mode for complicated expressions, e.g.: »"vanilla ice" OR (love NOT hate)«
    return $modes;
  }

  /**
   * {@inheritdoc}
   */
  protected function parseKeys($keys, $mode) {
    if ($keys === NULL || is_array($keys)) {
      return $keys;
    }
    $keys = '' . $keys;
    switch ($mode) {
      case 'direct':
        return $keys;
      case 'single':
        return array(
          '#conjunction' => $this->options['conjunction'],
          $keys,
        );
      case 'terms':
        $ret = preg_split('/\\s+/u', $keys);
        $quoted = FALSE;
        $str = '';
        foreach ($ret as $k => $v) {
          if (!$v) {
            continue;
          }
          if ($quoted) {
            if (substr($v, -1) == '"') {
              $v = substr($v, 0, -1);
              $str .= ' ' . $v;
              $ret[$k] = $str;
              $quoted = FALSE;
            }
            else {
              $str .= ' ' . $v;
              unset($ret[$k]);
            }
          }
          elseif ($v[0] == '"') {
            $len = strlen($v);
            if ($len > 1 && $v[$len - 1] == '"') {
              $ret[$k] = substr($v, 1, -1);
            }
            else {
              $str = substr($v, 1);
              $quoted = TRUE;
              unset($ret[$k]);
            }
          }
        }
        if ($quoted) {
          $ret[] = $str;
        }
        $ret['#conjunction'] = $this->options['conjunction'];
        return array_filter($ret);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function createFilter($conjunction = 'AND', $tags = array()) {
    $filter_class = $this->options['filter class'];
    return new $filter_class($conjunction, $tags);
  }

  /**
   * {@inheritdoc}
   */
  public function keys($keys = NULL) {
    $this->orig_keys = $keys;
    if (isset($keys)) {
      $this->keys = $this
        ->parseKeys($keys, $this->options['parse mode']);
    }
    else {
      $this->keys = NULL;
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function fields(array $fields) {
    $fulltext_fields = $this->index
      ->getFulltextFields();
    foreach (array_diff($fields, $fulltext_fields) as $field) {
      throw new SearchApiException(t('Trying to search on field @field which is no indexed fulltext field.', array(
        '@field' => $field,
      )));
    }
    $this->fields = $fields;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function filter(SearchApiQueryFilterInterface $filter) {
    $this->filter
      ->filter($filter);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function condition($field, $value, $operator = '=') {
    $this->filter
      ->condition($field, $value, $operator);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function sort($field, $order = 'ASC') {
    $fields = $this->index->options['fields'];
    $fields += array(
      'search_api_relevance' => array(
        'type' => 'decimal',
      ),
      'search_api_id' => array(
        'type' => 'integer',
      ),
    );
    if ($this
      ->getIndex()
      ->server()
      ->supportsFeature('search_api_random_sort')) {
      $fields['search_api_random'] = array(
        'type' => 'integer',
      );
    }
    if (empty($fields[$field])) {
      throw new SearchApiException(t('Trying to sort on unknown field @field.', array(
        '@field' => $field,
      )));
    }
    $type = $fields[$field]['type'];
    if (search_api_is_list_type($type) || search_api_is_text_type($type)) {
      throw new SearchApiException(t('Trying to sort on field @field of illegal type @type.', array(
        '@field' => $field,
        '@type' => $type,
      )));
    }
    $order = strtoupper(trim($order)) == 'DESC' ? 'DESC' : 'ASC';
    $this->sort[$field] = $order;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function range($offset = NULL, $limit = NULL) {
    $this->options['offset'] = $offset;
    $this->options['limit'] = $limit;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function execute() {
    $start = microtime(TRUE);

    // Prepare the query for execution by the server.
    $this
      ->preExecute();
    $pre_search = microtime(TRUE);

    // Execute query.
    $response = $this->index
      ->server()
      ->search($this);
    $post_search = microtime(TRUE);

    // Postprocess the search results.
    $this
      ->postExecute($response);
    $end = microtime(TRUE);
    $response['performance']['complete'] = $end - $start;
    $response['performance']['hooks'] = $response['performance']['complete'] - ($post_search - $pre_search);

    // Store search for later retrieval for facets, etc.
    search_api_current_search(NULL, $this, $response);
    return $response;
  }

  /**
   * Adds language filters for the query.
   *
   * Internal helper function.
   *
   * @param array $languages
   *   The languages for which results should be returned.
   *
   * @throws SearchApiException
   *   If there was a logical error in the combination of filters and languages.
   */
  protected function addLanguages(array $languages) {
    if (array_search(LANGUAGE_NONE, $languages) === FALSE) {
      $languages[] = LANGUAGE_NONE;
    }
    $languages = drupal_map_assoc($languages);
    $langs_to_add = $languages;
    $filters = $this->filter
      ->getFilters();
    while ($filters && $langs_to_add) {
      $filter = array_shift($filters);
      if (is_array($filter)) {
        if ($filter[0] == 'search_api_language' && $filter[2] == '=') {
          $lang = $filter[1];
          if (isset($languages[$lang])) {
            unset($langs_to_add[$lang]);
          }
          else {
            throw new SearchApiException(t('Impossible combination of filters and languages. There is a filter for "@language", but allowed languages are: "@languages".', array(
              '@language' => $lang,
              '@languages' => implode('", "', $languages),
            )));
          }
        }
      }
      else {
        if ($filter
          ->getConjunction() == 'AND') {
          $filters += $filter
            ->getFilters();
        }
      }
    }
    if ($langs_to_add) {
      if (count($langs_to_add) == 1) {
        $this
          ->condition('search_api_language', reset($langs_to_add));
      }
      else {
        $filter = $this
          ->createFilter('OR');
        foreach ($langs_to_add as $lang) {
          $filter
            ->condition('search_api_language', $lang);
        }
        $this
          ->filter($filter);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function preExecute() {

    // Make sure to only execute this once per query.
    if (!$this->pre_execute) {
      $this->pre_execute = TRUE;

      // Add filter for languages.
      if (isset($this->options['languages'])) {
        $this
          ->addLanguages($this->options['languages']);
      }

      // Add fulltext fields, unless set
      if ($this->fields === NULL) {
        $this->fields = $this->index
          ->getFulltextFields();
      }

      // Preprocess query.
      $this->index
        ->preprocessSearchQuery($this);

      // Let modules alter the query.
      drupal_alter('search_api_query', $this);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function postExecute(array &$results) {

    // Postprocess results.
    $this->index
      ->postprocessSearchResults($results, $this);

    // Let modules alter the results.
    drupal_alter('search_api_results', $results, $this);
  }

  /**
   * {@inheritdoc}
   */
  public function getIndex() {
    return $this->index;
  }

  /**
   * {@inheritdoc}
   */
  public function &getKeys() {
    return $this->keys;
  }

  /**
   * {@inheritdoc}
   */
  public function getOriginalKeys() {
    return $this->orig_keys;
  }

  /**
   * {@inheritdoc}
   */
  public function &getFields() {
    return $this->fields;
  }

  /**
   * {@inheritdoc}
   */
  public function getFilter() {
    return $this->filter;
  }

  /**
   * {@inheritdoc}
   */
  public function &getSort() {
    return $this->sort;
  }

  /**
   * {@inheritdoc}
   */
  public function getOption($name, $default = NULL) {
    return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
  }

  /**
   * {@inheritdoc}
   */
  public function setOption($name, $value) {
    $old = $this
      ->getOption($name);
    $this->options[$name] = $value;
    return $old;
  }

  /**
   * {@inheritdoc}
   */
  public function &getOptions() {
    return $this->options;
  }

  /**
   * Implements the magic __sleep() method to avoid serializing the index.
   */
  public function __sleep() {
    $this->index_id = $this->index->machine_name;
    $keys = get_object_vars($this);
    unset($keys['index']);
    return array_keys($keys);
  }

  /**
   * Implements the magic __wakeup() method to reload the query's index.
   */
  public function __wakeup() {
    if (!isset($this->index) && !empty($this->index_id)) {
      $this->index = search_api_index_load($this->index_id);
      unset($this->index_id);
    }
  }

  /**
   * Implements the magic __clone() method to clone the filter, too.
   */
  public function __clone() {
    $this->filter = clone $this->filter;
  }

  /**
   * Implements the magic __toString() method to simplify debugging.
   */
  public function __toString() {
    $ret = 'Index: ' . $this->index->machine_name . "\n";
    $ret .= 'Keys: ' . str_replace("\n", "\n  ", var_export($this->orig_keys, TRUE)) . "\n";
    if (isset($this->keys)) {
      $ret .= 'Parsed keys: ' . str_replace("\n", "\n  ", var_export($this->keys, TRUE)) . "\n";
      $ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n";
    }
    if ($filter = (string) $this->filter) {
      $filter = str_replace("\n", "\n  ", $filter);
      $ret .= "Filters:\n  {$filter}\n";
    }
    if ($this->sort) {
      $sort = array();
      foreach ($this->sort as $field => $order) {
        $sort[] = "{$field} {$order}";
      }
      $ret .= 'Sorting: ' . implode(', ', $sort) . "\n";
    }
    $options = $this
      ->sanitizeOptions($this->options);
    $options = str_replace("\n", "\n  ", var_export($options, TRUE));
    $ret .= 'Options: ' . $options . "\n";
    return $ret;
  }

  /**
   * Sanitizes an array of options in a way that plays nice with var_export().
   *
   * @param array $options
   *   An array of options.
   *
   * @return array
   *   The sanitized options.
   */
  protected function sanitizeOptions(array $options) {
    foreach ($options as $key => $value) {
      if (is_object($value)) {
        $options[$key] = 'object (' . get_class($value) . ')';
      }
      elseif (is_array($value)) {
        $options[$key] = $this
          ->sanitizeOptions($value);
      }
    }
    return $options;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
SearchApiQuery::$fields protected property The fields that will be searched for the keys.
SearchApiQuery::$filter protected property The search filter associated with this query.
SearchApiQuery::$index protected property The index this query will use.
SearchApiQuery::$index_id protected property The index's machine name.
SearchApiQuery::$keys protected property The search keys. If NULL, this will be a filter-only search.
SearchApiQuery::$options protected property Search options configuring this query.
SearchApiQuery::$orig_keys protected property The unprocessed search keys, as passed to the keys() method.
SearchApiQuery::$pre_execute protected property Flag for whether preExecute() was already called for this query.
SearchApiQuery::$sort protected property The sort associated with this query.
SearchApiQuery::addLanguages protected function Adds language filters for the query.
SearchApiQuery::condition public function Adds a new ($field $operator $value) condition filter. Overrides SearchApiQueryInterface::condition
SearchApiQuery::createFilter public function Creates a new filter to use with this query object. Overrides SearchApiQueryInterface::createFilter
SearchApiQuery::execute public function Executes this search query. Overrides SearchApiQueryInterface::execute
SearchApiQuery::fields public function Overrides SearchApiQueryInterface::fields
SearchApiQuery::filter public function Adds a subfilter to this query's filter. Overrides SearchApiQueryInterface::filter
SearchApiQuery::getFields public function Retrieves the fulltext fields that will be searched for the search keys. Overrides SearchApiQueryInterface::getFields
SearchApiQuery::getFilter public function Retrieves the filter object associated with this search query. Overrides SearchApiQueryInterface::getFilter
SearchApiQuery::getIndex public function Retrieves the index associated with this search. Overrides SearchApiQueryInterface::getIndex
SearchApiQuery::getKeys public function Retrieves the search keys for this query. Overrides SearchApiQueryInterface::getKeys
SearchApiQuery::getOption public function Retrieves an option set on this search query. Overrides SearchApiQueryInterface::getOption
SearchApiQuery::getOptions public function Retrieves all options set for this search query. Overrides SearchApiQueryInterface::getOptions
SearchApiQuery::getOriginalKeys public function Retrieves the unparsed search keys for this query as originally entered. Overrides SearchApiQueryInterface::getOriginalKeys
SearchApiQuery::getSort public function Retrieves the sorts set for this query. Overrides SearchApiQueryInterface::getSort
SearchApiQuery::keys public function Sets the keys to search for. Overrides SearchApiQueryInterface::keys
SearchApiQuery::parseKeys protected function
SearchApiQuery::parseModes public function Retrieves the parse modes supported by this query class. Overrides SearchApiQueryInterface::parseModes
SearchApiQuery::postExecute public function Postprocesses the search results before they are returned. Overrides SearchApiQueryInterface::postExecute
SearchApiQuery::preExecute public function Prepares the query object for the search. Overrides SearchApiQueryInterface::preExecute
SearchApiQuery::range public function Adds a range of results to return. Overrides SearchApiQueryInterface::range
SearchApiQuery::sanitizeOptions protected function Sanitizes an array of options in a way that plays nice with var_export().
SearchApiQuery::setOption public function Sets an option for this search query. Overrides SearchApiQueryInterface::setOption
SearchApiQuery::sort public function Adds a sort directive to this search query. Overrides SearchApiQueryInterface::sort
SearchApiQuery::__clone public function Implements the magic __clone() method to clone the filter, too.
SearchApiQuery::__construct public function Constructs a new search query. Overrides SearchApiQueryInterface::__construct
SearchApiQuery::__sleep public function Implements the magic __sleep() method to avoid serializing the index.
SearchApiQuery::__toString public function Implements the magic __toString() method to simplify debugging.
SearchApiQuery::__wakeup public function Implements the magic __wakeup() method to reload the query's index.