Skip to content

Commit fe13c0b

Browse files
authored
Added Vector query and Tag filter entities (#5)
* Added Vector query and Tag filter entities * Fixed feature tests * Added missing unit test * Added part of feature tests * Fixed unit test * Added missing coverage * Added more feature test coverage
1 parent c2bf21a commit fe13c0b

File tree

16 files changed

+1261
-5
lines changed

16 files changed

+1261
-5
lines changed

src/Enum/Condition.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Vladvildanov\PredisVl\Enum;
4+
5+
use Vladvildanov\PredisVl\Enum\Traits\EnumNames;
6+
7+
enum Condition: string
8+
{
9+
use EnumNames;
10+
11+
case equal = '==';
12+
case notEqual = '!=';
13+
case greaterThan = '>';
14+
case lowerThan = '<';
15+
case pattern = '%';
16+
}

src/Enum/Logical.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Vladvildanov\PredisVl\Enum;
4+
5+
use Vladvildanov\PredisVl\Enum\Traits\EnumNames;
6+
7+
enum Logical: string
8+
{
9+
use EnumNames;
10+
11+
case and = '&&';
12+
case or = '||';
13+
}

src/Factory.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Vladvildanov\PredisVl;
44

55
use Predis\Command\Argument\Search\CreateArguments;
6+
use Predis\Command\Argument\Search\SearchArguments;
67

78
/**
89
* Simple factory for general purpose objects creation.
@@ -16,4 +17,12 @@ public function createIndexBuilder(): CreateArguments
1617
{
1718
return new CreateArguments();
1819
}
20+
21+
/**
22+
* @inheritDoc
23+
*/
24+
public function createSearchBuilder(): SearchArguments
25+
{
26+
return new SearchArguments();
27+
}
1928
}

src/FactoryInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Vladvildanov\PredisVl;
44

55
use Predis\Command\Argument\Search\CreateArguments;
6+
use Predis\Command\Argument\Search\SearchArguments;
67

78
interface FactoryInterface
89
{
@@ -12,4 +13,11 @@ interface FactoryInterface
1213
* @return CreateArguments
1314
*/
1415
public function createIndexBuilder(): CreateArguments;
16+
17+
/**
18+
* Creates builder object for search command arguments.
19+
*
20+
* @return SearchArguments
21+
*/
22+
public function createSearchBuilder(): SearchArguments;
1523
}

src/Index/IndexInterface.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Vladvildanov\PredisVl\Index;
44

5+
use Vladvildanov\PredisVl\Query\QueryInterface;
6+
57
interface IndexInterface
68
{
79
/**
@@ -35,4 +37,12 @@ public function load(string $key, mixed $values): bool;
3537
* @return mixed
3638
*/
3739
public function fetch(string $id): mixed;
40+
41+
/**
42+
* Query for a data from current index.
43+
*
44+
* @param QueryInterface $query
45+
* @return mixed
46+
*/
47+
public function query(QueryInterface $query);
3848
}

src/Index/SearchIndex.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Vladvildanov\PredisVl\Enum\StorageType;
1111
use Vladvildanov\PredisVl\Factory;
1212
use Vladvildanov\PredisVl\FactoryInterface;
13+
use Vladvildanov\PredisVl\Query\QueryInterface;
1314

