From 58b1538b39faa1b596e77ece1dae2848286287dd Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sun, 24 Nov 2013 02:00:27 +0100 Subject: [PATCH] refactored elasticsearch Querybuilder build conditions --- framework/yii/db/QueryBuilder.php | 6 +- framework/yii/elasticsearch/Command.php | 4 +- framework/yii/elasticsearch/QueryBuilder.php | 148 ++++++++++++--------------- 3 files changed, 74 insertions(+), 84 deletions(-) diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php index 867c9e6..9327c79 100644 --- a/framework/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -863,12 +863,12 @@ class QueryBuilder extends \yii\base\Object * describe the interval that column value should be in. * @param array $params the binding parameters to be populated * @return string the generated SQL expression - * @throws Exception if wrong number of operands have been given. + * @throws InvalidParamException if wrong number of operands have been given. */ public function buildBetweenCondition($operator, $operands, &$params) { if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); + throw new InvalidParamException("Operator '$operator' requires three operands."); } list($column, $value1, $value2) = $operands; @@ -983,7 +983,7 @@ class QueryBuilder extends \yii\base\Object public function buildLikeCondition($operator, $operands, &$params) { if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); + throw new InvalidParamException("Operator '$operator' requires two operands."); } list($column, $values) = $operands; diff --git a/framework/yii/elasticsearch/Command.php b/framework/yii/elasticsearch/Command.php index 5071c0c..9ce4fb5 100644 --- a/framework/yii/elasticsearch/Command.php +++ b/framework/yii/elasticsearch/Command.php @@ -60,7 +60,9 @@ class Command extends Component try { $response = $this->db->http()->post($this->createUrl($url, $options), null, $query)->send(); } catch(ClientErrorResponseException $e) { - throw new Exception("elasticsearch error:\n\n" . $query . "\n\n" . $e->getMessage() . $e->getResponse()->getBody(true), [], 0, $e); + throw new Exception("elasticsearch error:\n\n" + . $query . "\n\n" . $e->getMessage() + . print_r(Json::decode($e->getResponse()->getBody(true)), true), [], 0, $e); } return Json::decode($response->getBody(true))['hits']; } diff --git a/framework/yii/elasticsearch/QueryBuilder.php b/framework/yii/elasticsearch/QueryBuilder.php index 18e9c4e..15f5f94 100644 --- a/framework/yii/elasticsearch/QueryBuilder.php +++ b/framework/yii/elasticsearch/QueryBuilder.php @@ -55,8 +55,19 @@ class QueryBuilder extends \yii\base\Object $parts['from'] = (int) $query->offset; } - $this->buildCondition($parts, $query->where); - $this->buildOrderBy($parts, $query->orderBy); + $filters = empty($query->filter) ? [] : [$query->filter]; + $whereFilter = $this->buildCondition($query->where); + if (!empty($whereFilter)) { + $filters[] = $whereFilter; + } + if (!empty($filters)) { + $parts['filter'] = count($filters) > 1 ? ['and' => $filters] : $filters[0]; + } + + $sort = $this->buildOrderBy($query->orderBy); + if (!empty($sort)) { + $parts['sort'] = $sort; + } if (empty($parts['query'])) { $parts['query'] = ["match_all" => (object)[]]; @@ -75,12 +86,12 @@ class QueryBuilder extends \yii\base\Object /** * adds order by condition to the query */ - public function buildOrderBy(&$query, $columns) + public function buildOrderBy($columns) { if (empty($columns)) { - return; + return []; } - $orders = array(); + $orders = []; foreach ($columns as $name => $direction) { // allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ if (is_array($direction)) { @@ -91,7 +102,7 @@ class QueryBuilder extends \yii\base\Object $orders[] = array($name => ($direction === SORT_DESC ? 'desc' : 'asc')); } } - $query['sort'] = $orders; + return $orders; } /** @@ -102,121 +113,102 @@ class QueryBuilder extends \yii\base\Object * @return string the generated SQL expression * @throws \yii\db\Exception if the condition is in bad format */ - public function buildCondition(&$query, $condition) + public function buildCondition($condition) { static $builders = array( - 'AND' => 'buildAndCondition', - 'OR' => 'buildAndCondition', - 'BETWEEN' => 'buildBetweenCondition', - 'NOT BETWEEN' => 'buildBetweenCondition', - 'IN' => 'buildInCondition', - 'NOT IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - 'NOT LIKE' => 'buildLikeCondition', - 'OR LIKE' => 'buildLikeCondition', - 'OR NOT LIKE' => 'buildLikeCondition', + 'and' => 'buildAndCondition', + 'or' => 'buildAndCondition', + 'between' => 'buildBetweenCondition', + 'not between' => 'buildBetweenCondition', + 'in' => 'buildInCondition', + 'not in' => 'buildInCondition', + 'like' => 'buildLikeCondition', + 'not like' => 'buildLikeCondition', + 'or like' => 'buildLikeCondition', + 'or not like' => 'buildLikeCondition', ); if (empty($condition)) { - return; + return []; } if (!is_array($condition)) { throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.'); } if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtoupper($condition[0]); + $operator = strtolower($condition[0]); if (isset($builders[$operator])) { $method = $builders[$operator]; array_shift($condition); - $this->$method($query, $operator, $condition); + return $this->$method($operator, $condition); } else { throw new InvalidParamException('Found unknown operator in query: ' . $operator); } } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - $this->buildHashCondition($query, $condition); + return $this->buildHashCondition($condition); } } - private function buildHashCondition(&$query, $condition) + private function buildHashCondition($condition) { + $parts = []; foreach($condition as $attribute => $value) { - // ['query']['filteredQuery'] - $query['filter']['bool']['must'][] = array( - 'term' => array($attribute => $value), - ); - } - return; // TODO more - $parts = array(); - foreach ($condition as $column => $value) { if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('IN', array($column, $value), $params); + $parts[] = ['in' => [$attribute => $value]]; } else { if ($value === null) { - $parts[] = "$column IS NULL"; // TODO null - } elseif ($value instanceof Expression) { - $parts[] = "$column=" . $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } + $parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]]; } else { - $phName = self::PARAM_PREFIX . count($params); - $parts[] = "$column=$phName"; - $params[$phName] = $value; + $parts[] = ['term' => [$attribute => $value]]; } } } - return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + return count($parts) === 1 ? $parts[0] : ['and' => $parts]; } - private function buildAndCondition($operator, $operands, &$params) + private function buildAndCondition($operator, $operands) { - $parts = array(); + $parts = []; foreach ($operands as $operand) { if (is_array($operand)) { - $operand = $this->buildCondition($operand, $params); + $operand = $this->buildCondition($operand); } - if ($operand !== '') { + if (!empty($operand)) { $parts[] = $operand; } } if (!empty($parts)) { - return '(' . implode(") $operator (", $parts) . ')'; + return [$operator => $parts]; } else { - return ''; + return []; } } - private function buildBetweenCondition($operator, $operands, &$params) + private function buildBetweenCondition($operator, $operands) { if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); + throw new InvalidParamException("Operator '$operator' requires three operands."); } list($column, $value1, $value2) = $operands; - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); + $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; + if ($operator == 'not between') { + $filter = ['not' => $filter]; } - $phName1 = self::PARAM_PREFIX . count($params); - $params[$phName1] = $value1; - $phName2 = self::PARAM_PREFIX . count($params); - $params[$phName2] = $value2; - - return "$column $operator $phName1 AND $phName2"; + return $filter; } private function buildInCondition($operator, $operands, &$params) { if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); + throw new InvalidParamException("Operator '$operator' requires two operands."); } list($column, $values) = $operands; $values = (array)$values; - if (empty($values) || $column === array()) { - return $operator === 'IN' ? '0=1' : ''; + if (empty($values) || $column === []) { + return $operator === 'in' ? ['script' => ['script' => '0=1']] : []; } if (count($column) > 1) { @@ -224,37 +216,33 @@ class QueryBuilder extends \yii\base\Object } elseif (is_array($column)) { $column = reset($column); } + $canBeNull = false; foreach ($values as $i => $value) { if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; + $values[$i] = $value = isset($value[$column]) ? $value[$column] : null; } if ($value === null) { - $values[$i] = 'NULL'; - } elseif ($value instanceof Expression) { - $values[$i] = $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } - } else { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $value; - $values[$i] = $phName; + $canBeNull = true; + unset($values[$i]); } } - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - if (count($values) > 1) { - return "$column $operator (" . implode(', ', $values) . ')'; + if (empty($values) && $canBeNull) { + return ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]; } else { - $operator = $operator === 'IN' ? '=' : '<>'; - return "$column$operator{$values[0]}"; + $filter = ['in' => [$column => $values]]; + if ($canBeNull) { + $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; + } + if ($operator == 'not in') { + $filter = ['not' => $filter]; + } + return $filter; } } protected function buildCompositeInCondition($operator, $columns, $values, &$params) { + throw new NotSupportedException('composite in is not supported by elasticsearch.'); $vss = array(); foreach ($values as $value) { $vs = array();