Compare commits

...

10 Commits

Author SHA1 Message Date
Till Krüss 153097374b Prepare v3.3.0 release (#1612) 2025-11-24 09:48:50 -08:00
Vladyslav Vildanov 6ef87493ca Added testing with 8.4-GA (#1611) 2025-11-19 08:30:39 -08:00
Vladyslav Vildanov 80430b4cde Added new experimental CAS/CAD functionallity (#1609)
* Added new CAS/CAD functionallity

* Updated CHANGELOG.md

* Codestyle fixes

* Fixed version constraint

* Marked feature as experimental

* Updated version constraints, added missing annotations
2025-11-12 10:14:06 +02:00
Vladyslav Vildanov d0f37f0bac Added temporary XREADGROUP_CLAIM command (#1608)
* Added temporary XREADGROUP_CLAIM command

* Updated CHANGELOG.md

* Added deprecation annotation
2025-11-12 09:14:57 +02:00
Vladyslav Vildanov f11e855047 Added experimental support for FT.HYBRID (#1607)
* Added basic structures and test coverage

* Added most of the test coverage

* Added more test coverage

* Codestyle fixes

* Fixed static analysis

* ANother static analysis fix

* Codestyle fixes

* Fixed version constraints

* Added missing annotations

* Use class context

* Added more test coverage

* Codestyle fixes

* Added experimental mentioning
2025-11-11 15:31:10 +02:00
Vladyslav Vildanov ce853ecec2 Added support for MSET command (#1610)
* Added support for MSET command

* Codestyle fixes
2025-11-11 14:49:07 +02:00
Vladyslav Vildanov f4dcf6ce57 Added testing with 8.4-RC1-pre.2 (#1605)
* Added testing with 8.4-RC1-pre.2

* Updated CHANGELOG.md
2025-11-03 16:49:12 +02:00
Vladyslav Vildanov 72a95b3d75 Added testing with 8.4-RC1-pre image (#1602)
* Added testing with 8.4-RC1-pre image

* Updated CHANGELOG.md
2025-10-27 09:47:24 -07:00
Pavlo Yatsukhnenko 62f4ebcc37 Add AUTH command to Relay connection tests (#1601) 2025-10-20 10:34:31 -07:00
Pavlo Yatsukhnenko f26a13c8f4 Improve Relay compatibility (#1599) 2025-10-20 07:52:40 -07:00
50 changed files with 3947 additions and 42 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
run: |
# Mapping of original redis versions to client test containers
declare -A redis_clients_version_mapping=(
["8.4"]="8.4-M01-pre"
["8.4"]="8.4.0"
["8.2"]="8.2.2-pre"
["8.0"]="8.0.2"
["7.4"]="7.4.2"
+6 -5
View File
@@ -1,16 +1,17 @@
## Changelog
## Unreleased
## v3.3.0 (2025-11-24)
### Added
- Added cluster support for `XADD`, `XDEL` and `XRANGE` (#1587)
- Added prefixable interface for `HEXPIRE` and `HEXPIRETIME` (#1592)
- Added new experimental CAS/CAD functionality (#1609)
- Added temporary `XREADGROUP_CLAIM` command (#1608)
- Added support for `MSET` command (#1610)
- Added experimental support for `FT.HYBRID` (#1607)
### Changed
- Refactor pipeline data writing depends on connection type (#1586)
- Improved compatiblity with Relay (#1597)
### Maintenance
- Added testing with 8.4-M01 (#1593)
- Refactor pipeline data writing depends on connection type (#1586)
## v3.2.0 (2025-08-05)
### Added
+1 -1
View File
@@ -1 +1 @@
3.2.0
3.3.0
+1 -1
View File
@@ -54,7 +54,7 @@ use Traversable;
*/
class Client implements ClientInterface, IteratorAggregate
{
public const VERSION = '3.2.0';
public const VERSION = '3.3.0';
/** @var OptionsInterface */
private $options;
+7 -1
View File
@@ -19,6 +19,7 @@ use Predis\Command\Argument\Search\AlterArguments;
use Predis\Command\Argument\Search\CreateArguments;
use Predis\Command\Argument\Search\DropArguments;
use Predis\Command\Argument\Search\ExplainArguments;
use Predis\Command\Argument\Search\HybridSearch\HybridSearchQuery;
use Predis\Command\Argument\Search\ProfileArguments;
use Predis\Command\Argument\Search\SchemaFields\FieldInterface;
use Predis\Command\Argument\Search\SearchArguments;
@@ -52,6 +53,8 @@ use Predis\Command\Redis\VADD;
*
* @method $this copy(string $source, string $destination, int $db = -1, bool $replace = false)
* @method $this del(array|string $keys)
* @method $this delex(string $key, string $flag, $flagValue)
* @method $this digest(string $key)
* @method $this dump($key)
* @method $this exists($key)
* @method $this expire($key, $seconds, string $expireOption = '')
@@ -126,6 +129,7 @@ use Predis\Command\Redis\VADD;
* @method $this ftdictdump(string $dict)
* @method $this ftdropindex(string $index, ?DropArguments $arguments = null)
* @method $this ftexplain(string $index, string $query, ?ExplainArguments $arguments = null)
* @method $this fthybrid(string $index, HybridSearchQuery $query)
* @method $this ftinfo(string $index)
* @method $this ftprofile(string $index, ProfileArguments $arguments)
* @method $this ftsearch(string $index, string $query, ?SearchArguments $arguments = null)
@@ -148,9 +152,10 @@ use Predis\Command\Redis\VADD;
* @method $this incrbyfloat($key, $increment)
* @method $this mget(array $keys)
* @method $this mset(array $dictionary)
* @method $this msetex(array $dictionary, ?string $existModifier = null, ?string $expireResolution = null, ?int $expireTTL = null)
* @method $this msetnx(array $dictionary)
* @method $this psetex($key, $milliseconds, $value)
* @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null, $flagValue = null)
* @method $this setbit($key, $offset, $value)
* @method $this setex($key, $seconds, $value)
* @method $this setnx($key, $value)
@@ -297,6 +302,7 @@ use Predis\Command\Redis\VADD;
* @method $this xrange(string $key, string $start, string $end, ?int $count = null)
* @method $this xread(int $count = null, int $block = null, array $streams = null, string ...$id)
* @method $this xreadgroup(string $group, string $consumer, ?int $count = null, ?int $blockMs = null, bool $noAck = false, string ...$keyOrId)
* @method $this xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null)
* @method $this xsetid(string $key, string $lastId, ?int $entriesAdded = null, ?string $maxDeleteId = null)
* @method $this xtrim(string $key, array|string $strategy, string $threshold, array $options = null)
* @method $this zadd($key, array $membersAndScoresDictionary)
+7 -1
View File
@@ -19,6 +19,7 @@ use Predis\Command\Argument\Search\AlterArguments;
use Predis\Command\Argument\Search\CreateArguments;
use Predis\Command\Argument\Search\DropArguments;
use Predis\Command\Argument\Search\ExplainArguments;
use Predis\Command\Argument\Search\HybridSearch\HybridSearchQuery;
use Predis\Command\Argument\Search\ProfileArguments;
use Predis\Command\Argument\Search\SchemaFields\FieldInterface;
use Predis\Command\Argument\Search\SearchArguments;
@@ -62,6 +63,8 @@ use Predis\Response\Status;
*
* @method int copy(string $source, string $destination, int $db = -1, bool $replace = false)
* @method int del(string[]|string $keyOrKeys, string ...$keys = null)
* @method int delex(string $key, string $flag, $flagValue)
* @method string digest(string $key)
* @method string|null dump(string $key)
* @method int exists(string $key)
* @method int expire(string $key, int $seconds, string $expireOption = '')
@@ -136,6 +139,7 @@ use Predis\Response\Status;
* @method array ftdictdump(string $dict)
* @method Status ftdropindex(string $index, ?DropArguments $arguments = null)
* @method string ftexplain(string $index, string $query, ?ExplainArguments $arguments = null)
* @method array fthybrid(string $index, HybridSearchQuery $query)
* @method array ftinfo(string $index)
* @method array ftprofile(string $index, ProfileArguments $arguments)
* @method array ftsearch(string $index, string $query, ?SearchArguments $arguments = null)
@@ -158,9 +162,10 @@ use Predis\Response\Status;
* @method string incrbyfloat(string $key, int|float $increment)
* @method array mget(string[]|string $keyOrKeys, string ...$keys = null)
* @method mixed mset(array $dictionary)
* @method array msetex(array $dictionary, ?string $existModifier = null, ?string $expireResolution = null, ?int $expireTTL = null)
* @method int msetnx(array $dictionary)
* @method Status psetex(string $key, $milliseconds, $value)
* @method Status|null set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method Status|null set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null, $flagValue = null)
* @method int setbit(string $key, $offset, $value)
* @method Status setex(string $key, $seconds, $value)
* @method int setnx(string $key, $value)
@@ -308,6 +313,7 @@ use Predis\Response\Status;
* @method array xrange(string $key, string $start, string $end, ?int $count = null)
* @method array|null xread(int $count = null, int $block = null, array $streams = null, string ...$id)
* @method array xreadgroup(string $group, string $consumer, ?int $count = null, ?int $blockMs = null, bool $noAck = false, string ...$keyOrId)
* @method array xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null)
* @method Status xsetid(string $key, string $lastId, ?int $entriesAdded = null, ?string $maxDeleteId = null)
* @method string xtrim(string $key, array|string $strategy, string $threshold, array $options = null)
* @method int zadd(string $key, array $membersAndScoresDictionary)
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\Combine;
use Predis\Command\Argument\ArrayableArgument;
abstract class BaseCombine implements ArrayableArgument
{
/**
* @var array
*/
protected $arguments = ['COMBINE'];
/**
* @var array
*/
protected $as = [];
/**
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->as, 'YIELD_SCORE_AS', $alias);
return $this;
}
/**
* {@inheritDoc}
*/
abstract public function toArray(): array;
}
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\Combine;
class LinearCombineConfig extends BaseCombine
{
/**
* @var float
*/
protected $alpha;
/**
* @var float
*/
protected $beta;
/**
* The weight for the text score (a value between 0 and 1).
*
* @param float $alpha
* @return $this
*/
public function alpha(float $alpha): self
{
$this->alpha = $alpha;
return $this;
}
/**
* The weight for the vector score (a value between 0 and 1).
*
* @param float $beta
* @return $this
*/
public function beta(float $beta): self
{
$this->beta = $beta;
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
$this->arguments[] = 'LINEAR';
$tokens = [];
if ($this->alpha !== null) {
array_push($tokens, 'ALPHA', $this->alpha);
}
if ($this->beta !== null) {
array_push($tokens, 'BETA', $this->beta);
}
if ($this->as) {
array_push($tokens, ...$this->as);
}
if (!empty($tokens)) {
array_push($this->arguments, count($tokens), ...$tokens);
}
return $this->arguments;
}
}
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\Combine;
class RRFCombineConfig extends BaseCombine
{
/**
* @var int
*/
protected $window;
/**
* @var int
*/
protected $rrfConstant;
/**
* The number of top results from each search type to consider for fusion. Defaults to 50.
*
* @param int $window
* @return $this
*/
public function window(int $window): self
{
$this->window = $window;
return $this;
}
/**
* The RRF ranking constant. A smaller value gives more weight to top-ranked items. Defaults to 60.
*
* @param int $constant
* @return $this
*/
public function rrfConstant(int $constant): self
{
$this->rrfConstant = $constant;
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
$this->arguments[] = 'RRF';
$tokens = [];
if ($this->window !== null) {
array_push($tokens, 'WINDOW', $this->window);
}
if ($this->rrfConstant !== null) {
array_push($tokens, 'CONSTANT', $this->rrfConstant);
}
if ($this->as) {
array_push($tokens, ...$this->as);
}
if (!empty($tokens)) {
array_push($this->arguments, count($tokens), ...$tokens);
}
return $this->arguments;
}
}
@@ -0,0 +1,352 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
use Predis\Command\Argument\Search\HybridSearch\Combine\LinearCombineConfig;
use Predis\Command\Argument\Search\HybridSearch\Combine\RRFCombineConfig;
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\KNNVectorSearchConfig;
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\RangeVectorSearchConfig;
use Predis\Command\Redis\Utils\CommandUtility;
use ValueError;
class HybridSearchQuery implements ArrayableArgument
{
public const SORT_ASC = 'ASC';
public const SORT_DESC = 'DESC';
/**
* @var SearchConfig
*/
protected $searchConfig;
/**
* The vector search portion of the query.
*
* @var KNNVectorSearchConfig|RangeVectorSearchConfig
*/
protected $vectorSearchConfig;
/**
* Configuration for the score fusion method (optional).
* If not provided, Reciprocal Rank Fusion (RRF) is used with server-side default parameters.
*
* @var RRFCombineConfig|LinearCombineConfig
*/
protected $combineConfig;
/**
* @var array
*/
protected $load = [];
/**
* @var array
*/
protected $groupBy = [];
/**
* @var array
*/
protected $apply = [];
/**
* @var array
*/
protected $sortBy = [];
/**
* @var string
*/
protected $filter;
/**
* @var array
*/
protected $limit = [];
/**
* @var array
*/
protected $params = [];
/**
* @var bool
*/
protected $explainScore = false;
/**
* @var bool
*/
protected $timeout = false;
/**
* @var array
*/
protected $withCursor = [];
/**
* @var array
*/
protected $arguments = [];
/**
* @param string $vectorSearchMethod Class type of desired vector search method
* @param string $combineMethod Class type of desired combine method
*/
public function __construct(
string $vectorSearchMethod = KNNVectorSearchConfig::class,
string $combineMethod = RRFCombineConfig::class
) {
$this->searchConfig = new SearchConfig();
$this->vectorSearchConfig = new $vectorSearchMethod();
$this->combineConfig = new $combineMethod();
}
/**
* @param callable(SearchConfig): void $callable
* @return $this
*/
public function buildSearchConfig(callable $callable): self
{
$callable($this->searchConfig);
return $this;
}
/**
* @param callable(KNNVectorSearchConfig|RangeVectorSearchConfig): void $callable
* @return $this
*/
public function buildVectorSearchConfig(callable $callable): self
{
$callable($this->vectorSearchConfig);
return $this;
}
/**
* @param callable(RRFCombineConfig|LinearCombineConfig): void $callable
* @return $this
*/
public function buildCombineConfig(callable $callable): self
{
$callable($this->combineConfig);
return $this;
}
/**
* The list of fields to return in the results.
*
* @param array $fields
* @return $this
*/
public function load(array $fields): self
{
array_push($this->load, 'LOAD', count($fields), ...$fields);
return $this;
}
/**
* @param array $fields
* @param Reducer[] $reducers
* @return $this
*/
public function groupBy(array $fields, array $reducers): self
{
array_push($this->groupBy, 'GROUPBY', count($fields), ...$fields);
foreach ($reducers as $reducer) {
array_push($this->groupBy, 'REDUCE', ...$reducer->toArray());
}
return $this;
}
/**
* @param array $expressionFieldDict field => function dictionary
* @return $this
*/
public function apply(array $expressionFieldDict): self
{
foreach ($expressionFieldDict as $field => $function) {
array_push($this->apply, 'APPLY', $function, 'AS', $field);
}
return $this;
}
/**
* Sorts the final results by a specific field.
*
* @param array<string, string> $fields Dictionary with fields and sort direction. Check class constants.
* @return $this
*/
public function sortBy(array $fields): self
{
$fieldsArray = [];
foreach ($fields as $field => $direction) {
if (!in_array(strtoupper($direction), [self::SORT_ASC, self::SORT_DESC])) {
throw new ValueError('Sort direction must be one of "ASC" or "DESC".');
}
array_push($fieldsArray, $field, $direction);
}
array_push($this->sortBy, 'SORTBY', count($fieldsArray), ...$fieldsArray);
return $this;
}
/**
* Final result filtering.
*
* @param string $expression
* @return $this
*/
public function filter(string $expression): self
{
$this->filter = $expression;
return $this;
}
/**
* @param int $offset
* @param int $num
* @return $this
*/
public function limit(int $offset, int $num): self
{
array_push($this->limit, 'LIMIT', $offset, $num);
return $this;
}
/**
* Binds values to named parameters in the query string.
*
* @param array $params
* @return $this
*/
public function params(array $params): self
{
$arrayParams = CommandUtility::dictionaryToArray($params);
array_push($this->params, 'PARAMS', count($arrayParams), ...$arrayParams);
return $this;
}
/**
* @return $this
*/
public function explainScore(): self
{
$this->explainScore = true;
return $this;
}
/**
* @return $this
*/
public function timeout(): self
{
$this->timeout = true;
return $this;
}
/**
* @param int|null $readSize
* @param int|null $idleTime
* @return $this
*/
public function withCursor(?int $readSize = null, ?int $idleTime = null): self
{
$this->withCursor[] = 'WITHCURSOR';
if ($readSize) {
array_push($this->withCursor, 'COUNT', $readSize);
}
if ($idleTime) {
array_push($this->withCursor, 'MAXIDLE', $idleTime);
}
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
$this->arguments = array_merge(
$this->arguments,
$this->searchConfig->toArray(),
$this->vectorSearchConfig->toArray()
);
$combineConfig = $this->combineConfig->toArray();
// Only add if any configuration was applied
if (count($combineConfig) > 2) {
$this->arguments = array_merge($this->arguments, $combineConfig);
}
if ($this->load) {
$this->arguments = array_merge($this->arguments, $this->load);
}
if ($this->groupBy) {
$this->arguments = array_merge($this->arguments, $this->groupBy);
}
if ($this->apply) {
$this->arguments = array_merge($this->arguments, $this->apply);
}
if ($this->sortBy) {
$this->arguments = array_merge($this->arguments, $this->sortBy);
}
if ($this->filter) {
array_push($this->arguments, 'FILTER', $this->filter);
}
if ($this->limit) {
$this->arguments = array_merge($this->arguments, $this->limit);
}
if ($this->params) {
$this->arguments = array_merge($this->arguments, $this->params);
}
if ($this->explainScore) {
$this->arguments[] = 'EXPLAINSCORE';
}
if ($this->timeout) {
$this->arguments[] = 'TIMEOUT';
}
if ($this->withCursor) {
$this->arguments = array_merge($this->arguments, $this->withCursor);
}
return $this->arguments;
}
}
@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
class Reducer implements ArrayableArgument
{
public const REDUCE_COUNT = 'COUNT';
public const REDUCE_COUNT_DISTINCT = 'COUNT_DISTINCT';
public const REDUCE_COUNT_DISTINCTISH = 'COUNT_DISTINCTISH';
public const REDUCE_SUM = 'SUM';
public const REDUCE_MIN = 'MIN';
public const REDUCE_MAX = 'MAX';
public const REDUCE_AVG = 'AVG';
public const REDUCE_STDDEV = 'STDDEV';
public const REDUCE_QUANTILE = 'QUANTILE';
/**
* @var array
*/
protected $arguments = [];
/**
* @param string $function One of the available functions. Check class constants.
* @param array $arguments List of properties
*/
public function __construct(string $function = self::REDUCE_COUNT, array $arguments = [], ?string $alias = null)
{
array_push($this->arguments, $function, count($arguments), ...$arguments);
if ($alias) {
array_push($this->arguments, 'AS', $alias);
}
}
public function toArray(): array
{
return $this->arguments;
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
class ScorerConfig implements ArrayableArgument
{
public const TYPE_BM25 = 'BM25';
public const TYPE_TFIDF = 'TFIDF';
public const TYPE_DISMAX = 'DISMAX';
public const TYPE_DOCSCORE = 'DOCSCORE';
/**
* @var array
*/
protected $arguments = [];
/**
* The text scoring algorithm. Defaults to BM25.
*
* @param string $type
* @return $this
*/
public function type(string $type = self::TYPE_BM25): self
{
$this->arguments[] = $type;
return $this;
}
/**
* An alias for the text score field in the results.
* The aliased field will be included in the `value` object of each returned document.
*
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->arguments, 'YIELD_SCORE_AS', $alias);
return $this;
}
public function toArray(): array
{
return $this->arguments;
}
}
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
class SearchConfig implements ArrayableArgument
{
/**
* @var array
*/
protected $arguments = ['SEARCH'];
/**
* @var ScorerConfig
*/
protected $scorerConfig;
public function __construct()
{
$this->scorerConfig = new ScorerConfig();
}
/**
* Search query.
*
* @param string $query
* @return $this
*/
public function query(string $query): self
{
$this->arguments[] = $query;
return $this;
}
/**
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->arguments, 'YIELD_SCORE_AS', $alias);
return $this;
}
/**
* @param callable(ScorerConfig): void $callable
* @return $this
*/
public function buildScorerConfig(callable $callable): self
{
$callable($this->scorerConfig);
return $this;
}
public function toArray(): array
{
$scorerConfig = $this->scorerConfig->toArray();
if (!empty($scorerConfig)) {
$this->arguments[] = 'SCORER';
$this->arguments = array_merge($this->arguments, $scorerConfig);
}
return $this->arguments;
}
}
@@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\VectorSearch;
use Predis\Command\Argument\ArrayableArgument;
use Predis\Command\Redis\Utils\VectorUtility;
abstract class BaseVectorSearchConfig implements ArrayableArgument
{
public const POLICY_ADHOC = 'ADHOC';
public const POLICY_BATCHES = 'BATCHES';
public const POLICY_ACORN = 'ACORN';
/**
* @var array
*/
protected $vector = [];
/**
* @var array
*/
protected $filter = [];
/**
* @var array
*/
protected $as = [];
/**
* @var array
*/
protected $arguments = ['VSIM'];
/**
* Vector to perform search against.
*
* @param string $field The vector field name to search against. Must start with "@".
* @param string|float[] $value Binary vector representation or array of floats as vector.
* @return self
*/
public function vector(string $field, $value): self
{
if (is_array($value)) {
array_push($this->vector, $field, VectorUtility::toBlob($value));
} else {
array_push($this->vector, $field, $value);
}
return $this;
}
/**
* @param string $expression
* @return $this
*/
public function filter(string $expression): self
{
array_push($this->filter, 'FILTER', $expression);
return $this;
}
/**
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->as, 'YIELD_SCORE_AS', $alias);
return $this;
}
abstract public function toArray(): array;
}
@@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\VectorSearch;
use ValueError;
class KNNVectorSearchConfig extends BaseVectorSearchConfig
{
/**
* @var int
*/
protected $k;
/**
* @var int
*/
protected $ef;
/**
* The number of nearest neighbors to find. Defaults to 10 on server side.
*
* @param int $k
* @return self
*/
public function k(int $k): self
{
$this->k = $k;
return $this;
}
/**
* The HNSW `ef_runtime` parameter for tuning the accuracy/speed trade-off.
*
* @param int $ef
* @return $this
*/
public function ef(int $ef): self
{
$this->ef = $ef;
return $this;
}
public function toArray(): array
{
if (!$this->vector) {
throw new ValueError('Vector configuration not specified.');
}
$this->arguments = array_merge($this->arguments, $this->vector);
if ($this->k || $this->ef) {
$this->arguments[] = 'KNN';
}
$tokens = [];
if ($this->k !== null) {
array_push($tokens, 'K', $this->k);
}
if ($this->ef !== null) {
array_push($tokens, 'EF_RUNTIME', $this->ef);
}
if (!empty($tokens)) {
array_push($this->arguments, count($tokens), ...$tokens);
}
if ($this->filter) {
$this->arguments = array_merge($this->arguments, $this->filter);
}
if ($this->as) {
array_push($this->arguments, ...$this->as);
}
return $this->arguments;
}
}
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\VectorSearch;
use ValueError;
class RangeVectorSearchConfig extends BaseVectorSearchConfig
{
/**
* @var int
*/
protected $radius;
/**
* @var float
*/
protected $epsilon;
/**
* The search radius/threshold. Finds all vectors within this distance.
*
* @param int $radius
* @return $this
*/
public function radius(int $radius): self
{
$this->radius = $radius;
return $this;
}
/**
* @param float $epsilon
* @return $this
*/
public function epsilon(float $epsilon): self
{
$this->epsilon = $epsilon;
return $this;
}
public function toArray(): array
{
if (!$this->vector) {
throw new ValueError('Vector configuration not specified.');
}
$this->arguments = array_merge($this->arguments, $this->vector);
if ($this->radius || $this->epsilon) {
$this->arguments[] = 'RANGE';
}
$tokens = [];
if ($this->radius !== null) {
array_push($tokens, 'RADIUS', $this->radius);
}
if ($this->epsilon !== null) {
array_push($tokens, 'EPSILON', $this->epsilon);
}
if (!empty($tokens)) {
array_push($this->arguments, count($tokens), ...$tokens);
}
if ($this->filter) {
$this->arguments = array_merge($this->arguments, $this->filter);
}
if ($this->as) {
array_push($this->arguments, ...$this->as);
}
return $this->arguments;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
use Predis\Command\PrefixableCommand;
class DELEX extends PrefixableCommand
{
/**
* {@inheritDoc}
*/
public function getId()
{
return 'DELEX';
}
/**
* {@inheritDoc}
*/
public function prefixKeys($prefix)
{
$this->applyPrefixForFirstArgument($prefix);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
use Predis\Command\PrefixableCommand;
class DIGEST extends PrefixableCommand
{
/**
* {@inheritDoc}
*/
public function getId()
{
return 'DIGEST';
}
/**
* {@inheritDoc}
*/
public function prefixKeys($prefix)
{
$this->applyPrefixForFirstArgument($prefix);
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
use Predis\Command\PrefixableCommand;
use ValueError;
class MSETEX extends PrefixableCommand
{
/**
* {@inheritDoc}
*/
public function getId()
{
return 'MSETEX';
}
public function setArguments(array $arguments)
{
$processedArguments = [count(array_keys($arguments[0]))];
array_walk($arguments[0], function ($value, $key) use (&$processedArguments) {
array_push($processedArguments, $key, $value);
});
if (isset($arguments[1])) {
if (!in_array(strtoupper($arguments[1]), ['NX', 'XX'])) {
throw new ValueError('Incorrect exist modifier. Should be one of: NX, XX.');
}
$processedArguments[] = strtoupper($arguments[1]);
}
if (isset($arguments[2])) {
if (!isset($arguments[3]) && strtoupper($arguments[2]) !== 'KEEPTTL') {
throw new ValueError('TTL should be specified along with expire resolution parameter');
}
if (!in_array(strtoupper($arguments[2]), ['EX', 'PX', 'EXAT', 'PXAT', 'KEEPTTL'])) {
throw new ValueError('Incorrect expire modifier. Should be one of: EX, PX, EXAT, PXAT, KEEPTTL');
}
if (strtoupper($arguments[2]) !== 'KEEPTTL') {
array_push($processedArguments, strtoupper($arguments[2]), $arguments[3]);
} else {
$processedArguments[] = strtoupper($arguments[2]);
}
}
parent::setArguments($processedArguments);
}
/**
* {@inheritDoc}
*/
public function prefixKeys($prefix)
{
$arguments = $this->getArguments();
$keysCount = $arguments[0];
$currentKeyIndex = 1;
while ($keysCount > 0) {
$arguments[$currentKeyIndex] = $prefix . $arguments[$currentKeyIndex];
$keysCount--;
$currentKeyIndex += 2;
}
parent::setRawArguments($arguments);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\PrefixableCommand as RedisCommand;
use Predis\Command\Redis\Utils\CommandUtility;
class FTHYBRID extends RedisCommand
{
/**
* @return string
*/
public function getId()
{
return 'FT.HYBRID';
}
public function setArguments(array $arguments)
{
[$index, $query] = $arguments;
parent::setArguments(array_merge(
[$index],
$query->toArray()
));
}
public function parseResponse($data)
{
$response = CommandUtility::arrayToDictionary($data, null, false);
foreach ($response['results'] as $key => $result) {
$response['results'][$key] = CommandUtility::arrayToDictionary($result);
}
return $response;
}
public function prefixKeys($prefix)
{
$this->applyPrefixForFirstArgument($prefix);
}
}
@@ -12,6 +12,7 @@
namespace Predis\Command\Redis\Utils;
use RuntimeException;
use UnexpectedValueException;
class CommandUtility
@@ -53,4 +54,36 @@ class CommandUtility
return $dict;
}
/**
* Converts a value into XXH3 hash.
*
* @param $value
* @return string
*/
public static function xxh3Hash($value): string
{
if (!in_array('xxh3', hash_algos(), true)) {
throw new RuntimeException('XXH3 algorithm is not supported. Please install PECL xxhash extension.');
}
return hash('xxh3', $value);
}
/**
* Converts associative array into flatten array (key1, value1...keyN, valueN).
*
* @param array $dict
* @return array
*/
public static function dictionaryToArray(array $dict): array
{
$array = [];
array_walk($dict, function ($value, $key) use (&$array) {
array_push($array, $key, $value);
});
return $array;
}
}
+7 -3
View File
@@ -57,9 +57,13 @@ class VLINKS extends RedisCommand
if (!is_null($data)) {
if ($this->withScores) {
foreach ($data as $key => $value) {
$data[$key] = CommandUtility::arrayToDictionary($value, function ($key, $value) {
return [$key, (float) $value];
});
if ($value === array_values($value)) {
$data[$key] = CommandUtility::arrayToDictionary($value, function ($key, $value) {
return [$key, (float) $value];
});
} else {
$data[$key] = $value;
}
}
}
}
+8 -1
View File
@@ -12,7 +12,7 @@
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\PrefixableCommand as RedisCommand;
/**
* @see https://redis.io/commands/?name=xgroup
@@ -78,4 +78,11 @@ class XGROUP extends RedisCommand
parent::setArguments($processedArguments);
}
public function prefixKeys($prefix)
{
$arguments = $this->getArguments();
$arguments[1] = $prefix . $arguments[1];
$this->setRawArguments($arguments);
}
}
+19 -1
View File
@@ -12,8 +12,12 @@
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\PrefixableCommand as RedisCommand;
/**
* @deprecated Public API will be changed in the next major version.
* XREADGROUP_CLAIM API will be used instead.
*/
class XREADGROUP extends RedisCommand
{
public function getId()
@@ -58,4 +62,18 @@ class XREADGROUP extends RedisCommand
return $result;
}
public function prefixKeys($prefix)
{
$arguments = $this->getArguments();
$keyIdsStartingIndex = array_search('STREAMS', $arguments) + 1;
$keysAndIdsCount = count($arguments) - $keyIdsStartingIndex;
$keysCount = $keysAndIdsCount / 2;
for ($i = $keyIdsStartingIndex; $i < $keyIdsStartingIndex + $keysCount; $i++) {
$arguments[$i] = $prefix . $arguments[$i];
}
parent::setRawArguments($arguments);
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
use Predis\Command\PrefixableCommand as RedisCommand;
/**
* This is a transitional command. In the next major version this command will replace XREADGROUP.
*/
class XREADGROUP_CLAIM extends RedisCommand
{
public function getId()
{
return 'XREADGROUP';
}
public function setArguments(array $arguments)
{
$processedArguments = ['GROUP', $arguments[0], $arguments[1]];
if (count($arguments) >= 4 && null !== $arguments[3]) {
array_push($processedArguments, 'COUNT', $arguments[3]);
}
if (count($arguments) >= 5 && null !== $arguments[4]) {
array_push($processedArguments, 'BLOCK', $arguments[4]);
}
if (count($arguments) >= 6 && false !== $arguments[5]) {
$processedArguments[] = 'NOACK';
}
if (count($arguments) >= 7 && false !== $arguments[6]) {
array_push($processedArguments, 'CLAIM', $arguments[6]);
}
array_push($processedArguments, 'STREAMS', ...array_keys($arguments[2]), ...array_values($arguments[2]));
parent::setArguments($processedArguments);
}
public function parseResponse($data)
{
if (!is_array($data) || $data === array_values($data)) {
return $data;
}
// Relay
$result = [];
foreach ($data as $key => $value) {
$group = [$key, $value];
$result[] = $group;
}
return $result;
}
public function prefixKeys($prefix)
{
$arguments = $this->getArguments();
$keyIdsStartingIndex = array_search('STREAMS', $arguments) + 1;
$keysAndIdsCount = count($arguments) - $keyIdsStartingIndex;
$keysCount = $keysAndIdsCount / 2;
for ($i = $keyIdsStartingIndex; $i < $keyIdsStartingIndex + $keysCount; $i++) {
$arguments[$i] = $prefix . $arguments[$i];
}
parent::setRawArguments($arguments);
}
}
+8 -1
View File
@@ -16,6 +16,7 @@ use PHPUnit\OneOfConstraint;
use PHPUnit\Util\Test as TestUtil;
use Predis\Client;
use Predis\Command;
use Predis\Command\Processor\KeyPrefixProcessor;
use Predis\Connection;
/**
@@ -262,8 +263,14 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase
$parameters ?: []
);
$commandsFactory = $this->getCommandFactory();
if (null !== $options && array_key_exists('prefix', $options)) {
$commandsFactory->setProcessor(new KeyPrefixProcessor($options['prefix']));
}
$options = array_merge(
['commands' => $this->getCommandFactory()],
['commands' => $commandsFactory],
$options ?: [],
getenv('USE_RELAY') ? ['connections' => 'relay'] : []
);
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\Combine;
use PHPUnit\Framework\TestCase;
class LinearCombineConfigTest extends TestCase
{
/**
* @dataProvider argumentsProvider
* @return void
*/
public function testToArray(
array $expectedReturn,
?float $alpha = null,
?float $beta = null,
?string $as = null
) {
$config = new LinearCombineConfig();
if ($alpha) {
$this->assertEquals($config, $config->alpha($alpha));
}
if ($beta) {
$this->assertEquals($config, $config->beta($beta));
}
if ($as) {
$this->assertEquals($config, $config->as($as));
}
$this->assertSame($expectedReturn, $config->toArray());
}
public function argumentsProvider(): array
{
return [
'with ALPHA' => [['COMBINE', 'LINEAR', 2, 'ALPHA', 0.2], 0.2, null, null],
'with BETA' => [['COMBINE', 'LINEAR', 2, 'BETA', 0.2], null, 0.2, null],
'with all arguments' => [
['COMBINE', 'LINEAR', 6, 'ALPHA', 0.3, 'BETA', 0.2, 'YIELD_SCORE_AS', 'alias'],
0.3, 0.2, 'alias',
],
];
}
}
@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\Combine;
use PHPUnit\Framework\TestCase;
class RRFCombineConfigTest extends TestCase
{
/**
* @dataProvider argumentsProvider
* @return void
*/
public function testToArray(
array $expectedReturn,
?int $window = null,
?int $rrfConstant = null,
?string $as = null
) {
$config = new RRFCombineConfig();
if ($window) {
$this->assertEquals($config, $config->window($window));
}
if ($rrfConstant) {
$this->assertEquals($config, $config->rrfConstant($rrfConstant));
}
if ($as) {
$this->assertEquals($config, $config->as($as));
}
$this->assertSame($expectedReturn, $config->toArray());
}
public function argumentsProvider(): array
{
return [
'with WINDOW' => [['COMBINE', 'RRF', 2, 'WINDOW', 5], 5, null, null],
'with CONSTANT' => [['COMBINE', 'RRF', 2, 'CONSTANT', 5], null, 5, null],
'with all' => [['COMBINE', 'RRF', 6, 'WINDOW', 10, 'CONSTANT', 5, 'YIELD_SCORE_AS', 'alias'], 10, 5, 'alias'],
];
}
}
@@ -0,0 +1,288 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use PHPUnit\Framework\TestCase;
use Predis\Command\Argument\Search\HybridSearch\Combine\LinearCombineConfig;
use Predis\Command\Argument\Search\HybridSearch\Combine\RRFCombineConfig;
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\KNNVectorSearchConfig;
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\RangeVectorSearchConfig;
use Predis\Command\Redis\Utils\VectorUtility;
class HybridSearchQueryTest extends TestCase
{
/**
* @dataProvider argumentsProvider
* @return void
*/
public function testToArray(HybridSearchQuery $config, array $expectedReturn)
{
$this->assertSame($expectedReturn, $config->toArray());
}
public function argumentsProvider(): array
{
return [
'with default configs' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
}),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10],
],
'with RANGE vector search' => [
(new HybridSearchQuery(RangeVectorSearchConfig::class))
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (RangeVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->radius(5)
->epsilon(0.2);
}),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'RANGE', 4, 'RADIUS', 5, 'EPSILON', 0.2],
],
'with COMBINE config - RRF' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->buildCombineConfig(function (RRFCombineConfig $config) {
$config
->window(5)
->rrfConstant(10);
}),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'COMBINE', 'RRF', 4, 'WINDOW', 5, 'CONSTANT', 10],
],
'with COMBINE config - LINEAR' => [
(new HybridSearchQuery(KNNVectorSearchConfig::class, LinearCombineConfig::class))
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->buildCombineConfig(function (LinearCombineConfig $config) {
$config
->alpha(0.2)
->beta(0.3);
}),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'COMBINE', 'LINEAR', 4, 'ALPHA', 0.2, 'BETA', 0.3],
],
'with LOAD' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->load(['field1', 'field2']),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'LOAD', 2, 'field1', 'field2'],
],
'with GROUPBY' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->groupBy(
['field1', 'field2'],
[
new Reducer(Reducer::REDUCE_COUNT, ['prop1', 'prop2'], 'alias'),
new Reducer(Reducer::REDUCE_MAX, ['prop1', 'prop2']),
]
),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'GROUPBY', 2, 'field1', 'field2', 'REDUCE', 'COUNT', 2, 'prop1', 'prop2', 'AS', 'alias', 'REDUCE', 'MAX', 2, 'prop1', 'prop2'],
],
'with APPLY' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->apply(['field' => 'expr']),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'APPLY', 'expr', 'AS', 'field'],
],
'with SORTBY' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->sortBy(['field' => 'DESC']),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'SORTBY', 2, 'field', 'DESC'],
],
'with FILTER' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->filter('expr'),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'FILTER', 'expr'],
],
'with LIMIT' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->limit(0, 10),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'LIMIT', 0, 10],
],
'with PARAMS' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->params(['param1' => 'value1', 'param2' => 'value2']),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'PARAMS', 4, 'param1', 'value1', 'param2', 'value2'],
],
'with EXPLAINSCORE' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->explainScore(),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'EXPLAINSCORE'],
],
'with TIMEOUT' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->timeout(),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'TIMEOUT'],
],
'with WITHCURSOR' => [
(new HybridSearchQuery())
->buildSearchConfig(function (SearchConfig $config) {
$config->buildScorerConfig(function (ScorerConfig $scorerConfig) {
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
})
->query('*');
})
->buildVectorSearchConfig(function (KNNVectorSearchConfig $config) {
$config
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(10);
})
->withCursor(10, 10),
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'WITHCURSOR', 'COUNT', 10, 'MAXIDLE', 10],
],
];
}
}
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use PHPUnit\Framework\TestCase;
class ReducerTest extends TestCase
{
/**
* @return void
*/
public function testToArray(): void
{
$reducer = new Reducer(Reducer::REDUCE_MAX, ['prop1', 'prop2'], 'alias');
$this->assertSame([Reducer::REDUCE_MAX, 2, 'prop1', 'prop2', 'AS', 'alias'], $reducer->toArray());
}
}
@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use PHPUnit\Framework\TestCase;
class ScorerConfigTest extends TestCase
{
/**
* @dataProvider argumentsProvider
* @return void
*/
public function testToArray(
array $expectedReturn,
?string $type = null,
?string $as = null
) {
$config = new ScorerConfig();
if ($type) {
$this->assertEquals($config, $config->type($type));
}
if ($as) {
$this->assertEquals($config, $config->as($as));
}
$this->assertSame($expectedReturn, $config->toArray());
}
public function argumentsProvider(): array
{
return [
'with TYPE' => [[ScorerConfig::TYPE_BM25], ScorerConfig::TYPE_BM25],
'with YIELD_SCORE_AS' => [['YIELD_SCORE_AS', 'alias'], null, 'alias'],
'with all' => [
[ScorerConfig::TYPE_DISMAX, 'YIELD_SCORE_AS', 'alias'],
ScorerConfig::TYPE_DISMAX, 'alias',
],
];
}
}
@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch;
use PHPUnit\Framework\TestCase;
class SearchConfigTest extends TestCase
{
/**
* @dataProvider argumentsProvider
* @return void
*/
public function testToArray(
array $expectedReturn,
?string $query = null,
?string $type = null,
?string $as = null
) {
$config = new SearchConfig();
if ($query) {
$this->assertEquals($config, $config->query($query));
}
if ($type) {
$this->assertEquals($config, $config->buildScorerConfig(function (ScorerConfig $scorerConfig) use ($type) {
$scorerConfig->type($type);
}));
}
if ($as) {
$this->assertEquals($config, $config->as($as));
}
$this->assertSame($expectedReturn, $config->toArray());
}
public function argumentsProvider(): array
{
return [
'with query' => [['SEARCH', '*'], '*', null, null],
'with AS' => [['SEARCH', '*', 'YIELD_SCORE_AS', 'alias'], '*', null, 'alias'],
'with SCORER' => [
['SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DOCSCORE],
'*', ScorerConfig::TYPE_DOCSCORE, null,
],
];
}
}
@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\VectorSearch;
use PHPUnit\Framework\TestCase;
use Predis\Command\Redis\Utils\VectorUtility;
use ValueError;
class KNNVectorSearchConfigTest extends TestCase
{
/**
* @dataProvider argumentsProvider
* @return void
*/
public function testToArray(KNNVectorSearchConfig $config, array $expectedReturn)
{
$this->assertSame($expectedReturn, $config->toArray());
}
/**
* @return void
*/
public function testToArrayThrowsExceptionOnMissingProperty(): void
{
$config = new KNNVectorSearchConfig();
$this->expectException(ValueError::class);
$this->expectExceptionMessage('Vector configuration not specified.');
$config
->filter('filter')
->as('alias')
->toArray();
}
public function argumentsProvider(): array
{
return [
'with vector and K' => [
(new KNNVectorSearchConfig())
->vector('vector', [0.1, 0.2, 0.3])
->k(5),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 2, 'K', 5]],
'with vector, K and EF_RUNTIME' => [
(new KNNVectorSearchConfig())
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->ef(5),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 5]],
'with vector, K and FILTER' => [
(new KNNVectorSearchConfig())
->vector('vector', [0.1, 0.2, 0.3])
->k(5)
->filter('*'),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 2, 'K', 5, 'FILTER', '*']],
'with all' => [
(new KNNVectorSearchConfig())
->vector('vector', VectorUtility::toBlob([0.1, 0.2, 0.3]))
->k(5)
->ef(5)
->filter('*')
->as('alias'),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'KNN', 4, 'K', 5, 'EF_RUNTIME', 5, 'FILTER', '*', 'YIELD_SCORE_AS', 'alias']],
];
}
}
@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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\HybridSearch\VectorSearch;
use PHPUnit\Framework\TestCase;
use Predis\Command\Redis\Utils\VectorUtility;
use ValueError;
class RangeVectorSearchConfigTest extends TestCase
{
/**
* @dataProvider argumentsProvider
* @return void
*/
public function testToArray(RangeVectorSearchConfig $config, array $expectedReturn)
{
$this->assertSame($expectedReturn, $config->toArray());
}
/**
* @return void
*/
public function testAs()
{
$config = new RangeVectorSearchConfig();
$this->assertEquals($config, $config->as('alias'));
}
/**
* @return void
*/
public function testToArrayThrowsExceptionOnMissingProperty(): void
{
$config = new RangeVectorSearchConfig();
$this->expectException(ValueError::class);
$this->expectExceptionMessage('Vector configuration not specified.');
$config
->filter('filter')
->as('alias')
->toArray();
}
public function argumentsProvider(): array
{
return [
'with vector and RADIUS' => [
(new RangeVectorSearchConfig())
->vector('vector', [0.1, 0.2, 0.3])
->radius(5),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'RANGE', 2, 'RADIUS', 5]],
'with vector, RADIUS and EPSILON' => [
(new RangeVectorSearchConfig())
->vector('vector', [0.1, 0.2, 0.3])
->radius(5)
->epsilon(0.2),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'RANGE', 4, 'RADIUS', 5, 'EPSILON', 0.2]],
'with vector, RADIUS and FILTER' => [
(new RangeVectorSearchConfig())
->vector('vector', [0.1, 0.2, 0.3])
->radius(5)
->filter('*'),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'RANGE', 2, 'RADIUS', 5, 'FILTER', '*']],
'with all arguments' => [
(new RangeVectorSearchConfig())
->vector('vector', VectorUtility::toBlob([0.1, 0.2, 0.3]))
->radius(5)
->epsilon(0.2)
->as('alias')
->filter('*'),
['VSIM', 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3]), 'RANGE', 4, 'RADIUS', 5, 'EPSILON', 0.2, 'FILTER', '*', 'YIELD_SCORE_AS', 'alias']],
];
}
}
@@ -241,6 +241,14 @@ class KeyPrefixProcessorTest extends PredisTestCase
['key1', 'key2', 'key3'],
['prefix:key1', 'prefix:key2', 'prefix:key3'],
],
['DELEX',
['key1', 'IFEQ', 'value'],
['prefix:key1', 'IFEQ', 'value'],
],
['DIGEST',
['key1'],
['prefix:key1'],
],
['TYPE',
['key'],
['prefix:key'],
@@ -301,6 +309,10 @@ class KeyPrefixProcessorTest extends PredisTestCase
['foo', 'bar', 'hoge', 'piyo'],
['prefix:foo', 'bar', 'prefix:hoge', 'piyo'],
],
['MSETEX',
[['key' => 'value', 'key1' => 'value', 'key2' => 'value'], 'NX', 'EX', 10],
[3, 'prefix:key', 'value', 'prefix:key1', 'value', 'prefix:key2', 'value', 'NX', 'EX', 10],
],
['MSETNX',
['foo', 'bar', 'hoge', 'piyo'],
['prefix:foo', 'bar', 'prefix:hoge', 'piyo'],
@@ -804,6 +816,18 @@ class KeyPrefixProcessorTest extends PredisTestCase
['key', 'MAXLEN', 100],
['prefix:key', 'MAXLEN', 100],
],
['XGROUP',
['CREATE', 'key', 'group', '$'],
['CREATE', 'prefix:key', 'group', '$'],
],
['XREADGROUP',
['group', 'consumer', 10, 10, true, 'stream', 'stream1', '0-0', '0-0'],
['GROUP', 'group', 'consumer', 'COUNT', 10, 'BLOCK', 10, 'NOACK', 'STREAMS', 'prefix:stream', 'prefix:stream1', '0-0', '0-0'],
],
['XREADGROUP_CLAIM',
['group', 'consumer', ['stream' => '0-0', 'stream1' => '0-0'], 10, 10, true, 10],
['GROUP', 'group', 'consumer', 'COUNT', 10, 'BLOCK', 10, 'NOACK', 'CLAIM', 10, 'STREAMS', 'prefix:stream', 'prefix:stream1', '0-0', '0-0'],
],
['ZPOPMIN',
['key'],
['prefix:key'],
+115
View File
@@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
use Predis\Command\Redis\Utils\CommandUtility;
class DELEX_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return DELEX::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'DELEX';
}
/**
* @group disconnected
*/
public function testFilterArguments(): void
{
$arguments = ['key1'];
$expected = ['key1'];
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group connected
* @requires PHP >= 8.1
* @requiresRedisVersion >= 8.3.224
*/
public function testDeleteValueWithDifferentModifiers(): void
{
$redis = $this->getClient();
$this->assertEquals(
'OK',
$redis->set('foo', 'bar')
);
$value = $redis->get('foo');
$this->assertEquals(
1,
$redis->delex('foo', 'IFEQ', $value)
);
$this->assertEquals(
'OK',
$redis->set('foo', 'bar')
);
$this->assertEquals(
0,
$redis->delex('foo', 'IFEQ', 'wrong')
);
$this->assertEquals(
1,
$redis->delex('foo', 'IFNE', 'not equal')
);
$this->assertEquals(
'OK',
$redis->set('foo', 'bar')
);
$this->assertEquals(
0,
$redis->delex('foo', 'IFNE', $value)
);
$this->assertEquals(
1,
$redis->delex('foo', 'IFDEQ', CommandUtility::xxh3Hash($value))
);
$this->assertEquals(
'OK',
$redis->set('foo', 'bar')
);
$this->assertEquals(
0,
$redis->delex('foo', 'IFDEQ', CommandUtility::xxh3Hash('wrong'))
);
$this->assertEquals(
1,
$redis->delex('foo', 'IFDNE', CommandUtility::xxh3Hash('not equal'))
);
$this->assertEquals(
'OK',
$redis->set('foo', 'bar')
);
$this->assertEquals(
0,
$redis->delex('foo', 'IFDNE', CommandUtility::xxh3Hash($value))
);
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
use Predis\Command\Redis\Utils\CommandUtility;
class DIGEST_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return DIGEST::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'DIGEST';
}
/**
* @group disconnected
*/
public function testFilterArguments(): void
{
$arguments = ['key1'];
$expected = ['key1'];
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group connected
* @requires PHP >= 8.1
* @requiresRedisVersion >= 8.3.224
*/
public function testReturnsDigestOfGivenValue(): void
{
$redis = $this->getClient();
$this->assertEquals('OK', $redis->set('foo', 'bar'));
$this->assertEquals(CommandUtility::xxh3Hash('bar'), $redis->digest('foo'));
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
use ValueError;
class MSETEX_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return MSETEX::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'MSETEX';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 8.3.224
*/
public function testSetWithModifiers()
{
$redis = $this->getClient();
$this->assertEquals(1, $redis->msetex(['foo' => 'bar', 'bar' => 'baz'], 'nx'));
$this->assertEquals(0, $redis->msetex(['foo' => 'bar', 'bar' => 'baz'], 'nx'));
$this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], 'xx'));
$this->assertEquals(0, $redis->msetex(['foo' => 'baz', 'baz' => 'bar'], 'xx'));
$this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'ex', 10));
$this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'px', 1000));
$this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'exat', time() + 10));
$this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'pxat', (time() * 1000) + 1000));
$this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'keepttl'));
$this->assertGreaterThan(0, $redis->expiretime('foo'));
}
/**
* @group disconnected
* @return void
*/
public function testThrowsExceptionOnInvalidArguments(): void
{
$command = $this->getCommand();
$this->expectException(ValueError::class);
$this->expectExceptionMessage('Incorrect exist modifier. Should be one of: NX, XX.');
$command->setArguments([['key' => 'value'], 'wrong']);
$this->expectException(ValueError::class);
$this->expectExceptionMessage('TTL should be specified along with expire resolution parameter');
$command->setArguments([['key' => 'value'], null, 'EX']);
$this->expectException(ValueError::class);
$this->expectExceptionMessage('Incorrect expire modifier. Should be one of: EX, PX, EXAT, PXAT, KEEPTTL');
$command->setArguments([['key' => 'value'], null, 'wrong', 10]);
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
[['field1' => 'value1', 'field2' => 'value2']],
[2, 'field1', 'value1', 'field2', 'value2'],
],
'with exist modifier - NX' => [
[['field1' => 'value1', 'field2' => 'value2'], 'nx'],
[2, 'field1', 'value1', 'field2', 'value2', 'NX'],
],
'with exist modifier - XX' => [
[['field1' => 'value1', 'field2' => 'value2'], 'xx'],
[2, 'field1', 'value1', 'field2', 'value2', 'XX'],
],
'with expire modifier - EX' => [
[['field1' => 'value1', 'field2' => 'value2'], null, 'EX', 10],
[2, 'field1', 'value1', 'field2', 'value2', 'EX', 10],
],
'with expire modifier - PX' => [
[['field1' => 'value1', 'field2' => 'value2'], null, 'PX', 10],
[2, 'field1', 'value1', 'field2', 'value2', 'PX', 10],
],
'with expire modifier - EXAT' => [
[['field1' => 'value1', 'field2' => 'value2'], null, 'EXAT', 10],
[2, 'field1', 'value1', 'field2', 'value2', 'EXAT', 10],
],
'with expire modifier - PXAT' => [
[['field1' => 'value1', 'field2' => 'value2'], null, 'PXAT', 10],
[2, 'field1', 'value1', 'field2', 'value2', 'PXAT', 10],
],
'with expire modifier - KEETTL' => [
[['field1' => 'value1', 'field2' => 'value2'], null, 'KEEPTTL'],
[2, 'field1', 'value1', 'field2', 'value2', 'KEEPTTL'],
],
'with all modifiers' => [
[['field1' => 'value1', 'field2' => 'value2'], 'xx', 'KEEPTTL'],
[2, 'field1', 'value1', 'field2', 'value2', 'XX', 'KEEPTTL'],
],
];
}
}
+45
View File
@@ -13,6 +13,7 @@
namespace Predis\Command\Redis;
use Predis\Command\PrefixableCommand;
use Predis\Command\Redis\Utils\CommandUtility;
/**
* @group commands
@@ -256,6 +257,50 @@ class SET_Test extends PredisCommandTestCase
$this->testSetStringValueWithModifierXX();
}
/**
* @group connected
* @requires PHP >= 8.1
* @requiresRedisVersion >= 8.3.224
*/
public function testSetStringValueWithNewModifiers(): void
{
$redis = $this->getClient();
$this->assertEquals(
'OK',
$redis->set('foo', 'bar')
);
$value = $redis->get('foo');
$this->assertEquals(
'OK',
$redis->set('foo', $value, null, null, 'IFEQ', $value)
);
$this->assertNull(
$redis->set('foo', $value, null, null, 'IFEQ', 'wrong')
);
$this->assertEquals(
'OK',
$redis->set('foo', $value, null, null, 'IFNE', 'not equal')
);
$this->assertNull(
$redis->set('foo', $value, null, null, 'IFNE', $value)
);
$this->assertEquals(
'OK',
$redis->set('foo', $value, null, null, 'IFDEQ', CommandUtility::xxh3Hash($value))
);
$this->assertNull(
$redis->set('foo', $value, null, null, 'IFDEQ', CommandUtility::xxh3Hash('wrong'))
);
$this->assertEquals(
'OK',
$redis->set('foo', $value, null, null, 'IFDNE', CommandUtility::xxh3Hash('not equal'))
);
$this->assertNull(
$redis->set('foo', $value, null, null, 'IFDNE', CommandUtility::xxh3Hash($value))
);
}
/**
* @group connected
* @group cluster
File diff suppressed because it is too large Load Diff
-4
View File
@@ -66,8 +66,6 @@ class VEMB_Test extends PredisCommandTestCase
/**
* @dataProvider quantisationProvider
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -100,8 +98,6 @@ class VEMB_Test extends PredisCommandTestCase
/**
* @dataProvider quantisationProvider
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -62,8 +62,6 @@ class VGETATTR_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -88,8 +86,6 @@ class VGETATTR_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -62,8 +62,6 @@ class VLINKS_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -96,8 +94,6 @@ class VLINKS_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
-4
View File
@@ -54,8 +54,6 @@ class VREM_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -80,8 +78,6 @@ class VREM_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -59,8 +59,6 @@ class VSETATTR_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -80,8 +78,6 @@ class VSETATTR_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
-2
View File
@@ -134,8 +134,6 @@ class VSIM_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 8.0.0
*/
@@ -0,0 +1,291 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 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;
class XREADGROUP_CLAIM_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return XREADGROUP_CLAIM::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'XREADGROUP';
}
/**
* @dataProvider argumentsProvider
* @group disconnected
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 5.0.0
*/
public function testReadsFromGivenConsumerGroup(): void
{
$redis = $this->getClient();
$streamInitId = $redis->xadd('stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('stream', 'group', $streamInitId));
$nextId = $redis->xadd('stream', ['newField' => 'newValue']);
$expectedResponse = [
[
'stream',
[
[$nextId, ['newField', 'newValue']],
],
],
];
$this->assertSame(
$expectedResponse,
$redis->xreadgroup_claim(
'group',
'consumer',
['stream' => '>']
)
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 5.0.0
*/
public function testReadsFromConsumerGroupFromMultipleStreams(): void
{
$redis = $this->getClient();
$streamInitId = $redis->xadd('stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('stream', 'group', $streamInitId));
$anotherStreamInitId = $redis->xadd('another_stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('another_stream', 'group', $anotherStreamInitId));
$nextId = $redis->xadd('stream', ['newField' => 'newValue']);
$anotherNextId = $redis->xadd('another_stream', ['newField' => 'newValue']);
$expectedResponse = [
[
'stream',
[
[$nextId, ['newField', 'newValue']],
],
],
[
'another_stream',
[
[$anotherNextId, ['newField', 'newValue']],
],
],
];
$this->assertSame(
$expectedResponse,
$redis->xreadgroup_claim(
'group',
'consumer',
['stream' => '>', 'another_stream' => '>']
)
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 5.0.0
*/
public function testPrefixStreamKeys(): void
{
$redis = $this->createClient(null, ['prefix' => 'prefix:']);
$streamInitId = $redis->xadd('stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('stream', 'group', $streamInitId));
$anotherStreamInitId = $redis->xadd('another_stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('another_stream', 'group', $anotherStreamInitId));
$nextId = $redis->xadd('stream', ['newField' => 'newValue']);
$anotherNextId = $redis->xadd('another_stream', ['newField' => 'newValue']);
$expectedResponse = [
[
'prefix:stream',
[
[$nextId, ['newField', 'newValue']],
],
],
[
'prefix:another_stream',
[
[$anotherNextId, ['newField', 'newValue']],
],
],
];
$this->assertSame(
$expectedResponse,
$redis->xreadgroup_claim(
'group',
'consumer',
['stream' => '>', 'another_stream' => '>']
)
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 8.3.224
*/
public function testReadsAndClaimFromConsumerGroupFromSingleStream(): void
{
$redis = $this->getClient();
$streamInitId = $redis->xadd('stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('stream', 'group', $streamInitId));
$nextId = $redis->xadd('stream', ['newField' => 'newValue']);
$expectedResponse = [
[
'stream',
[
[$nextId, ['newField', 'newValue'], '0', '0'],
],
],
];
$this->assertEmpty(
$redis->xpending('stream', 'group', null, '-', '+', 5)
);
$this->assertEquals(
$expectedResponse,
$redis->xreadgroup_claim(
'group',
'consumer',
['stream' => '>'], null, null, false, 10)
);
$this->assertCount(
4,
$redis->xpending('stream', 'group', null, '-', '+', 5)[0]
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 8.3.224
*/
public function testReadsAndClaimFromConsumerGroupFromMultipleStreams(): void
{
$redis = $this->getClient();
$streamInitId = $redis->xadd('stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('stream', 'group', $streamInitId));
$anotherStreamInitId = $redis->xadd('another_stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('another_stream', 'group', $anotherStreamInitId));
$nextId = $redis->xadd('stream', ['newField' => 'newValue']);
$anotherNextId = $redis->xadd('another_stream', ['newField' => 'newValue']);
$expectedResponse = [
[
'stream',
[
[$nextId, ['newField', 'newValue'], '0', '0'],
],
],
[
'another_stream',
[
[$anotherNextId, ['newField', 'newValue'], '0', '0'],
],
],
];
$this->assertEmpty(
$redis->xpending('stream', 'group', null, '-', '+', 5)
);
$this->assertEmpty(
$redis->xpending('another_stream', 'group', null, '-', '+', 5)
);
$this->assertEquals(
$expectedResponse,
$redis->xreadgroup_claim(
'group',
'consumer',
['stream' => '>', 'another_stream' => '>'], null, null, false, 10)
);
$this->assertCount(
4,
$redis->xpending('stream', 'group', null, '-', '+', 5)[0]
);
$this->assertCount(
4,
$redis->xpending('another_stream', 'group', null, '-', '+', 5)[0]
);
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['group', 'consumer', ['stream' => '0-0']],
['GROUP', 'group', 'consumer', 'STREAMS', 'stream', '0-0'],
],
'with COUNT modifier' => [
['group', 'consumer', ['stream' => '0-0'], 10],
['GROUP', 'group', 'consumer', 'COUNT', 10, 'STREAMS', 'stream', '0-0'],
],
'with BLOCK modifier' => [
['group', 'consumer', ['stream' => '0-0'], null, 10],
['GROUP', 'group', 'consumer', 'BLOCK', 10, 'STREAMS', 'stream', '0-0'],
],
'with NOACK modifier' => [
['group', 'consumer', ['stream' => '0-0'], null, null, true],
['GROUP', 'group', 'consumer', 'NOACK', 'STREAMS', 'stream', '0-0'],
],
'with CLAIM modifier' => [
['group', 'consumer', ['stream' => '0-0'], null, null, false, 10],
['GROUP', 'group', 'consumer', 'CLAIM', 10, 'STREAMS', 'stream', '0-0'],
],
'with all arguments' => [
['group', 'consumer', ['stream' => '0-0', 'stream1' => '0-0'], 10, 10, true, 20],
['GROUP', 'group', 'consumer', 'COUNT', 10, 'BLOCK', 10, 'NOACK', 'CLAIM', 20, 'STREAMS', 'stream', 'stream1', '0-0', '0-0'],
],
];
}
}
+49 -2
View File
@@ -54,8 +54,6 @@ class XREADGROUP_Test extends PredisCommandTestCase
/**
* @group connected
* @group relay-incompatible
* @group relay-fixme
* @return void
* @requiresRedisVersion >= 5.0.0
*/
@@ -138,6 +136,55 @@ class XREADGROUP_Test extends PredisCommandTestCase
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 5.0.0
*/
public function testPrefixStreamKeys(): void
{
$redis = $this->createClient(null, ['prefix' => 'prefix:']);
$streamInitId = $redis->xadd('stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('stream', 'group', $streamInitId));
$anotherStreamInitId = $redis->xadd('another_stream', ['field' => 'value']);
$this->assertEquals('OK', $redis->xgroup->create('another_stream', 'group', $anotherStreamInitId));
$nextId = $redis->xadd('stream', ['newField' => 'newValue']);
$anotherNextId = $redis->xadd('another_stream', ['newField' => 'newValue']);
$expectedResponse = [
[
'prefix:stream',
[
[$nextId, ['newField', 'newValue']],
],
],
[
'prefix:another_stream',
[
[$anotherNextId, ['newField', 'newValue']],
],
],
];
$this->assertSame(
$expectedResponse,
$redis->xreadgroup(
'group',
'consumer',
null,
null,
false,
'stream',
'another_stream',
'>',
'>'
)
);
}
/**
* @group connected
* @return void
@@ -14,11 +14,13 @@ namespace Predis\Command\Utils;
use Predis\Command\Redis\Utils\CommandUtility;
use PredisTestCase;
use RuntimeException;
use UnexpectedValueException;
class CommandUtilityTest extends PredisTestCase
{
/**
* @group disconnected
* @dataProvider arrayProvider
* @param array $actual
* @param array $expected
@@ -32,6 +34,7 @@ class CommandUtilityTest extends PredisTestCase
}
/**
* @group disconnected
* @return void
*/
public function testArrayToDictionaryThrowsExceptionOnOddNumberOfElements()
@@ -42,6 +45,49 @@ class CommandUtilityTest extends PredisTestCase
CommandUtility::arrayToDictionary(['key1', 'value1', 'key1']);
}
/**
* @group disconnected
* @requires PHP >= 8.1
* @return void
*/
public function testXXH3Hash()
{
$expectedHash = '87d57e269b9df0f0';
$this->assertEquals($expectedHash, CommandUtility::xxh3Hash('value'));
}
/**
* @group disconnected
* @requires PHP < 8.1
* @return void
*/
public function testXXH3HashRaiseExceptionOnPHPLowerThan81()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('XXH3 algorithm is not supported. Please install PECL xxhash extension.');
CommandUtility::xxh3Hash('value');
}
/**
* @group disconnected
* @return void
*/
public function testDictionaryToArray(): void
{
$dict = [
'key1' => 'value1',
'key2' => 'value2',
'key3' => 'value3',
];
$this->assertSame(
['key1', 'value1', 'key2', 'value2', 'key3', 'value3'],
CommandUtility::dictionaryToArray($dict)
);
}
public function arrayProvider(): array
{
return [
@@ -472,7 +472,9 @@ class RelayConnectionTest extends PredisTestCase
*/
public function testExecutesCommand(): void
{
$connection = new RelayConnection(new Parameters(), new Relay());
$parameters = $this->getParameters();
$connection = new RelayConnection($parameters, new Relay());
$connection->executeCommand(new RawCommand('AUTH', [$parameters->password]));
$this->assertEquals(
'OK',
@@ -491,7 +493,9 @@ class RelayConnectionTest extends PredisTestCase
*/
public function testConnectWithOnConnectionCommands(): void
{
$connection = new RelayConnection(new Parameters(), new Relay());
$parameters = $this->getParameters();
$connection = new RelayConnection($parameters, new Relay());
$connection->addConnectCommand(new RawCommand('AUTH', [$parameters->password]));
$connection->addConnectCommand(new RawCommand('CLIENT', ['SETNAME', 'predis']));
$connection->connect();