diff --git a/generator/mql-specifications b/generator/mql-specifications index d23e9a860..e403986ec 160000 --- a/generator/mql-specifications +++ b/generator/mql-specifications @@ -1 +1 @@ -Subproject commit d23e9a8603cb29c5ea90f571bdabcde1d834de4c +Subproject commit e403986ec78395128f728d7234dc517e45408b62 diff --git a/src/Builder/Search/CompoundOperator.php b/src/Builder/Search/CompoundOperator.php index bc72afc4b..368e06cb9 100644 --- a/src/Builder/Search/CompoundOperator.php +++ b/src/Builder/Search/CompoundOperator.php @@ -15,9 +15,13 @@ use MongoDB\Builder\Type\OperatorInterface; use MongoDB\Builder\Type\Optional; use MongoDB\Builder\Type\SearchOperatorInterface; +use MongoDB\Exception\InvalidArgumentException; use MongoDB\Model\BSONArray; use stdClass; +use function array_is_list; +use function is_array; + /** * The compound operator combines two or more operators into a single query. * Each element of a compound query is called a clause, and each clause @@ -40,6 +44,7 @@ final class CompoundOperator implements SearchOperatorInterface, OperatorInterfa 'filter' => 'filter', 'minimumShouldMatch' => 'minimumShouldMatch', 'score' => 'score', + 'doesNotAffect' => 'doesNotAffect', ]; /** @var Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $must */ @@ -60,6 +65,9 @@ final class CompoundOperator implements SearchOperatorInterface, OperatorInterfa /** @var Optional|Document|Serializable|array|stdClass $score */ public readonly Optional|Document|Serializable|stdClass|array $score; + /** @var Optional|BSONArray|PackedArray|array|string $doesNotAffect */ + public readonly Optional|PackedArray|BSONArray|array|string $doesNotAffect; + /** * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $must * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $mustNot @@ -67,6 +75,7 @@ final class CompoundOperator implements SearchOperatorInterface, OperatorInterfa * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $filter * @param Optional|int $minimumShouldMatch * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public function __construct( Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $must = Optional::Undefined, @@ -75,6 +84,7 @@ public function __construct( Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $filter = Optional::Undefined, Optional|int $minimumShouldMatch = Optional::Undefined, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ) { $this->must = $must; $this->mustNot = $mustNot; @@ -82,5 +92,10 @@ public function __construct( $this->filter = $filter; $this->minimumShouldMatch = $minimumShouldMatch; $this->score = $score; + if (is_array($doesNotAffect) && ! array_is_list($doesNotAffect)) { + throw new InvalidArgumentException('Expected $doesNotAffect argument to be a list, got an associative array.'); + } + + $this->doesNotAffect = $doesNotAffect; } } diff --git a/src/Builder/Search/EqualsOperator.php b/src/Builder/Search/EqualsOperator.php index 127b2ef76..196aee686 100644 --- a/src/Builder/Search/EqualsOperator.php +++ b/src/Builder/Search/EqualsOperator.php @@ -14,14 +14,20 @@ use MongoDB\BSON\Document; use MongoDB\BSON\Int64; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\PackedArray; use MongoDB\BSON\Serializable; use MongoDB\BSON\UTCDateTime; use MongoDB\Builder\Type\Encode; use MongoDB\Builder\Type\OperatorInterface; use MongoDB\Builder\Type\Optional; use MongoDB\Builder\Type\SearchOperatorInterface; +use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Model\BSONArray; use stdClass; +use function array_is_list; +use function is_array; + /** * The equals operator checks whether a field matches a value you specify. * @@ -34,7 +40,7 @@ final class EqualsOperator implements SearchOperatorInterface, OperatorInterface { public const ENCODE = Encode::Object; public const NAME = 'equals'; - public const PROPERTIES = ['path' => 'path', 'value' => 'value', 'score' => 'score']; + public const PROPERTIES = ['path' => 'path', 'value' => 'value', 'score' => 'score', 'doesNotAffect' => 'doesNotAffect']; /** @var array|string $path */ public readonly array|string $path; @@ -45,18 +51,28 @@ final class EqualsOperator implements SearchOperatorInterface, OperatorInterface /** @var Optional|Document|Serializable|array|stdClass $score */ public readonly Optional|Document|Serializable|stdClass|array $score; + /** @var Optional|BSONArray|PackedArray|array|string $doesNotAffect */ + public readonly Optional|PackedArray|BSONArray|array|string $doesNotAffect; + /** * @param array|string $path * @param Binary|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public function __construct( array|string $path, DateTimeInterface|Binary|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ) { $this->path = $path; $this->value = $value; $this->score = $score; + if (is_array($doesNotAffect) && ! array_is_list($doesNotAffect)) { + throw new InvalidArgumentException('Expected $doesNotAffect argument to be a list, got an associative array.'); + } + + $this->doesNotAffect = $doesNotAffect; } } diff --git a/src/Builder/Search/FactoryTrait.php b/src/Builder/Search/FactoryTrait.php index fd191b3da..d334fc7c8 100644 --- a/src/Builder/Search/FactoryTrait.php +++ b/src/Builder/Search/FactoryTrait.php @@ -68,6 +68,7 @@ public static function autocomplete( * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $filter * @param Optional|int $minimumShouldMatch * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public static function compound( Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $must = Optional::Undefined, @@ -76,8 +77,9 @@ public static function compound( Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $filter = Optional::Undefined, Optional|int $minimumShouldMatch = Optional::Undefined, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ): CompoundOperator { - return new CompoundOperator($must, $mustNot, $should, $filter, $minimumShouldMatch, $score); + return new CompoundOperator($must, $mustNot, $should, $filter, $minimumShouldMatch, $score, $doesNotAffect); } /** @@ -110,13 +112,15 @@ public static function embeddedDocument( * @param array|string $path * @param Binary|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public static function equals( array|string $path, DateTimeInterface|Binary|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ): EqualsOperator { - return new EqualsOperator($path, $value, $score); + return new EqualsOperator($path, $value, $score, $doesNotAffect); } /** @@ -236,13 +240,15 @@ public static function hasRoot( * @param array|string $path * @param BSONArray|DateTimeInterface|PackedArray|Type|array|bool|float|int|null|stdClass|string $value * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public static function in( array|string $path, DateTimeInterface|PackedArray|Type|BSONArray|stdClass|array|bool|float|int|null|string $value, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ): InOperator { - return new InOperator($path, $value, $score); + return new InOperator($path, $value, $score, $doesNotAffect); } /** @@ -330,6 +336,7 @@ public static function queryString(array|string $defaultPath, string $query): Qu * @param Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt * @param Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public static function range( array|string $path, @@ -338,8 +345,9 @@ public static function range( Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt = Optional::Undefined, Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte = Optional::Undefined, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ): RangeOperator { - return new RangeOperator($path, $gt, $gte, $lt, $lte, $score); + return new RangeOperator($path, $gt, $gte, $lt, $lte, $score, $doesNotAffect); } /** diff --git a/src/Builder/Search/InOperator.php b/src/Builder/Search/InOperator.php index 1bc2cf8da..eb5bfa7dd 100644 --- a/src/Builder/Search/InOperator.php +++ b/src/Builder/Search/InOperator.php @@ -36,7 +36,7 @@ final class InOperator implements SearchOperatorInterface, OperatorInterface { public const ENCODE = Encode::Object; public const NAME = 'in'; - public const PROPERTIES = ['path' => 'path', 'value' => 'value', 'score' => 'score']; + public const PROPERTIES = ['path' => 'path', 'value' => 'value', 'score' => 'score', 'doesNotAffect' => 'doesNotAffect']; /** @var array|string $path */ public readonly array|string $path; @@ -47,15 +47,20 @@ final class InOperator implements SearchOperatorInterface, OperatorInterface /** @var Optional|Document|Serializable|array|stdClass $score */ public readonly Optional|Document|Serializable|stdClass|array $score; + /** @var Optional|BSONArray|PackedArray|array|string $doesNotAffect */ + public readonly Optional|PackedArray|BSONArray|array|string $doesNotAffect; + /** * @param array|string $path * @param BSONArray|DateTimeInterface|PackedArray|Type|array|bool|float|int|null|stdClass|string $value * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public function __construct( array|string $path, DateTimeInterface|PackedArray|Type|BSONArray|stdClass|array|bool|float|int|null|string $value, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ) { $this->path = $path; if (is_array($value) && ! array_is_list($value)) { @@ -64,5 +69,10 @@ public function __construct( $this->value = $value; $this->score = $score; + if (is_array($doesNotAffect) && ! array_is_list($doesNotAffect)) { + throw new InvalidArgumentException('Expected $doesNotAffect argument to be a list, got an associative array.'); + } + + $this->doesNotAffect = $doesNotAffect; } } diff --git a/src/Builder/Search/RangeOperator.php b/src/Builder/Search/RangeOperator.php index 56b2635a5..64ed4839d 100644 --- a/src/Builder/Search/RangeOperator.php +++ b/src/Builder/Search/RangeOperator.php @@ -13,14 +13,20 @@ use MongoDB\BSON\Document; use MongoDB\BSON\Int64; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\PackedArray; use MongoDB\BSON\Serializable; use MongoDB\BSON\UTCDateTime; use MongoDB\Builder\Type\Encode; use MongoDB\Builder\Type\OperatorInterface; use MongoDB\Builder\Type\Optional; use MongoDB\Builder\Type\SearchOperatorInterface; +use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Model\BSONArray; use stdClass; +use function array_is_list; +use function is_array; + /** * The range operator supports querying and scoring numeric, date, and string values. * You can use this operator to find results that are within a given numeric, date, objectId, or letter (from the English alphabet) range. @@ -42,6 +48,7 @@ final class RangeOperator implements SearchOperatorInterface, OperatorInterface 'lt' => 'lt', 'lte' => 'lte', 'score' => 'score', + 'doesNotAffect' => 'doesNotAffect', ]; /** @var array|string $path */ @@ -62,6 +69,9 @@ final class RangeOperator implements SearchOperatorInterface, OperatorInterface /** @var Optional|Document|Serializable|array|stdClass $score */ public readonly Optional|Document|Serializable|stdClass|array $score; + /** @var Optional|BSONArray|PackedArray|array|string $doesNotAffect */ + public readonly Optional|PackedArray|BSONArray|array|string $doesNotAffect; + /** * @param array|string $path * @param Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gt @@ -69,6 +79,7 @@ final class RangeOperator implements SearchOperatorInterface, OperatorInterface * @param Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt * @param Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte * @param Optional|Document|Serializable|array|stdClass $score + * @param Optional|BSONArray|PackedArray|array|string $doesNotAffect */ public function __construct( array|string $path, @@ -77,6 +88,7 @@ public function __construct( Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt = Optional::Undefined, Optional|DateTimeInterface|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte = Optional::Undefined, Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + Optional|PackedArray|BSONArray|array|string $doesNotAffect = Optional::Undefined, ) { $this->path = $path; $this->gt = $gt; @@ -84,5 +96,10 @@ public function __construct( $this->lt = $lt; $this->lte = $lte; $this->score = $score; + if (is_array($doesNotAffect) && ! array_is_list($doesNotAffect)) { + throw new InvalidArgumentException('Expected $doesNotAffect argument to be a list, got an associative array.'); + } + + $this->doesNotAffect = $doesNotAffect; } } diff --git a/tests/Builder/Search/FacetOperatorTest.php b/tests/Builder/Search/FacetOperatorTest.php index d52f71e30..1752e1be5 100644 --- a/tests/Builder/Search/FacetOperatorTest.php +++ b/tests/Builder/Search/FacetOperatorTest.php @@ -59,4 +59,88 @@ public function testFacet(): void $this->assertSamePipeline(Pipelines::FacetFacet, $pipeline); } + + public function testInterFacetFilterExclusionExample(): void + { + $pipeline = new Pipeline( + Stage::searchMeta( + Search::facet( + facets: object( + accommodatesFacet: object( + path: 'accommodates', + type: 'number', + boundaries: [1, 2, 4, 8], + ), + cancellationFacet: object( + path: 'cancellation_policy', + type: 'string', + ), + roomTypeFacet: object( + path: 'room_type', + type: 'string', + ), + ), + operator: Search::compound( + must: [ + Search::text( + path: 'description', + query: 'new york city', + ), + ], + filter: [ + Search::equals( + path: 'cancellation_policy', + value: 'moderate', + doesNotAffect: 'accommodatesFacet', + ), + ], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FacetInterFacetFilterExclusionExample, $pipeline); + } + + public function testMultiSelectFacetingExample(): void + { + $pipeline = new Pipeline( + Stage::searchMeta( + Search::facet( + facets: object( + accommodatesFacet: object( + path: 'accommodates', + type: 'number', + boundaries: [1, 2, 4, 8], + ), + cancellationFacet: object( + path: 'cancellation_policy', + type: 'string', + ), + roomTypeFacet: object( + path: 'room_type', + type: 'string', + ), + ), + operator: Search::compound( + must: [ + Search::text( + path: 'description', + query: 'new york city', + ), + ], + filter: [ + Search::equals( + path: 'cancellation_policy', + value: 'moderate', + doesNotAffect: 'cancellationFacet', + ), + ], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FacetMultiSelectFacetingExample, $pipeline); + } } diff --git a/tests/Builder/Search/Pipelines.php b/tests/Builder/Search/Pipelines.php index 8fa7e7f69..ddd59cbf9 100644 --- a/tests/Builder/Search/Pipelines.php +++ b/tests/Builder/Search/Pipelines.php @@ -989,6 +989,136 @@ enum Pipelines: string ] JSON; + /** + * Multi-Select Faceting Example + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/facet/#multi-select-faceting-example + */ + case FacetMultiSelectFacetingExample = <<<'JSON' + [ + { + "$searchMeta": { + "facet": { + "operator": { + "compound": { + "must": [ + { + "text": { + "path": "description", + "query": "new york city" + } + } + ], + "filter": [ + { + "equals": { + "path": "cancellation_policy", + "value": "moderate", + "doesNotAffect": "cancellationFacet" + } + } + ] + } + }, + "facets": { + "accommodatesFacet": { + "path": "accommodates", + "type": "number", + "boundaries": [ + { + "$numberInt": "1" + }, + { + "$numberInt": "2" + }, + { + "$numberInt": "4" + }, + { + "$numberInt": "8" + } + ] + }, + "cancellationFacet": { + "path": "cancellation_policy", + "type": "string" + }, + "roomTypeFacet": { + "path": "room_type", + "type": "string" + } + } + } + } + } + ] + JSON; + + /** + * Inter-Facet Filter Exclusion Example + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/facet/#inter-facet-filter-exclusion-example + */ + case FacetInterFacetFilterExclusionExample = <<<'JSON' + [ + { + "$searchMeta": { + "facet": { + "operator": { + "compound": { + "must": [ + { + "text": { + "path": "description", + "query": "new york city" + } + } + ], + "filter": [ + { + "equals": { + "path": "cancellation_policy", + "value": "moderate", + "doesNotAffect": "accommodatesFacet" + } + } + ] + } + }, + "facets": { + "accommodatesFacet": { + "path": "accommodates", + "type": "number", + "boundaries": [ + { + "$numberInt": "1" + }, + { + "$numberInt": "2" + }, + { + "$numberInt": "4" + }, + { + "$numberInt": "8" + } + ] + }, + "cancellationFacet": { + "path": "cancellation_policy", + "type": "string" + }, + "roomTypeFacet": { + "path": "room_type", + "type": "string" + } + } + } + } + } + ] + JSON; + /** * Disjoint *