protected function Database::createKeysQuery in Search API 8
Creates a SELECT query for given search keys.
Used as a helper method in createDbQuery() and createDbCondition().
Parameters
string|array $keys: The search keys, formatted like the return value of \Drupal\search_api\ParseMode\ParseModeInterface::parseInput(), but preprocessed according to internal requirements.
array $fields: The fulltext fields on which to search, with their names as keys mapped to internal information about them.
array $all_fields: Internal information about all indexed fields on the index.
\Drupal\search_api\IndexInterface $index: The index we're searching on.
Return value
\Drupal\Core\Database\Query\SelectInterface A SELECT query returning item_id and score (or only item_id, if $keys['#negation'] is set).
2 calls to Database::createKeysQuery()
- Database::createDbCondition in modules/
search_api_db/ src/ Plugin/ search_api/ backend/ Database.php - Creates a database query condition for a given search filter.
- Database::createDbQuery in modules/
search_api_db/ src/ Plugin/ search_api/ backend/ Database.php - Creates a database query for a search.
File
- modules/
search_api_db/ src/ Plugin/ search_api/ backend/ Database.php, line 1962
Class
- Database
- Indexes and searches items using the database.
Namespace
Drupal\search_api_db\Plugin\search_api\backendCode
protected function createKeysQuery($keys, array $fields, array $all_fields, IndexInterface $index) {
if (!is_array($keys)) {
$keys = [
'#conjunction' => 'AND',
$keys,
];
}
$neg = !empty($keys['#negation']);
$conj = $keys['#conjunction'];
$words = [];
$nested = [];
$negated = [];
$db_query = NULL;
$mul_words = FALSE;
$neg_nested = $neg && $conj == 'AND';
$match_parts = $this->configuration['matching'] !== 'words';
$keyword_hits = [];
$prefix_search = $this->configuration['matching'] === 'prefix';
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', [
'item_id',
'word',
]);
}
elseif ($neg) {
$db_query
->fields('t', [
'item_id',
]);
}
elseif ($not_nested && $match_parts) {
$db_query
->fields('t', [
'item_id',
]);
$db_query
->addExpression('SUM(t.score)', 'score');
}
elseif ($not_nested || $match_parts) {
$db_query
->fields('t', [
'item_id',
'score',
]);
}
else {
$db_query
->fields('t', [
'item_id',
'score',
'word',
]);
}
if (!$match_parts) {
$db_query
->condition('t.word', $words, 'IN');
}
else {
$db_or = $db_query
->orConditionGroup();
// GROUP BY all existing non-aggregated columns.
foreach ($db_query
->getFields() as $column) {
$db_query
->groupBy("{$column['table']}.{$column['field']}");
}
foreach ($words as $i => $word) {
$like = $this->database
->escapeLike($word);
$like = $prefix_search ? "{$like}%" : "%{$like}%";
$db_or
->condition('t.word', $like, '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' . ++$this->expressionCounter;
$like = '%' . $this->database
->escapeLike($word) . '%';
$alias = $db_query
->addExpression("CASE WHEN t.word LIKE :like_{$alias} THEN 1 ELSE 0 END", $alias, [
":like_{$alias}" => $like,
]);
$db_query
->groupBy($alias);
$keyword_hits[] = $alias;
}
// Also add expressions for any nested queries.
for ($i = $word_count; $i < $subs; ++$i) {
$alias = 'w' . ++$this->expressionCounter;
$alias = $db_query
->addExpression('0', $alias);
$db_query
->groupBy($alias);
$keyword_hits[] = $alias;
}
$db_query
->condition($db_or);
}
$field_names = array_keys($fields);
$field_names = array_map([
__CLASS__,
'getTextFieldName',
], $field_names);
$db_query
->condition('t.field_name', $field_names, '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, 't.word', [
$var => $word,
]);
}
else {
$i += $word_count;
for ($j = 0; $j < $subs; ++$j) {
$alias = $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, [
$var => $subs,
]);
}
else {
$db_query
->having('COUNT(t.word) >= ' . $var, [
$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', [
':score' => self::SCORE_MULTIPLIER,
]);
$db_query
->distinct();
}
}
if ($conj == 'AND') {
$condition = $db_query;
}
else {
$condition = $db_query
->conditionGroupFactory('OR');
$db_query
->condition($condition);
}
foreach ($negated as $k) {
$nested_query = $this
->createKeysQuery($k, $fields, $all_fields, $index);
// For a "NOT IN", the SELECT must not have more than one column.
$num_fields = count($nested_query
->getFields());
$num_expressions = count($nested_query
->getExpressions());
if ($num_fields + $num_expressions > 1) {
$nested_query = $this->database
->select($nested_query, 't')
->fields('t', [
'item_id',
]);
}
$condition
->condition('t.item_id', $nested_query, 'NOT IN');
}
if (isset($old_query)) {
$condition
->condition('t.item_id', $old_query, 'NOT IN');
}
}
if ($neg_nested) {
$db_query = $this->database
->select($db_query, 't')
->fields('t', [
'item_id',
]);
}
return $db_query;
}