Skip to content

Commit e74499a

Browse files
WIP - Metadata filter
1 parent 8aad8c9 commit e74499a

File tree

4 files changed

+979
-0
lines changed

4 files changed

+979
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPVector\Metadata;
6+
7+
use PHPVector\Document;
8+
use PHPVector\MetadataFilter;
9+
10+
/**
11+
* Evaluates whether a document's metadata matches a set of filters.
12+
*
13+
* Filter logic:
14+
* - Top-level filters are ANDed together
15+
* - Nested arrays (array of MetadataFilter) are ORed within the group
16+
* - Example: [[$f1, $f2], $f3] = (f1 OR f2) AND f3
17+
*/
18+
final class MetadataFilterEvaluator
19+
{
20+
/**
21+
* Check if a document's metadata matches all filters.
22+
*
23+
* @param Document $document The document to check
24+
* @param array<MetadataFilter|array<MetadataFilter>> $filters Filters to apply
25+
* @return bool True if document matches all filters
26+
*/
27+
public function matches(Document $document, array $filters): bool
28+
{
29+
foreach ($filters as $filter) {
30+
if (is_array($filter)) {
31+
// OR group: at least one must match
32+
if (!$this->matchesOrGroup($document, $filter)) {
33+
return false;
34+
}
35+
} elseif (!$this->matchesSingleFilter($document, $filter)) {
36+
// Single filter
37+
return false;
38+
}
39+
}
40+
41+
return true;
42+
}
43+
44+
/**
45+
* Check if document matches at least one filter in an OR group.
46+
*
47+
* @param Document $document The document to check
48+
* @param array<MetadataFilter> $filters OR group of filters
49+
* @return bool True if at least one filter matches
50+
*/
51+
private function matchesOrGroup(Document $document, array $filters): bool
52+
{
53+
foreach ($filters as $filter) {
54+
if ($this->matchesSingleFilter($document, $filter)) {
55+
return true;
56+
}
57+
}
58+
59+
return false;
60+
}
61+
62+
/**
63+
* Check if document matches a single filter.
64+
*
65+
* @param Document $document The document to check
66+
* @param MetadataFilter $filter The filter to apply
67+
* @return bool True if a document matches the filter
68+
*/
69+
private function matchesSingleFilter(Document $document, MetadataFilter $filter): bool
70+
{
71+
$metadata = $document->metadata;
72+
73+
// Missing metadata key returns false
74+
if (!array_key_exists($filter->key, $metadata)) {
75+
return false;
76+
}
77+
78+
$metadataValue = $metadata[$filter->key];
79+
$filterValue = $filter->value;
80+
81+
return match ($filter->operator) {
82+
'=' => $metadataValue === $filterValue,
83+
'!=' => $metadataValue !== $filterValue,
84+
'<' => $metadataValue < $filterValue,
85+
'<=' => $metadataValue <= $filterValue,
86+
'>' => $metadataValue > $filterValue,
87+
'>=' => $metadataValue >= $filterValue,
88+
'in' => is_array($filterValue) && in_array($metadataValue, $filterValue, true),
89+
'not_in' => is_array($filterValue) && !in_array($metadataValue, $filterValue, true),
90+
'contains' => $this->evaluateContains($metadataValue, $filterValue),
91+
default => false,
92+
};
93+
}
94+
95+
private function evaluateContains(mixed $metadataValue, mixed $filterValue): bool
96+
{
97+
if (!is_array($metadataValue)) {
98+
return false;
99+
}
100+
101+
return in_array($filterValue, $metadataValue, true);
102+
}
103+
}

src/MetadataFilter.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPVector;
6+
7+
use InvalidArgumentException;
8+
9+
final class MetadataFilter
10+
{
11+
private const VALID_OPERATORS = ['=', '!=', '<', '<=', '>', '>=', 'in', 'not_in', 'contains'];
12+
13+
/**
14+
* @param string $key Metadata field name to filter on.
15+
* @param mixed $value Value to compare against.
16+
* @param string $operator Comparison operator.
17+
*/
18+
public function __construct(
19+
public readonly string $key,
20+
public readonly mixed $value,
21+
public readonly string $operator = '=',
22+
) {
23+
if (!in_array($operator, self::VALID_OPERATORS, true)) {
24+
throw new InvalidArgumentException(
25+
sprintf(
26+
'Unknown operator "%s". Valid operators are: %s',
27+
$operator,
28+
implode(', ', self::VALID_OPERATORS)
29+
)
30+
);
31+
}
32+
33+
if (in_array($operator, ['in', 'not_in'], true) && !is_array($value)) {
34+
throw new InvalidArgumentException(
35+
sprintf('Operator "%s" requires an array value.', $operator)
36+
);
37+
}
38+
39+
if ($operator === 'contains' && is_array($value)) {
40+
throw new InvalidArgumentException(
41+
'Operator "contains" requires a single value, not an array.'
42+
);
43+
}
44+
}
45+
46+
public static function eq(string $key, mixed $value): self
47+
{
48+
return new self($key, $value, '=');
49+
}
50+
51+
public static function neq(string $key, mixed $value): self
52+
{
53+
return new self($key, $value, '!=');
54+
}
55+
56+
public static function lt(string $key, mixed $value): self
57+
{
58+
return new self($key, $value, '<');
59+
}
60+
61+
public static function lte(string $key, mixed $value): self
62+
{
63+
return new self($key, $value, '<=');
64+
}
65+
66+
public static function gt(string $key, mixed $value): self
67+
{
68+
return new self($key, $value, '>');
69+
}
70+
71+
public static function gte(string $key, mixed $value): self
72+
{
73+
return new self($key, $value, '>=');
74+
}
75+
76+
public static function in(string $key, array $values): self
77+
{
78+
return new self($key, $values, 'in');
79+
}
80+
81+
public static function notIn(string $key, array $values): self
82+
{
83+
return new self($key, $values, 'not_in');
84+
}
85+
86+
public static function contains(string $key, mixed $value): self
87+
{
88+
return new self($key, $value, 'contains');
89+
}
90+
}

0 commit comments

Comments
 (0)