1415
class SearchIndex implements IndexInterface
1516
{
@@ -94,6 +95,10 @@ public function create(bool $isOverwrite = false): bool
9495
*/
9596
public function load(string $key, mixed $values): bool
9697
{
98+
$key = (array_key_exists('prefix', $this->schema['index']))
99+
? $this->schema['index']['prefix'] . $key
100+
: $key;
101+
97102
if (is_string($values)) {
98103
$response = $this->client->jsonset($key, '$', $values);
99104
} elseif (is_array($values)) {
@@ -122,6 +127,44 @@ public function fetch(string $id): mixed
122127
return $this->client->hgetall($key);
123128
}
124129

130+
/**
131+
* @inheritDoc
132+
*/
133+
public function query(QueryInterface $query)
134+
{
135+
$response = $this->client->ftsearch(
136+
$this->schema['index']['name'],
137+
$query->getQueryString(),
138+
$query->getSearchArguments()
139+
);
140+
141+
$processedResponse = ['count' => $response[0]];
142+
$withScores = in_array('WITHSCORES', $query->getSearchArguments()->toArray(), true);
143+
144+
if (count($response) > 1) {
145+
for ($i = 1, $iMax = count($response); $i < $iMax; $i++) {
146+
$processedResponse['results'][$response[$i]] = [];
147+
148+
// Different return type depends on WITHSCORE condition
149+
if ($withScores) {
150+
$processedResponse['results'][$response[$i]]['score'] = $response[$i + 1];
151+
$step = 2;
152+
} else {
153+
$step = 1;
154+
}
155+
156+
for ($j = 0, $jMax = count($response[$i + $step]); $j < $jMax; $j++) {
157+
$processedResponse['results'][$response[$i]][$response[$i + $step][$j]] = $response[$i + $step][$j + 1];
158+
++$j;
159+
}
160+
161+
$i += $step;
162+
}
163+
}
164+
165+
return $processedResponse;
166+
}
167+
125168
/**
126169
* Validates schema array.
127170
*

src/Query/AbstractQuery.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Vladvildanov\PredisVl\Query;
4+
5+
use Predis\Command\Argument\Search\SearchArguments;
6+
use Vladvildanov\PredisVl\Factory;
7+
use Vladvildanov\PredisVl\FactoryInterface;
8+
use Vladvildanov\PredisVl\Query\Filter\FilterInterface;
9+
10+
abstract class AbstractQuery implements QueryInterface
11+
{
12+
/**
13+
* @var FactoryInterface
14+
*/
15+
protected FactoryInterface $factory;
16+
17+
/**
18+
* @var array{first: int, limit: int}|null
19+
*/
20+
protected ?array $pagination;
21+
22+
public function __construct(protected ?FilterInterface $filter = null, FactoryInterface $factory = null)
23+
{
24+
$this->factory = $factory ?? new Factory();
25+
}
26+
27+
/**
28+
* @inheritDoc
29+
*/
30+
public function getFilter(): FilterInterface
31+
{
32+
return $this->filter;
33+
}
34+
35+
/**
36+
* @inheritDoc
37+
*/
38+
abstract public function getSearchArguments(): SearchArguments;
39+
40+
/**
41+
* @inheritDoc
42+
*/
43+
abstract public function getQueryString(): string;
44+
45+
/**
46+
* @inheritDoc
47+
*/
48+
public function setPagination(int $first, int $limit): void
49+
{
50+
$this->pagination['first'] = $first;
51+
$this->pagination['limit'] = $limit;
52+
}
53+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Vladvildanov\PredisVl\Query\Filter;
4+
5+
use Vladvildanov\PredisVl\Enum\Condition;
6+
7+
interface FilterInterface
8+
{
9+
/**
10+
* Creates field-based filter.
11+
*
12+
* @param string $fieldName
13+
* @param Condition $condition
14+
* @param mixed $value
15+
*/
16+
public function __construct(string $fieldName, Condition $condition, mixed $value);
17+
18+
/**
19+
* Returns expression-string representation of current filter.
20+
*
21+
* @return string
22+
*/
23+
public function toExpression(): string;
24+
}

src/Query/Filter/TagFilter.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Vladvildanov\PredisVl\Query\Filter;
4+
5+
use JetBrains\PhpStorm\ArrayShape;
6+
use Vladvildanov\PredisVl\Enum\Condition;
7+
use Vladvildanov\PredisVl\Enum\Logical;
8+
9+
class TagFilter implements FilterInterface
10+
{
11+
/**
12+
* Mappings according to Redis Query Syntax
13+
*
14+
* @link https://redis.io/docs/interact/search-and-query/advanced-concepts/query_syntax/
15+
* @var array
16+
*/
17+
private array $conditionMappings = [
18+
'==' => '',
19+
'!=' => '-'
20+
];
21+
22+
/**
23+
* Creates tag filter based on condition.
24+
* Value can be provided as a string (single tag) or as an array (multiple tags).
25+
*
26+
* @param string $fieldName
27+
* @param Condition $condition
28+
* @param string|array $value
29+
*/
30+
public function __construct(
31+
private readonly string $fieldName,
32+
private readonly Condition $condition,
33+
#[ArrayShape([
34+
'conjunction' => Logical::class,
35+
'tags' => 'array',
36+
])] private $value
37+
) {
38+
}
39+
40+
/**
41+
* @inheritDoc
42+
*/
43+
public function toExpression(): string
44+
{
45+
$condition = $this->conditionMappings[$this->condition->value];
46+
47+
if (is_string($this->value)) {
48+
return "{$condition}@{$this->fieldName}:{{$this->value}}";
49+
}
50+
51+
if ($this->value['conjunction'] === Logical::or) {
52+
$query = "{$condition}@{$this->fieldName}:{";
53+
54+
foreach ($this->value['tags'] as $tag) {
55+
$query .= $tag . " | ";
56+
}
57+
58+
return trim($query, '| ') . '}';
59+
}
60+
61+
$query = '';
62+
63+
foreach ($this->value['tags'] as $tag) {
64+
$query .= "{$condition}@{$this->fieldName}:{{$tag}} ";
65+
}
66+
67+
return trim($query);
68+
}
69+
}

src/Query/QueryInterface.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Vladvildanov\PredisVl\Query;
4+
5+
use Predis\Command\Argument\Search\SearchArguments;
6+
use Vladvildanov\PredisVl\Query\Filter\FilterInterface;
7+
8+
interface QueryInterface
9+
{
10+
/**
11+
* Returns filter for current query.
12+
*
13+
* @return FilterInterface|null
14+
*/
15+
public function getFilter(): ?FilterInterface;
16+
17+
/**
18+
* Returns Predis search builder object.
19+
*
20+
* @return SearchArguments
21+
*/
22+
public function getSearchArguments(): SearchArguments;
23+
24+
/**
25+
* Returns generated query string.
26+
*
27+
* @return string
28+
*/
29+
public function getQueryString(): string;
30+
31+
/**
32+
* Set the paging parameters for the query to limit the results between first and num_results.
33+
*
34+
* @param int $first
35+
* @param int $limit
36+
* @return void
37+
*/
38+
public function setPagination(int $first, int $limit): void;
39+
}

0 commit comments

Comments
 (0)