mirror of
https://github.com/predis/predis.git
synced 2026-08-30 04:02:22 +00:00
Extended RediSearch support by implementing FT.AGGREGATE command (#1176)
* Added support for new arguments for BITPOS, BITCOUNT commands (#1045) * Added support for new arguments for EXPIRE, EXPIREAT commands (#1046) * Extended core support by implementing SORT_RO command (#1044) * Added support for SORT_RO command * Codestyle fixes * Added command description --------- Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local> * fix deprecated call * Added support for container commands (#1049) * Added support for container commands FUNCTION LOAD, FUNCTION DELETE and FCALL * Changed ContainerInterface and AbstractContainer * Re-implement logic of abstract methods --------- Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local> * Added stream commands to KeyPrefixProcessor (#1051) Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local> * Fix return type of ReplicationInterface::getSlaves (#1111) * Codestyle fixes * Changed return annotation * Added support for FT.AGGREGATE command * Added command usage example --------- Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local> Co-authored-by: Till Krüss <till@kruss.io> Co-authored-by: Stephan <glaubinix@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
684bb34cf2
commit
bc42b3bcc5
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2023 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Predis\Client;
|
||||
use Predis\Command\Argument\Search\AggregateArguments;
|
||||
use Predis\Command\Argument\Search\CreateArguments;
|
||||
use Predis\Command\Argument\Search\Schema;
|
||||
|
||||
require __DIR__ . '/../../shared.php';
|
||||
|
||||
// Example of FT.AGGREGATE command usage:
|
||||
|
||||
// 1. Create index
|
||||
$client = new Client();
|
||||
|
||||
$ftCreateArguments = (new CreateArguments())->prefix(['user:']);
|
||||
$schema = (new Schema())
|
||||
->addTextField('name')
|
||||
->addTextField('country')
|
||||
->addNumericField('dob', '', Schema::SORTABLE);
|
||||
|
||||
$client->ftcreate('idx', $schema, $ftCreateArguments);
|
||||
|
||||
// 2. Add documents
|
||||
$client->hset('user:0', 'name', 'Vlad', 'country', 'Ukraine', 'dob', 813801600);
|
||||
$client->hset('user:1', 'name', 'Vlad', 'country', 'Israel', 'dob', 782265600);
|
||||
$client->hset('user:2', 'name', 'Vlad', 'country', 'Ukraine', 'dob', 813801600);
|
||||
|
||||
// 3. Execute aggregation query
|
||||
$ftAggregateArguments = (new AggregateArguments())
|
||||
->apply('year(@dob)', 'birth')
|
||||
->groupBy('@country', '@birth')
|
||||
->reduce('COUNT', true, 'country_birth_Vlad_count')
|
||||
->sortBy(0, '@birth', 'DESC');
|
||||
|
||||
$response = $client->ftaggregate('idx', '@name: "Vlad"', $ftAggregateArguments);
|
||||
|
||||
// Response grouped by user country and birth year, with users count in each group, sorted by birth year from DESC.
|
||||
echo 'Response:' . "\n";
|
||||
print_r($response);
|
||||
@@ -14,6 +14,7 @@ namespace Predis;
|
||||
|
||||
use Predis\Command\Argument\Geospatial\ByInterface;
|
||||
use Predis\Command\Argument\Geospatial\FromInterface;
|
||||
use Predis\Command\Argument\Search\AggregateArguments;
|
||||
use Predis\Command\Argument\Search\AlterArguments;
|
||||
use Predis\Command\Argument\Search\CreateArguments;
|
||||
use Predis\Command\Argument\Search\DropArguments;
|
||||
@@ -95,6 +96,7 @@ use Predis\Command\Container\Search\FTCONFIG;
|
||||
* @method $this decrby($key, $decrement)
|
||||
* @method $this failover(?To $to = null, bool $abort = false, int $timeout = -1)
|
||||
* @method $this fcall(string $function, array $keys, ...$args)
|
||||
* @method $this ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
|
||||
* @method $this ftaliasadd(string $alias, string $index)
|
||||
* @method $this ftaliasdel(string $alias)
|
||||
* @method $this ftaliasupdate(string $alias, string $index)
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Predis;
|
||||
|
||||
use Predis\Command\Argument\Geospatial\ByInterface;
|
||||
use Predis\Command\Argument\Geospatial\FromInterface;
|
||||
use Predis\Command\Argument\Search\AggregateArguments;
|
||||
use Predis\Command\Argument\Search\AlterArguments;
|
||||
use Predis\Command\Argument\Search\CreateArguments;
|
||||
use Predis\Command\Argument\Search\DropArguments;
|
||||
@@ -104,6 +105,7 @@ use Predis\Response\Status;
|
||||
* @method int decrby(string $key, int $decrement)
|
||||
* @method Status failover(?To $to = null, bool $abort = false, int $timeout = -1)
|
||||
* @method mixed fcall(string $function, array $keys, ...$args)
|
||||
* @method array ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
|
||||
* @method Status ftaliasadd(string $alias, string $index)
|
||||
* @method Status ftaliasdel(string $alias)
|
||||
* @method Status ftaliasupdate(string $alias, string $index)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2023 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Command\Argument\Search;
|
||||
|
||||
class AggregateArguments extends CommonArguments
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $sortingEnum = [
|
||||
'asc' => 'ASC',
|
||||
'desc' => 'DESC',
|
||||
];
|
||||
|
||||
/**
|
||||
* Loads document attributes from the source document.
|
||||
*
|
||||
* @param string ...$fields Could be just '*' to load all fields
|
||||
* @return $this
|
||||
*/
|
||||
public function load(string ...$fields): self
|
||||
{
|
||||
$arguments = func_get_args();
|
||||
|
||||
$this->arguments[] = 'LOAD';
|
||||
|
||||
if ($arguments[0] === '*') {
|
||||
$this->arguments[] = '*';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->arguments[] = count($arguments);
|
||||
$this->arguments = array_merge($this->arguments, $arguments);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads document attributes from the source document.
|
||||
*
|
||||
* @param string ...$properties
|
||||
* @return $this
|
||||
*/
|
||||
public function groupBy(string ...$properties): self
|
||||
{
|
||||
$arguments = func_get_args();
|
||||
|
||||
array_push($this->arguments, 'GROUPBY', count($arguments));
|
||||
$this->arguments = array_merge($this->arguments, $arguments);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the results in the pipeline based on one or more properties.
|
||||
*
|
||||
* If you want to add alias property to your argument just add "true" value in arguments enumeration,
|
||||
* next value will be considered as alias to previous one.
|
||||
*
|
||||
* Example: 'argument', true, 'name' => 'argument' AS 'name'
|
||||
*
|
||||
* @param string $function
|
||||
* @param string|bool ...$argument
|
||||
* @return $this
|
||||
*/
|
||||
public function reduce(string $function, ...$argument): self
|
||||
{
|
||||
$arguments = func_get_args();
|
||||
$functionValue = array_shift($arguments);
|
||||
$argumentsCounter = 0;
|
||||
|
||||
for ($i = 0, $iMax = count($arguments); $i < $iMax; $i++) {
|
||||
if (true === $arguments[$i]) {
|
||||
$arguments[$i] = 'AS';
|
||||
$i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$argumentsCounter++;
|
||||
}
|
||||
|
||||
array_push($this->arguments, 'REDUCE', $functionValue);
|
||||
$this->arguments = array_merge($this->arguments, [$argumentsCounter], $arguments);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the pipeline up until the point of SORTBY, using a list of properties.
|
||||
*
|
||||
* @param int $max
|
||||
* @param string ...$properties Enumeration of properties, including sorting direction (ASC, DESC)
|
||||
* @return $this
|
||||
*/
|
||||
public function sortBy(int $max = 0, ...$properties): self
|
||||
{
|
||||
$arguments = func_get_args();
|
||||
$maxValue = array_shift($arguments);
|
||||
|
||||
$this->arguments[] = 'SORTBY';
|
||||
$this->arguments = array_merge($this->arguments, [count($arguments)], $arguments);
|
||||
|
||||
if ($maxValue !== 0) {
|
||||
array_push($this->arguments, 'MAX', $maxValue);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a 1-to-1 transformation on one or more properties and either stores the result
|
||||
* as a new property down the pipeline or replaces any property using this transformation.
|
||||
*
|
||||
* @param string $expression
|
||||
* @param string $as
|
||||
* @return $this
|
||||
*/
|
||||
public function apply(string $expression, string $as = ''): self
|
||||
{
|
||||
array_push($this->arguments, 'APPLY', $expression);
|
||||
|
||||
if ($as !== '') {
|
||||
array_push($this->arguments, 'AS', $as);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan part of the results with a quicker alternative than LIMIT.
|
||||
*
|
||||
* @param int $readSize
|
||||
* @param int $idleTime
|
||||
* @return $this
|
||||
*/
|
||||
public function withCursor(int $readSize = 0, int $idleTime = 0): self
|
||||
{
|
||||
$this->arguments[] = 'WITHCURSOR';
|
||||
|
||||
if ($readSize !== 0) {
|
||||
array_push($this->arguments, 'COUNT', $readSize);
|
||||
}
|
||||
|
||||
if ($idleTime !== 0) {
|
||||
array_push($this->arguments, 'MAXIDLE', $idleTime);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,77 @@ class CommonArguments implements ArrayableArgument
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not try to use stemming for query expansion but searches the query terms verbatim.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function verbatim(): self
|
||||
{
|
||||
$this->arguments[] = 'VERBATIM';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the timeout parameter of the module.
|
||||
*
|
||||
* @param int $timeout
|
||||
* @return $this
|
||||
*/
|
||||
public function timeout(int $timeout): self
|
||||
{
|
||||
$this->arguments[] = 'TIMEOUT';
|
||||
$this->arguments[] = $timeout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
|
||||
*
|
||||
* @param int $offset
|
||||
* @param int $num
|
||||
* @return $this
|
||||
*/
|
||||
public function limit(int $offset, int $num): self
|
||||
{
|
||||
array_push($this->arguments, 'LIMIT', $offset, $num);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds filter expression into index.
|
||||
*
|
||||
* @param string $filter
|
||||
* @return $this
|
||||
*/
|
||||
public function filter(string $filter): self
|
||||
{
|
||||
$this->arguments[] = 'FILTER';
|
||||
$this->arguments[] = $filter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines one or more value parameters. Each parameter has a name and a value.
|
||||
*
|
||||
* Example: ['name1', 'value1', 'name2', 'value2'...]
|
||||
*
|
||||
* @param array $nameValuesDictionary
|
||||
* @return $this
|
||||
*/
|
||||
public function params(array $nameValuesDictionary): self
|
||||
{
|
||||
$this->arguments[] = 'PARAMS';
|
||||
$this->arguments[] = count($nameValuesDictionary);
|
||||
$this->arguments = array_merge($this->arguments, $nameValuesDictionary);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
||||
@@ -58,20 +58,6 @@ class CreateArguments extends CommonArguments
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds filter expression into index.
|
||||
*
|
||||
* @param string $filter
|
||||
* @return $this
|
||||
*/
|
||||
public function filter(string $filter): self
|
||||
{
|
||||
$this->arguments[] = 'FILTER';
|
||||
$this->arguments[] = $filter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document attribute set as document language.
|
||||
*
|
||||
|
||||
@@ -36,18 +36,6 @@ class SearchArguments extends CommonArguments
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not try to use stemming for query expansion but searches the query terms verbatim.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function verbatim(): self
|
||||
{
|
||||
$this->arguments[] = 'VERBATIM';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the sorting key, right after the id and score and/or payload, if requested.
|
||||
*
|
||||
@@ -237,20 +225,6 @@ class SearchArguments extends CommonArguments
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the timeout parameter of the module.
|
||||
*
|
||||
* @param int $timeout
|
||||
* @return $this
|
||||
*/
|
||||
public function timeout(int $timeout): self
|
||||
{
|
||||
$this->arguments[] = 'TIMEOUT';
|
||||
$this->arguments[] = $timeout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the query terms in the same order in the document as in the query, regardless of the offsets between them.
|
||||
* Typically used in conjunction with SLOP.
|
||||
@@ -329,35 +303,4 @@ class SearchArguments extends CommonArguments
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
|
||||
*
|
||||
* @param int $offset
|
||||
* @param int $num
|
||||
* @return $this
|
||||
*/
|
||||
public function limit(int $offset, int $num): self
|
||||
{
|
||||
array_push($this->arguments, 'LIMIT', $offset, $num);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines one or more value parameters. Each parameter has a name and a value.
|
||||
*
|
||||
* Example: ['name1', 'value1', 'name2', 'value2'...]
|
||||
*
|
||||
* @param array $nameValuesDictionary
|
||||
* @return $this
|
||||
*/
|
||||
public function params(array $nameValuesDictionary): self
|
||||
{
|
||||
$this->arguments[] = 'PARAMS';
|
||||
$this->arguments[] = count($nameValuesDictionary);
|
||||
$this->arguments = array_merge($this->arguments, $nameValuesDictionary);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2023 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Command\Redis\Search;
|
||||
|
||||
use Predis\Command\Command as RedisCommand;
|
||||
|
||||
/**
|
||||
* @see https://redis.io/commands/ft.aggregate/
|
||||
*
|
||||
* Run a search query on an index, and perform aggregate transformations
|
||||
* on the results, extracting statistics etc. from them
|
||||
*/
|
||||
class FTAGGREGATE extends RedisCommand
|
||||
{
|
||||
public function getId()
|
||||
{
|
||||
return 'FT.AGGREGATE';
|
||||
}
|
||||
|
||||
public function setArguments(array $arguments)
|
||||
{
|
||||
[$index, $query] = $arguments;
|
||||
$commandArguments = (!empty($arguments[2])) ? $arguments[2]->toArray() : [];
|
||||
|
||||
parent::setArguments(array_merge(
|
||||
[$index, $query],
|
||||
$commandArguments
|
||||
));
|
||||
}
|
||||
|
||||
public function parseResponse($data)
|
||||
{
|
||||
if (count($data) > 1) {
|
||||
$result = [$data[0]];
|
||||
|
||||
for ($i = 1, $iMax = count($data); $i < $iMax; ++$i) {
|
||||
for ($j = 0, $jMax = count($data[$i]); $j < $jMax; ++$j) {
|
||||
if (array_key_exists($j + 1, $data[$i])) {
|
||||
$result[$i][(string) $data[$i][$j]] = $data[$i][++$j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2023 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Command\Argument\Search;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AggregateArgumentsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var AggregateArguments
|
||||
*/
|
||||
private $arguments;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->arguments = new AggregateArguments();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider loadProvider
|
||||
* @param array $arguments
|
||||
* @param array $expectedResponse
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithLoadModifier(array $arguments, array $expectedResponse): void
|
||||
{
|
||||
$this->arguments->load(...$arguments);
|
||||
|
||||
$this->assertSame($expectedResponse, $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithGroupByModifier(): void
|
||||
{
|
||||
$this->arguments->groupBy('property1', 'property2');
|
||||
|
||||
$this->assertSame(['GROUPBY', 2, 'property1', 'property2'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider reduceProvider
|
||||
* @param array $arguments
|
||||
* @param array $expectedResponse
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithReduceModifier(array $arguments, array $expectedResponse): void
|
||||
{
|
||||
$this->arguments->reduce(...$arguments);
|
||||
|
||||
$this->assertSame($expectedResponse, $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider sortByProvider
|
||||
* @param array $arguments
|
||||
* @param array $expectedResponse
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithSortByModifier(array $arguments, array $expectedResponse): void
|
||||
{
|
||||
$this->arguments->sortBy(...$arguments);
|
||||
|
||||
$this->assertSame($expectedResponse, $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider applyProvider
|
||||
* @param array $arguments
|
||||
* @param array $expectedResponse
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithApplyByModifier(array $arguments, array $expectedResponse): void
|
||||
{
|
||||
$this->arguments->apply(...$arguments);
|
||||
|
||||
$this->assertSame($expectedResponse, $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider withCursorProvider
|
||||
* @param array $arguments
|
||||
* @param array $expectedResponse
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithWithCursorByModifier(array $arguments, array $expectedResponse): void
|
||||
{
|
||||
$this->arguments->withCursor(...$arguments);
|
||||
|
||||
$this->assertSame($expectedResponse, $this->arguments->toArray());
|
||||
}
|
||||
|
||||
public function loadProvider(): array
|
||||
{
|
||||
return [
|
||||
'with given fields' => [
|
||||
['field1', 'field2'],
|
||||
['LOAD', 2, 'field1', 'field2'],
|
||||
],
|
||||
'with all fields' => [
|
||||
['*'],
|
||||
['LOAD', '*'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function reduceProvider(): array
|
||||
{
|
||||
return [
|
||||
'without aliases' => [
|
||||
['function', 'arg1', 'arg2'],
|
||||
['REDUCE', 'function', 2, 'arg1', 'arg2'],
|
||||
],
|
||||
'with aliases' => [
|
||||
['function', 'arg1', true, 'alias1', 'arg2', true, 'alias2'],
|
||||
['REDUCE', 'function', 2, 'arg1', 'AS', 'alias1', 'arg2', 'AS', 'alias2'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function sortByProvider(): array
|
||||
{
|
||||
return [
|
||||
'without sorting direction and max value' => [
|
||||
[0, 'property1', 'property2'],
|
||||
['SORTBY', 2, 'property1', 'property2'],
|
||||
],
|
||||
'with sorting direction' => [
|
||||
[0, 'property1', 'ASC', 'property2', 'DESC'],
|
||||
['SORTBY', 4, 'property1', 'ASC', 'property2', 'DESC'],
|
||||
],
|
||||
'with max value' => [
|
||||
[2, 'property1', 'property2'],
|
||||
['SORTBY', 2, 'property1', 'property2', 'MAX', 2],
|
||||
],
|
||||
'with sorting direction and max value' => [
|
||||
[2, 'property1', 'ASC', 'property2', 'DESC'],
|
||||
['SORTBY', 4, 'property1', 'ASC', 'property2', 'DESC', 'MAX', 2],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function applyProvider(): array
|
||||
{
|
||||
return [
|
||||
'with default arguments' => [
|
||||
['expression'],
|
||||
['APPLY', 'expression'],
|
||||
],
|
||||
'with alias' => [
|
||||
['expression', 'name'],
|
||||
['APPLY', 'expression', 'AS', 'name'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function withCursorProvider(): array
|
||||
{
|
||||
return [
|
||||
'with default arguments' => [
|
||||
[],
|
||||
['WITHCURSOR'],
|
||||
],
|
||||
'with readSize argument' => [
|
||||
[2],
|
||||
['WITHCURSOR', 'COUNT', 2],
|
||||
],
|
||||
'with maxIdle argument' => [
|
||||
[0, 2],
|
||||
['WITHCURSOR', 'MAXIDLE', 2],
|
||||
],
|
||||
'with readSize and maxIdle arguments' => [
|
||||
[3, 2],
|
||||
['WITHCURSOR', 'COUNT', 3, 'MAXIDLE', 2],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -85,4 +85,54 @@ class CommonArgumentsTest extends TestCase
|
||||
|
||||
$this->assertSame(['WITHPAYLOADS'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithVerbatimModifier(): void
|
||||
{
|
||||
$this->arguments->verbatim();
|
||||
|
||||
$this->assertSame(['VERBATIM'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithTimeoutModifier(): void
|
||||
{
|
||||
$this->arguments->timeout(2);
|
||||
|
||||
$this->assertSame(['TIMEOUT', 2], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithLimitModifier(): void
|
||||
{
|
||||
$this->arguments->limit(2, 2);
|
||||
|
||||
$this->assertSame(['LIMIT', 2, 2], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithFilterModifier(): void
|
||||
{
|
||||
$this->arguments->filter('@age>16');
|
||||
|
||||
$this->assertSame(['FILTER', '@age>16'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithParamsModifier(): void
|
||||
{
|
||||
$this->arguments->params(['name1', 'value1', 'name2', 'value2']);
|
||||
|
||||
$this->assertSame(['PARAMS', 4, 'name1', 'value1', 'name2', 'value2'], $this->arguments->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,16 +58,6 @@ class CreateArgumentsTest extends TestCase
|
||||
$this->assertSame(['PREFIX', 2, 'prefix:', 'prefix1:'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithFilterModifier(): void
|
||||
{
|
||||
$this->arguments->filter('@age>16');
|
||||
|
||||
$this->assertSame(['FILTER', '@age>16'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -37,16 +37,6 @@ class SearchArgumentsTest extends TestCase
|
||||
$this->assertSame(['NOCONTENT'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithVerbatimModifier(): void
|
||||
{
|
||||
$this->arguments->verbatim();
|
||||
|
||||
$this->assertSame(['VERBATIM'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -155,16 +145,6 @@ class SearchArgumentsTest extends TestCase
|
||||
$this->assertSame(['SLOP', 2], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithTimeoutModifier(): void
|
||||
{
|
||||
$this->arguments->timeout(2);
|
||||
|
||||
$this->assertSame(['TIMEOUT', 2], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -229,26 +209,6 @@ class SearchArgumentsTest extends TestCase
|
||||
$this->arguments->sortBy('sort_attribute', 'wrong');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithLimitModifier(): void
|
||||
{
|
||||
$this->arguments->limit(2, 2);
|
||||
|
||||
$this->assertSame(['LIMIT', 2, 2], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithParamsModifier(): void
|
||||
{
|
||||
$this->arguments->params(['name1', 'value1', 'name2', 'value2']);
|
||||
|
||||
$this->assertSame(['PARAMS', 4, 'name1', 'value1', 'name2', 'value2'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2023 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Command\Redis\Search;
|
||||
|
||||
use Predis\Command\Argument\Search\AggregateArguments;
|
||||
use Predis\Command\Argument\Search\CreateArguments;
|
||||
use Predis\Command\Argument\Search\SchemaFields\AbstractField;
|
||||
use Predis\Command\Argument\Search\SchemaFields\NumericField;
|
||||
use Predis\Command\Argument\Search\SchemaFields\TextField;
|
||||
use Predis\Command\Redis\PredisCommandTestCase;
|
||||
use Predis\Response\ServerException;
|
||||
|
||||
class FTAGGREGATE_Test extends PredisCommandTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function getExpectedCommand(): string
|
||||
{
|
||||
return FTAGGREGATE::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function getExpectedId(): string
|
||||
{
|
||||
return 'FTAGGREGATE';
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @dataProvider argumentsProvider
|
||||
*/
|
||||
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
$command->setArguments($actualArguments);
|
||||
|
||||
$this->assertSameValues($expectedArguments, $command->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @dataProvider responsesProvider
|
||||
*/
|
||||
public function testParseResponse(array $actualResponse, array $expectedResponse): void
|
||||
{
|
||||
$this->assertSame($expectedResponse, $this->getCommand()->parseResponse($actualResponse));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @return void
|
||||
* @requiresRediSearchVersion >= 1.1.0
|
||||
*/
|
||||
public function testReturnsAggregatedSearchResultWithGivenModifiers(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
$expectedResponse = [
|
||||
2,
|
||||
[
|
||||
'country' => 'Ukraine',
|
||||
'birth' => '1995',
|
||||
'country_birth_Vlad_count' => '2',
|
||||
],
|
||||
[
|
||||
'country' => 'Israel',
|
||||
'birth' => '1994',
|
||||
'country_birth_Vlad_count' => '1',
|
||||
],
|
||||
];
|
||||
|
||||
$ftCreateArguments = (new CreateArguments())->prefix(['user:']);
|
||||
$schema = [
|
||||
new TextField('name'),
|
||||
new TextField('country'),
|
||||
new NumericField('dob', '', AbstractField::SORTABLE),
|
||||
];
|
||||
|
||||
$this->assertEquals('OK', $redis->ftcreate('idx', $schema, $ftCreateArguments));
|
||||
$this->assertSame(
|
||||
3,
|
||||
$redis->hset('user:0', 'name', 'Vlad', 'country', 'Ukraine', 'dob', 813801600)
|
||||
);
|
||||
$this->assertSame(
|
||||
3,
|
||||
$redis->hset('user:1', 'name', 'Vlad', 'country', 'Israel', 'dob', 782265600)
|
||||
);
|
||||
$this->assertSame(
|
||||
3,
|
||||
$redis->hset('user:2', 'name', 'Vlad', 'country', 'Ukraine', 'dob', 813801600)
|
||||
);
|
||||
|
||||
$ftAggregateArguments = (new AggregateArguments())
|
||||
->apply('year(@dob)', 'birth')
|
||||
->groupBy('@country', '@birth')
|
||||
->reduce('COUNT', true, 'country_birth_Vlad_count')
|
||||
->sortBy(0, '@birth', 'DESC');
|
||||
|
||||
$this->assertSame(
|
||||
$expectedResponse,
|
||||
$redis->ftaggregate('idx', '@name: "Vlad"', $ftAggregateArguments)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @return void
|
||||
* @requiresRediSearchVersion >= 1.1.0
|
||||
*/
|
||||
public function testThrowsExceptionOnNonExistingIndex(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->expectExceptionMessage('index: no such index');
|
||||
|
||||
$redis->ftaggregate('index', 'query');
|
||||
}
|
||||
|
||||
public function argumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
'with default arguments' => [
|
||||
['index', 'query'],
|
||||
['index', 'query'],
|
||||
],
|
||||
'with VERBATIM modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->verbatim()],
|
||||
['index', 'query', 'VERBATIM'],
|
||||
],
|
||||
'with LOAD modifier - specified fields' => [
|
||||
['index', 'query', (new AggregateArguments())->load('field1', 'field2')],
|
||||
['index', 'query', 'LOAD', 2, 'field1', 'field2'],
|
||||
],
|
||||
'with LOAD modifier - all fields' => [
|
||||
['index', 'query', (new AggregateArguments())->load('*')],
|
||||
['index', 'query', 'LOAD', '*'],
|
||||
],
|
||||
'with TIMEOUT modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->timeout(2)],
|
||||
['index', 'query', 'TIMEOUT', 2],
|
||||
],
|
||||
'with GROUPBY modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->groupBy('property1', 'property2')],
|
||||
['index', 'query', 'GROUPBY', 2, 'property1', 'property2'],
|
||||
],
|
||||
'with REDUCE modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->reduce('function', 'arg1', true, 'alias1', 'arg2')],
|
||||
['index', 'query', 'REDUCE', 'function', 2, 'arg1', 'AS', 'alias1', 'arg2'],
|
||||
],
|
||||
'with SORTBY modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->sortBy(2, 'property1', 'ASC', 'property2', 'DESC')],
|
||||
['index', 'query', 'SORTBY', 2, 'property1', 'ASC', 'property2', 'DESC', 'MAX', 2],
|
||||
],
|
||||
'with APPLY modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->apply('expression', 'name')],
|
||||
['index', 'query', 'APPLY', 'expression', 'AS', 'name'],
|
||||
],
|
||||
'with LIMIT modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->limit(2, 3)],
|
||||
['index', 'query', 'LIMIT', 2, 3],
|
||||
],
|
||||
'with FILTER modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->filter('filter')],
|
||||
['index', 'query', 'FILTER', 'filter'],
|
||||
],
|
||||
'with WITHCURSOR modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->withCursor(10, 20)],
|
||||
['index', 'query', 'WITHCURSOR', 'COUNT', 10, 'MAXIDLE', 20],
|
||||
],
|
||||
'with PARAMS modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->params(['name1', 'value1', 'name2', 'value2'])],
|
||||
['index', 'query', 'PARAMS', 4, 'name1', 'value1', 'name2', 'value2'],
|
||||
],
|
||||
'with DIALECT modifier' => [
|
||||
['index', 'query', (new AggregateArguments())->dialect('dialect')],
|
||||
['index', 'query', 'DIALECT', 'dialect'],
|
||||
],
|
||||
'with chain of arguments' => [
|
||||
[
|
||||
'index',
|
||||
'@name: "test"',
|
||||
(new AggregateArguments())
|
||||
->apply('year(@dob)', 'birth')
|
||||
->groupBy('@birth', '@country')
|
||||
->reduce('COUNT', true, 'num_visits')
|
||||
->sortBy(0, '@day'),
|
||||
],
|
||||
[
|
||||
'index', '@name: "test"', 'APPLY', 'year(@dob)', 'AS', 'birth', 'GROUPBY', 2, '@birth', '@country',
|
||||
'REDUCE', 'COUNT', 0, 'AS', 'num_visits', 'SORTBY', 1, '@day',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function responsesProvider(): array
|
||||
{
|
||||
return [
|
||||
'with one element response' => [
|
||||
[100],
|
||||
[100],
|
||||
],
|
||||
'with many elements response' => [
|
||||
[
|
||||
1,
|
||||
[
|
||||
'Country',
|
||||
'Ukraine',
|
||||
'Birth',
|
||||
'1995',
|
||||
'Count',
|
||||
2,
|
||||
],
|
||||
],
|
||||
[
|
||||
1,
|
||||
[
|
||||
'Country' => 'Ukraine',
|
||||
'Birth' => '1995',
|
||||
'Count' => 2,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user