LEVENSHTEIN

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2022-12-18 21:46:08 +10:00
parent 7a6e6fbada
commit df1f06ad80
3 changed files with 228 additions and 60 deletions

View File

@ -103,9 +103,11 @@ class ActiveQuery extends Query implements ActiveQueryInterface
public function all($db = null)
{
$statement = $this->createCommand($db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::query');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query');
$cursor = $statement->execute();

View File

@ -23,7 +23,7 @@ abstract class Migration extends Component implements MigrationInterface
{
parent::init();
$this->db = Instance::ensure($this->db, Connection::class;
$this->db = Instance::ensure($this->db, Connection::class);
}
public function execute($aql, $bindValues = [], $params = [])

View File

@ -74,12 +74,19 @@ class Query extends Component implements QueryInterface
*
* [свойство => его значение]
*/
public array $search;
public array|string $search;
/**
* Тип поиска
* Фильтр
*
* [свойство => его значение]
*/
public string $searchType = 'START';
public array|string $filter;
/**
* Отладка
*/
public bool $debug = false;
public $orderBy;
@ -145,6 +152,11 @@ class Query extends Component implements QueryInterface
]
);
if ($this->debug) {
var_dump($aql);
die;
}
return $this->getStatement($options, $db);
}
@ -221,22 +233,27 @@ class Query extends Component implements QueryInterface
/**
*/
public function search(array $text, string $type = 'START'): self
public function search(array|string $expressions): self
{
$this->search = $text;
$this->searchType = $type;
$this->search = $expressions;
return $this;
}
/**
*/
public function filter(array|string $expressions): self
{
$this->filter = $expressions;
return $this;
}
/**
* Обойти коллекцию вершин по направлению
*
* Генерация AQL выражения
*
* @see https://www.arangodb.com/docs/3.7/aql/operations-let.html
*
* @param mixed $vertex Коллекция вершин из которой требуется обход
* @param string $direction Направление ('INBOUND', 'OUTBOUND', 'ANY')
*/
@ -253,24 +270,43 @@ class Query extends Component implements QueryInterface
return $this;
}
/**
* Проверка типа и конвертация
*/
protected static function checkArrayAndConvert(string|array $text): string
{
if (is_array($text)) {
return self::convertArrayToString($text);
}
return $text;
}
/**
* Конвертация в строку
*/
protected static function convertArrayToString(array $text): string
{
// Очистка
array_walk($text, 'trim');
// Конвертация
return implode(", ", $text);
}
/**
* Генерация AQL конструкции "FOR"
*
* Примеры:
* 1. "FOR account"
* 2. "FOR account, account_edge_supply"
*
*/
protected static function genFor(string|array $for): string
{
if (is_array($for)) {
// Если передан массив, то конвертировать в строку
// Очистка элементов через trim()
array_walk($for, 'trim');
// Конвертация
$for = implode(", ", $for);
$for = self::convertArrayToString($for);
}
// Генерация
@ -368,19 +404,17 @@ class Query extends Component implements QueryInterface
foreach ($conditions as $condition) {
// Перебор выражений
genForeach_recursion:
foreach ($condition as $FOR => $IN) {
// Инициализация операндов
if (is_int($FOR) && is_array($IN)) {
// Вложенный массив (неожиданные входные данные)
// Реинициализация
$condition = $IN;
// !!! Вход в рекурсию !!!
// Обработка вложенного массива
$aql .= ' ' . $this->genForeach([$IN]);
// Перебор вложенного массива
goto genForeach_recursion;
continue;
}
$aql .= "FOR $FOR IN $IN ";
@ -751,8 +785,13 @@ class Query extends Component implements QueryInterface
return '';
}
$orders = [];
foreach ($columns as $name => $direction) {
$orders[] = $this->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
$orders[] = $this->quoteColumnName($name) . match($direction) {
SORT_DESC => ' DESC',
SORT_ASC => ' ASC',
default => ''
};
}
return 'SORT ' . implode(', ', $orders);
@ -819,27 +858,33 @@ class Query extends Component implements QueryInterface
protected function genQuery($query = null, array $params = [])
{
// Инициализация
isset($query) ? $query : $query = $this;
$this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция'));
$this->for ?? $this->for = $this->in;
$this->collection ?? $this->collection = $this->in;
$query ?? $query = $this;
$query->in ?? $query->in = $query->collection ?? throw new Exception('Не найдена коллекция');
$query->for ?? $query->for = $query->in;
$query->collection ?? $query->collection = self::checkArrayAndConvert($query->for);
$params = array_merge($params, $query->params);
$clauses = [
static::genFor($query->for ?? $query->collection),
static::genIn($query->in ?? $query->collection, $query->traversals),
static::genLet($query->lets),
$this->genForeach($query->foreach),
$this->genWhere($query->where, $params),
isset($this->search) ? $this->genSearch($this->search, $this->searchType) : null,
$this->genOrderBy($query->orderBy, $params),
$this->genLimit($query->limit, $query->offset, $params),
$this->genSelect($query->select, $params),
$query::genFor($query->for),
$query::genIn($query->in, $query->traversals),
$query::genLet($query->lets),
$query->genForeach($query->foreach),
isset($query->search) ? $query->genSearch($query->search) : null,
isset($query->filter) ? $query->genFilter($query->filter) : null,
$query->genWhere($query->where, $params),
$query->genOrderBy($query->orderBy, $params),
$query->genLimit($query->limit, $query->offset, $params),
$query->genSelect($query->select, $params),
];
$aql = implode($query->separator, array_filter($clauses));
if ($query->debug) {
var_dump($aql);
die;
}
return self::prepareBindVars($aql, $params);
}
@ -874,9 +919,11 @@ class Query extends Component implements QueryInterface
public function all($db = null)
{
$statement = $this->createCommand($db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::query');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query');
$cursor = $statement->execute();
@ -885,6 +932,7 @@ class Query extends Component implements QueryInterface
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
return $this->prepareResult($cursor->getAll());
}
@ -894,10 +942,14 @@ class Query extends Component implements QueryInterface
public function one($db = null)
{
$this->limit(1);
$statement = $this->createCommand($db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::query');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query');
$cursor = $statement->execute();
@ -922,13 +974,13 @@ class Query extends Component implements QueryInterface
public function insert($columns, $params = [], $db = null)
{
// Инициализация
$this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция'));
$this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция');
$this->collection ?? $this->collection = $this->in;
$data = Serializer::encode($columns);
$clauses = [
"INSERT $data IN {$this->quoteCollectionName($this->in ?? $this->collection)}",
"INSERT $data IN {$this->quoteCollectionName($this->collection)}",
$this->genOptions(),
];
@ -968,15 +1020,15 @@ class Query extends Component implements QueryInterface
public function update($columns, $params = [], $db = null)
{
// Инициализация
$this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция'));
$this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция');
$this->for ?? $this->for = $this->in;
$this->collection ?? $this->collection = $this->in;
$this->collection ?? $this->collection = self::checkArrayAndConvert($this->for);
$clauses = [
static::genFor($this->for ?? $this->collection),
static::genIn($this->in ?? $this->collection, $this->traversals),
static::genFor($this->for),
static::genIn($this->in, $this->traversals),
$this->genWhere($this->where, $params),
$this->genUpdate($this->collection, $columns),
$this->genUpdate($this->in, $columns),
$this->genOptions(),
];
@ -1020,15 +1072,15 @@ class Query extends Component implements QueryInterface
public function remove($params = [], $db = null)
{
// Инициализация
$this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция'));
$this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция');
$this->for ?? $this->for = $this->in;
$this->collection ?? $this->collection = $this->in;
$this->collection ?? $this->collection = self::checkArrayAndConvert($this->for);
$clauses = [
static::genFor($this->for ?? $this->collection),
static::genIn($this->in ?? $this->collection, $this->traversals),
static::genFor($this->for),
static::genIn($this->in, $this->traversals),
$this->genWhere($this->where, $params),
$this->genRemove($this->in ?? $this->collection),
$this->genRemove($this->in),
$this->genOptions(),
];
@ -1085,15 +1137,63 @@ class Query extends Component implements QueryInterface
* @param $columns
* @return string
*/
protected function genSearch(array $expression, string $type = 'START'): string
protected function genSearch(array|string $expressions): string
{
if (is_string($expressions)) return $expressions;
// Инициализация строки запроса
$query = 'SEARCH ';
return match (strtoupper($type)) {
'START', 'STARTS', 'STARTS_WITH' => $query . $this->filterStartsWith($expression),
'CONTAINS', 'LIKE' => $query . $this->filterContains($expression),
default => $query . Serializer::encode($expression)
};
foreach ($expressions as $expression) {
// Перебор выражений
// Инициализация оператора
$operator = isset($expression['operator']) ? ' ' . $expression['operator'] . ' ' : '';
// Генерация строки запроса
$query .= match (strtoupper($expression['type'])) {
'START', 'STARTS', 'STARTS_WITH' => $operator . $this->filterStartsWith($expression['condition']),
'START_SENSETIVE' => $operator . $this->filterStartsWith($expression['condition'], sensetive: true),
'CONTAINS', 'LIKE' => $operator . $this->filterContains($expression['condition']),
'LEVENSHTEIN' => $operator . $this->filterLevenshtein($expression['condition'], ...$expression['parameters']),
'PHRASE' => $operator . $this->filterPhrase($expression['condition'], ...$expression['parameters']),
default => $operator . Serializer::encode($expression['condition'])
};
}
return $query;
}
/**
* @param $collection
* @param $columns
* @return string
*/
protected function genFilter(array|string $expressions): string
{
if (isString($expressions)) return $expressions;
// Инициализация строки запроса
$query = 'FILTER ';
foreach ($expressions as $expression) {
// Перебор выражений
// Инициализация оператора
$operator = isset($expression['operator']) ? ' ' . $expression['operator'] . ' ' : '';
// Генерация строки запроса
$query .= match (strtoupper($expression['type'])) {
'START', 'STARTS', 'STARTS_WITH' => $operator . $this->filterStartsWith($expression['condition']),
'START_SENSETIVE' => $operator . $this->filterStartsWith($expression['condition'], sensetive: true),
'CONTAINS', 'LIKE' => $operator . $this->filterContains($expression['condition']),
'LEVENSHTEIN' => $operator . $this->filterLevenshtein($expression['condition'], ...$expression['parameters']),
'PHRASE' => $operator . $this->filterPhrase($expression['condition'], ...$expression['parameters']),
default => $operator . Serializer::encode($expression['condition'])
};
}
return $query;
}
/**
@ -1133,8 +1233,6 @@ class Query extends Component implements QueryInterface
*
* Генерация AQL выражения
*
* @see https://www.arangodb.com/docs/3.7/aql/operations-let.html
*
* @param string $direction Направление
* @param mixed $vertex Коллекция вершин из которой требуется обход
*/
@ -1279,7 +1377,7 @@ class Query extends Component implements QueryInterface
{
$this->foreach = match (true) {
empty($this->foreach) => [$expression],
default => $this->foreach []= [$expression]
default => $this->foreach[] = [$expression]
};
return $this;
@ -1298,6 +1396,17 @@ class Query extends Component implements QueryInterface
return $this;
}
/**
* @param array $expression
*/
public function debug(bool $status = true)
{
$this->debug = $status;
return $this;
}
/**
*/
public function let(string $name, mixed $value): static
@ -1375,20 +1484,38 @@ class Query extends Component implements QueryInterface
return $this;
}
public function filterStartsWith(array $expression): string
public function filterStartsWith(array $expression, bool $sensetive = false): string
{
// Генерация
foreach ($expression as $key => $value) {
if (isset($return)) {
$return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")";
if ($sensetive) {
if (isset($return)) {
$return .= ' OR STARTS_WITH(' . $this->filterLower($this->quoteCollectionName($this->collection) . ".$key") . ", " . $this->filterLower("\"$value\"") . ")";
} else {
$return = 'STARTS_WITH(' . $this->filterLower($this->quoteCollectionName($this->collection) . ".$key") . ", " . $this->filterLower("\"$value\"") . ")";
}
} else {
$return = 'STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")";
if (isset($return)) {
$return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")";
} else {
$return = 'STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")";
}
}
}
return $return;
}
public function filterUpper(string $target): string
{
return "UPPER($target)";
}
public function filterLower(string $target): string
{
return "LOWER($target)";
}
public function filterContains(array $expression): string
{
// Инициализация
@ -1406,6 +1533,44 @@ class Query extends Component implements QueryInterface
return $return;
}
public function filterPhrase(array $expression, string $analyzer = 'text_en'): string
{
// Инициализация
$return = [];
foreach ($expression as $key => $value) {
// Перебор выражений
if ($return) {
$return .= ' OR PHRASE(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", \"$analyzer\")";
} else {
$return = 'PHRASE(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", \"$analyzer\")";
}
}
return $return;
}
public function filterLevenshtein(array $expression, string $analyzer = 'text_en', int $distance = 3, bool $transpositions = true): string
{
// Инициализация
$return = [];
$transpositions = $transpositions ? 'true' : 'false';
foreach ($expression as $key => $value) {
// Перебор выражений
if ($return) {
$return .= ' OR ANALYZER(LEVENSHTEIN_MATCH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", $distance, $transpositions), \"$analyzer\")";
} else {
$return = 'ANALYZER(LEVENSHTEIN_MATCH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", $distance, $transpositions), \"$analyzer\")";
}
}
return $return;
}
/**
* Adds an additional WHERE condition to the existing one but ignores [[isEmpty()|empty operands]].
* The new condition and the existing one will be joined using the 'AND' operator.
@ -1603,6 +1768,7 @@ class Query extends Component implements QueryInterface
$result[$column] = SORT_ASC;
}
}
return $result;
}
}