Esempio n. 1
3
 /**
  * Extracts the positive keywords from a keys array.
  *
  * @param array $keys
  *   A search keys array, as specified by
  *   \Drupal\search_api\Query\QueryInterface::getKeys().
  *
  * @return string[]
  *   An array of all unique positive keywords contained in the keys array.
  */
 protected function flattenKeysArray(array $keys)
 {
     if (!empty($keys['#negation'])) {
         return array();
     }
     $keywords = array();
     foreach ($keys as $i => $key) {
         if (!Element::child($i)) {
             continue;
         }
         if (is_array($key)) {
             $keywords += $this->flattenKeysArray($key);
         } else {
             $keywords[$key] = $key;
         }
     }
     return $keywords;
 }
Esempio n. 2
3
  /**
   * Creates a SELECT query for given search keys.
   *
   * Used as a helper method in createDbQuery() and createFilterCondition().
   *
   * @param string|array $keys
   *   The search keys, formatted like the return value of
   *   \Drupal\search_api\Query\QueryInterface::getKeys(), but preprocessed
   *   according to internal requirements.
   * @param array $fields
   *   The fulltext fields on which to search, with their names as keys mapped
   *   to internal information about them.
   * @param array $all_fields
   *   Internal information about all indexed fields on the index.
   * @param \Drupal\search_api\IndexInterface $index
   *   The index we're searching on.
   *
   * @return \Drupal\Core\Database\Query\SelectInterface
   *   A SELECT query returning item_id and score (or only item_id, if
   *   $keys['#negation'] is set).
   */
  protected function createKeysQuery($keys, array $fields, array $all_fields, IndexInterface $index) {
    if (!is_array($keys)) {
      $keys = array(
        '#conjunction' => 'AND',
        $keys,
      );
    }

    $neg = !empty($keys['#negation']);
    $conj = $keys['#conjunction'];
    $words = array();
    $nested = array();
    $negated = array();
    $db_query = NULL;
    $mul_words = FALSE;
    $neg_nested = $neg && $conj == 'AND';

    foreach ($keys as $i => $key) {
      if (!Element::child($i)) {
        continue;
      }
      if (is_scalar($key)) {
        $words[] = $key;
      }
      elseif (empty($key['#negation'])) {
        if ($neg) {
          // If this query is negated, we also only need item IDs from
          // subqueries.
          $key['#negation'] = TRUE;
        }
        $nested[] = $key;
      }
      else {
        $negated[] = $key;
      }
    }
    $subs = count($words) + count($nested);
    $not_nested = ($subs <= 1 && count($fields) == 1) || ($neg && $conj == 'OR' && !$negated);

    if ($words) {
      // All text fields in the index share a table. Get name from the first.
      $field = reset($fields);
      $db_query = $this->database->select($field['table'], 't');
      $mul_words = count($words) > 1;
      if ($neg_nested) {
        $db_query->fields('t', array('item_id', 'word'));
      }
      elseif ($neg) {
        $db_query->fields('t', array('item_id'));
      }
      elseif ($not_nested) {
        $db_query->fields('t', array('item_id', 'score'));
      }
      else {
        $db_query->fields('t', array('item_id', 'score', 'word'));
      }
      $db_query->condition('word', $words, 'IN');
      $db_query->condition('field_name', array_map(array(__CLASS__, 'getTextFieldName'), array_keys($fields)), 'IN');
    }

    if ($nested) {
      $word = '';
      foreach ($nested as $k) {
        $query = $this->createKeysQuery($k, $fields, $all_fields, $index);
        if (!$neg) {
          $word .= ' ';
          $var = ':word' . strlen($word);
          $query->addExpression($var, 'word', array($var => $word));
        }
        if (!isset($db_query)) {
          $db_query = $query;
        }
        elseif ($not_nested) {
          $db_query->union($query, 'UNION');
        }
        else {
          $db_query->union($query, 'UNION ALL');
        }
      }
    }

    if (isset($db_query) && !$not_nested) {
      $db_query = $this->database->select($db_query, 't');
      $db_query->addField('t', 'item_id', 'item_id');
      if (!$neg) {
        $db_query->addExpression('SUM(t.score)', 'score');
        $db_query->groupBy('t.item_id');
      }
      if ($conj == 'AND' && $subs > 1) {
        $var = ':subs' . ((int) $subs);
        if (!$db_query->getGroupBy()) {
          $db_query->groupBy('t.item_id');
        }
        if ($mul_words) {
          $db_query->having('COUNT(DISTINCT t.word) >= ' . $var, array($var => $subs));
        }
        else {
          $db_query->having('COUNT(t.word) >= ' . $var, array($var => $subs));
        }
      }
    }

    if ($negated) {
      if (!isset($db_query) || $conj == 'OR') {
        if (isset($db_query)) {
          // We are in a rather bizarre case where the keys are something like
          // "a OR (NOT b)".
          $old_query = $db_query;
        }

        // We use this table because all items should be contained exactly once.
        $db_info = $this->getIndexDbInfo($index);
        $db_query = $this->database->select($db_info['index_table'], 't');
        $db_query->addField('t', 'item_id', 'item_id');
        if (!$neg) {
          $db_query->addExpression(':score', 'score', array(':score' => self::SCORE_MULTIPLIER));
          $db_query->distinct();
        }
      }

      if ($conj == 'AND') {
        foreach ($negated as $k) {
          $db_query->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields, $index), 'NOT IN');
        }
      }
      else {
        $or = new Condition('OR');
        foreach ($negated as $k) {
          $or->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields, $index), 'NOT IN');
        }
        if (isset($old_query)) {
          $or->condition('t.item_id', $old_query, 'NOT IN');
        }
        $db_query->condition($or);
      }
    }

    if ($neg_nested) {
      $db_query = $this->database->select($db_query, 't')->fields('t', array('item_id'));
    }

    return $db_query;
  }
