Same name and namespace in other branches
  1. 8.9.x core/modules/search/src/SearchQuery.php \Drupal\search\SearchQuery::parseSearchExpression()
  2. 9 core/modules/search/src/SearchQuery.php \Drupal\search\SearchQuery::parseSearchExpression()

Parses the search query into SQL conditions.

Sets up the following variables:

  • $this->keys
  • $this->words
  • $this->conditions
  • $this->simple
  • $this->matches
2 calls to SearchQuery::parseSearchExpression()
SearchQuery::prepareAndNormalize in core/modules/search/src/SearchQuery.php
Prepares the query and calculates the normalization factor.
ViewsSearchQuery::publicParseSearchExpression in core/modules/search/src/ViewsSearchQuery.php
Executes and returns the protected parseSearchExpression method.

File

core/modules/search/src/SearchQuery.php, line 223

Class

SearchQuery
Search query extender and helper functions.

Namespace

Drupal\search

Code

protected function parseSearchExpression() {

  // Matches words optionally prefixed by a - sign. A word in this case is
  // something between two spaces, optionally quoted.
  preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' ' . $this->searchExpression, $keywords, PREG_SET_ORDER);
  if (count($keywords) == 0) {
    return;
  }

  // Classify tokens.
  $in_or = FALSE;
  $limit_combinations = \Drupal::config('search.settings')
    ->get('and_or_limit');

  /** @var \Drupal\search\SearchTextProcessorInterface $text_processor */
  $text_processor = \Drupal::service('search.text_processor');

  // The first search expression does not count as AND.
  $and_count = -1;
  $or_count = 0;
  foreach ($keywords as $match) {
    if ($or_count && $and_count + $or_count >= $limit_combinations) {

      // Ignore all further search expressions to prevent Denial-of-Service
      // attacks using a high number of AND/OR combinations.
      $this->status |= SearchQuery::EXPRESSIONS_IGNORED;
      break;
    }

    // Strip off phrase quotes.
    $phrase = FALSE;
    if ($match[2][0] == '"') {
      $match[2] = substr($match[2], 1, -1);
      $phrase = TRUE;
      $this->simple = FALSE;
    }

    // Simplify keyword according to indexing rules and external
    // preprocessors. Use same process as during search indexing, so it
    // will match search index.
    $words = $text_processor
      ->analyze($match[2]);

    // Re-explode in case simplification added more words, except when
    // matching a phrase.
    $words = $phrase ? [
      $words,
    ] : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY);

    // Negative matches.
    if ($match[1] == '-') {
      $this->keys['negative'] = array_merge($this->keys['negative'], $words);
    }
    elseif ($match[2] == 'OR' && count($this->keys['positive'])) {
      $last = array_pop($this->keys['positive']);

      // Starting a new OR?
      if (!is_array($last)) {
        $last = [
          $last,
        ];
      }
      $this->keys['positive'][] = $last;
      $in_or = TRUE;
      $or_count++;
      continue;
    }
    elseif ($match[2] == 'AND' || $match[2] == 'and') {
      continue;
    }
    else {
      if ($match[2] == 'or') {

        // Lower-case "or" instead of "OR" is a warning condition.
        $this->status |= SearchQuery::LOWER_CASE_OR;
      }
      if ($in_or) {

        // Add to last element (which is an array).
        $this->keys['positive'][count($this->keys['positive']) - 1] = array_merge($this->keys['positive'][count($this->keys['positive']) - 1], $words);
      }
      else {
        $this->keys['positive'] = array_merge($this->keys['positive'], $words);
        $and_count++;
      }
    }
    $in_or = FALSE;
  }

  // Convert keywords into SQL statements.
  $has_and = FALSE;
  $has_or = FALSE;

  // Positive matches.
  foreach ($this->keys['positive'] as $key) {

    // Group of ORed terms.
    if (is_array($key) && count($key)) {

      // If we had already found one OR, this is another one ANDed with the
      // first, meaning it is not a simple query.
      if ($has_or) {
        $this->simple = FALSE;
      }
      $has_or = TRUE;
      $has_new_scores = FALSE;
      $query_or = $this->connection
        ->condition('OR');
      foreach ($key as $or) {
        [
          $num_new_scores,
        ] = $this
          ->parseWord($or);
        $has_new_scores |= $num_new_scores;
        $query_or
          ->condition('d.data', "% {$or} %", 'LIKE');
      }
      if (count($query_or)) {
        $this->conditions
          ->condition($query_or);

        // A group of OR keywords only needs to match once.
        $this->matches += $has_new_scores > 0;
      }
    }
    else {
      $has_and = TRUE;
      [
        $num_new_scores,
        $num_valid_words,
      ] = $this
        ->parseWord($key);
      $this->conditions
        ->condition('d.data', "% {$key} %", 'LIKE');
      if (!$num_valid_words) {
        $this->simple = FALSE;
      }

      // Each AND keyword needs to match at least once.
      $this->matches += $num_new_scores;
    }
  }
  if ($has_and && $has_or) {
    $this->simple = FALSE;
  }

  // Negative matches.
  foreach ($this->keys['negative'] as $key) {
    $this->conditions
      ->condition('d.data', "% {$key} %", 'NOT LIKE');
    $this->simple = FALSE;
  }
}