/** * 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; }
/** * 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; }
/** * 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); } }
/** * 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; }