Esempio n. 3
2
 /**
  * Tests the child() method.
  */
 public function testChild()
 {
     $this->assertFalse(Element::child('#property'));
     $this->assertTrue(Element::child('property'));
     $this->assertTrue(Element::child('property#'));
 }
 /**
  * Preprocesses the search keywords.
  *
  * Calls processKey() for individual strings.
  *
  * @param array|string $keys
  *   Either a parsed keys array, or a single keywords string.
  */
 protected function processKeys(&$keys)
 {
     if (is_array($keys)) {
         foreach ($keys as $key => &$v) {
             if (Element::child($key)) {
                 $this->processKeys($v);
                 if ($v === '') {
                     unset($keys[$key]);
                 }
             }
         }
     } else {
         $this->processKey($keys);
     }
 }
Esempio n. 5
2
 /**
  * Creates a SELECT query for given search keys.
  *
  * Used as a helper method in createDbQuery() and createDbCondition().
  *
  * @param string|array $keys
  *   The search keys, formatted like the return value of
  *   \Drupal\search_api\Query\QueryInterface::getKeys(), but preprocessed
  *   according to internal requirements.
  * @param array $fields
  *   The fulltext fields on which to search, with their names as keys mapped
  *   to internal information about them.
  * @param array $all_fields
  *   Internal information about all indexed fields on the index.
  * @param \Drupal\search_api\IndexInterface $index
  *   The index we're searching on.
  *
  * @return \Drupal\Core\Database\Query\SelectInterface
  *   A SELECT query returning item_id and score (or only item_id, if
  *   $keys['#negation'] is set).
  */
 protected function createKeysQuery($keys, array $fields, array $all_fields, IndexInterface $index)
 {
     if (!is_array($keys)) {
         $keys = array('#conjunction' => 'AND', $keys);
     }
     $neg = !empty($keys['#negation']);
     $conj = $keys['#conjunction'];
     $words = array();
     $nested = array();
     $negated = array();
     $db_query = NULL;
     $mul_words = FALSE;
     $neg_nested = $neg && $conj == 'AND';
     $match_parts = !empty($this->configuration['partial_matches']);
     $keyword_hits = array();
     foreach ($keys as $i => $key) {
         if (!Element::child($i)) {
             continue;
         }
         if (is_scalar($key)) {
             $words[] = $key;
         } elseif (empty($key['#negation'])) {
             if ($neg) {
                 // If this query is negated, we also only need item IDs from
                 // subqueries.
                 $key['#negation'] = TRUE;
             }
             $nested[] = $key;
         } else {
             $negated[] = $key;
         }
     }
     $word_count = count($words);
     $subs = $word_count + count($nested);
     $not_nested = $subs <= 1 && count($fields) == 1 || $neg && $conj == 'OR' && !$negated;
     if ($words) {
         // All text fields in the index share a table. Get name from the first.
         $field = reset($fields);
         $db_query = $this->database->select($field['table'], 't');
         $mul_words = $word_count > 1;
         if ($neg_nested) {
             $db_query->fields('t', array('item_id', 'word'));
         } elseif ($neg) {
             $db_query->fields('t', array('item_id'));
         } elseif ($not_nested) {
             $db_query->fields('t', array('item_id', 'score'));
         } else {
             $db_query->fields('t', array('item_id', 'score', 'word'));
         }
         if (!$match_parts) {
             $db_query->condition('word', $words, 'IN');
         } else {
             $db_or = new Condition('OR');
             // GROUP BY all existing non-grouped, non-aggregated columns – except
             // "word", which we remove since it will be useless to us in this case.
             $columns =& $db_query->getFields();
             unset($columns['word']);
             foreach (array_keys($columns) as $column) {
                 $db_query->groupBy($column);
             }
             foreach ($words as $i => $word) {
                 $db_or->condition('t.word', '%' . $this->database->escapeLike($word) . '%', 'LIKE');
                 // Add an expression for each keyword that shows whether the indexed
                 // word matches that particular keyword. That way we don't return a
                 // result multiple times if a single indexed word (partially) matches
                 // multiple keywords. We also remember the column name so we can
                 // afterwards verify that each word matched at least once.
                 $alias = 'w' . $i;
                 $alias = $db_query->addExpression("t.word LIKE '%" . $this->database->escapeLike($word) . "%'", $alias);
                 $db_query->groupBy($alias);
                 $keyword_hits[] = $alias;
             }
             // Also add expressions for any nested queries.
             for ($i = $word_count; $i < $subs; ++$i) {
                 $alias = 'w' . $i;
                 $alias = $db_query->addExpression('0', $alias);
                 $db_query->groupBy($alias);
                 $keyword_hits[] = $alias;
             }
             $db_query->condition($db_or);
         }
         $db_query->condition('field_name', array_map(array(__CLASS__, 'getTextFieldName'), array_keys($fields)), 'IN');
     }
     if ($nested) {
         $word = '';
         foreach ($nested as $i => $k) {
             $query = $this->createKeysQuery($k, $fields, $all_fields, $index);
             if (!$neg) {
                 if (!$match_parts) {
                     $word .= ' ';
                     $var = ':word' . strlen($word);
                     $query->addExpression($var, 'word', array($var => $word));
                 } else {
                     $i += $word_count;
                     for ($j = 0; $j < $subs; ++$j) {
                         $alias = isset($keyword_hits[$j]) ? $keyword_hits[$j] : "w{$j}";
                         $keyword_hits[$j] = $query->addExpression($i == $j ? '1' : '0', $alias);
                     }
                 }
             }
             if (!isset($db_query)) {
                 $db_query = $query;
             } elseif ($not_nested) {
                 $db_query->union($query, 'UNION');
             } else {
                 $db_query->union($query, 'UNION ALL');
             }
         }
     }
     if (isset($db_query) && !$not_nested) {
         $db_query = $this->database->select($db_query, 't');
         $db_query->addField('t', 'item_id', 'item_id');
         if (!$neg) {
             $db_query->addExpression('SUM(t.score)', 'score');
             $db_query->groupBy('t.item_id');
         }
         if ($conj == 'AND' && $subs > 1) {
             $var = ':subs' . (int) $subs;
             if (!$db_query->getGroupBy()) {
                 $db_query->groupBy('t.item_id');
             }
             if (!$match_parts) {
                 if ($mul_words) {
                     $db_query->having('COUNT(DISTINCT t.word) >= ' . $var, array($var => $subs));
                 } else {
                     $db_query->having('COUNT(t.word) >= ' . $var, array($var => $subs));
                 }
             } else {
                 foreach ($keyword_hits as $alias) {
                     $db_query->having("SUM({$alias}) >= 1");
                 }
             }
         }
     }
     if ($negated) {
         if (!isset($db_query) || $conj == 'OR') {
             if (isset($db_query)) {
                 // We are in a rather bizarre case where the keys are something like
                 // "a OR (NOT b)".
                 $old_query = $db_query;
             }
             // We use this table because all items should be contained exactly once.
             $db_info = $this->getIndexDbInfo($index);
             $db_query = $this->database->select($db_info['index_table'], 't');
             $db_query->addField('t', 'item_id', 'item_id');
             if (!$neg) {
                 $db_query->addExpression(':score', 'score', array(':score' => self::SCORE_MULTIPLIER));
                 $db_query->distinct();
             }
         }
         if ($conj == 'AND') {
             foreach ($negated as $k) {
                 $db_query->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields, $index), 'NOT IN');
             }
         } else {
             $or = new Condition('OR');
             foreach ($negated as $k) {
                 $or->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields, $index), 'NOT IN');
             }
             if (isset($old_query)) {
                 $or->condition('t.item_id', $old_query, 'NOT IN');
             }
             $db_query->condition($or);
         }
     }
     if ($neg_nested) {
         $db_query = $this->database->select($db_query, 't')->fields('t', array('item_id'));
     }
     return $db_query;
 }