diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php index d5557079bf..444af0dd50 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php @@ -570,7 +570,7 @@ public function sample(int $size): Stage\Sample */ public function search(): Stage\Search { - $stage = new Stage\Search($this); + $stage = new Stage\Search($this, $this->getDocumentPersister()); return $this->addStage($stage); } @@ -660,7 +660,7 @@ public function sortByCount(string $expression): Stage\SortByCount */ public function vectorSearch(): Stage\VectorSearch { - $stage = new Stage\VectorSearch($this); + $stage = new Stage\VectorSearch($this, $this->getDocumentPersister()); return $this->addStage($stage); } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php index dc38503739..b00071900a 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php @@ -9,6 +9,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SearchOperator; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperators; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperatorsTrait; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use function in_array; use function is_array; @@ -70,7 +71,7 @@ class Search extends Stage implements SupportsAllSearchOperators /** @var array */ private array $sort = []; - public function __construct(Builder $builder) + public function __construct(Builder $builder, private DocumentPersister $persister) { parent::__construct($builder); } @@ -105,7 +106,7 @@ public function getExpression(): array } if ($this->sort) { - $params->sort = (object) $this->sort; + $params->sort = (object) $this->persister->prepareSort($this->sort); } if ($this->operator !== null) { @@ -214,4 +215,9 @@ protected function getSearchStage(): static { return $this; } + + protected function getDocumentPersister(): DocumentPersister + { + return $this->persister; + } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php index c7dede7c4c..ffaa9dcfd3 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php @@ -7,6 +7,10 @@ use Doctrine\ODM\MongoDB\Aggregation\Stage; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; + +use function array_map; +use function is_array; /** * @internal @@ -18,7 +22,7 @@ */ abstract class AbstractSearchOperator extends Stage implements SearchOperator { - public function __construct(private Search $search) + public function __construct(private Search $search, private DocumentPersister $persister) { parent::__construct($search->builder); } @@ -64,4 +68,35 @@ protected function getSearchStage(): Search { return $this->search; } + + /** + * @param T $field + * + * @return T + * + * @template T of string|string[] + */ + protected function prepareFieldPath(string|array $field): string|array + { + if (is_array($field)) { + return array_map($this->persister->prepareFieldName(...), $field); + } + + return $this->persister->prepareFieldName($field); + } + + /** + * @param list|object> $documents + * + * @return list|object> + */ + protected function prepareDocuments(array $documents): array + { + return array_map($this->persister->prepareQueryOrNewObj(...), $documents); + } + + protected function getDocumentPersister(): DocumentPersister + { + return $this->persister; + } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php index bb0d8fcace..95f367e62b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use function array_values; @@ -23,9 +24,9 @@ class Autocomplete extends AbstractSearchOperator implements ScoredSearchOperato private string $tokenOrder = ''; private ?object $fuzzy = null; - public function __construct(Search $search, string $path, string ...$query) + public function __construct(Search $search, DocumentPersister $persister, string $path, string ...$query) { - parent::__construct($search); + parent::__construct($search, $persister); $this->query(...$query); $this->path($path); @@ -79,7 +80,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->tokenOrder) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php index fe8d5a11b1..cec0677a93 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/EmbeddedDocument.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; /** * @internal @@ -19,9 +20,9 @@ class EmbeddedDocument extends AbstractSearchOperator implements SupportsEmbedda private string $path; private ?SearchOperator $operator = null; - public function __construct(Search $search, string $path) + public function __construct(Search $search, DocumentPersister $persister, string $path) { - parent::__construct($search); + parent::__construct($search, $persister); $this->path($path); } @@ -52,7 +53,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { - $params = (object) ['path' => $this->path]; + $params = (object) ['path' => $this->prepareFieldPath($this->path)]; if ($this->operator) { $params->operator = (object) $this->operator->getExpression(); diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php index fbaf401ed3..51627535e4 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Equals.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use MongoDB\BSON\ObjectId; use MongoDB\BSON\UTCDateTime; @@ -22,9 +23,9 @@ class Equals extends AbstractSearchOperator implements ScoredSearchOperator private mixed $value; /** @param string|int|float|ObjectId|UTCDateTime|null $value */ - public function __construct(Search $search, string $path = '', $value = null) + public function __construct(Search $search, DocumentPersister $persister, string $path = '', $value = null) { - parent::__construct($search); + parent::__construct($search, $persister); $this ->path($path) @@ -54,7 +55,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { $params = (object) [ - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), 'value' => $this->value, ]; diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php index 8ad546384c..1f2577798c 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Exists.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; /** * @internal @@ -13,9 +14,9 @@ */ class Exists extends AbstractSearchOperator { - public function __construct(Search $search, private string $path = '') + public function __construct(Search $search, DocumentPersister $persister, private string $path = '') { - parent::__construct($search); + parent::__construct($search, $persister); } public function getOperatorName(): string @@ -25,6 +26,6 @@ public function getOperatorName(): string public function getOperatorParams(): object { - return (object) ['path' => $this->path]; + return (object) ['path' => $this->prepareFieldPath($this->path)]; } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php index c7ab3e4014..9cb35c500b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoShape.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\Geometry; use GeoJson\Geometry\LineString; use GeoJson\Geometry\MultiPolygon; @@ -27,9 +28,9 @@ class GeoShape extends AbstractSearchOperator implements ScoredSearchOperator private LineString|Point|Polygon|MultiPolygon|array|null $geometry = null; /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ - public function __construct(Search $search, $geometry = null, string $relation = '', string ...$path) + public function __construct(Search $search, DocumentPersister $persister, $geometry = null, string $relation = '', string ...$path) { - parent::__construct($search); + parent::__construct($search, $persister); $this ->geometry($geometry) @@ -67,7 +68,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { $params = (object) [ - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), 'relation' => $this->relation, 'geometry' => $this->geometry instanceof Geometry ? $this->geometry->jsonSerialize() diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php index c72b283e47..635d14882b 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/GeoWithin.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\Geometry; use GeoJson\Geometry\MultiPolygon; use GeoJson\Geometry\Point; @@ -27,9 +28,9 @@ class GeoWithin extends AbstractSearchOperator implements ScoredSearchOperator private array|MultiPolygon|Polygon|null $geometry = null; - public function __construct(Search $search, string ...$path) + public function __construct(Search $search, DocumentPersister $persister, string ...$path) { - parent::__construct($search); + parent::__construct($search, $persister); $this->path(...$path); } @@ -84,7 +85,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { - $params = (object) ['path' => $this->path]; + $params = (object) ['path' => $this->prepareFieldPath($this->path)]; if ($this->box) { $params->box = $this->box; diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php index 198a7f3413..170c791c2e 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/MoreLikeThis.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use function array_values; @@ -19,9 +20,9 @@ class MoreLikeThis extends AbstractSearchOperator private array $like = []; /** @param array|object $documents */ - public function __construct(Search $search, ...$documents) + public function __construct(Search $search, DocumentPersister $persister, ...$documents) { - parent::__construct($search); + parent::__construct($search, $persister); $this->like = array_values($documents); } @@ -33,6 +34,8 @@ public function getOperatorName(): string public function getOperatorParams(): object { - return (object) ['like' => $this->like]; + return (object) [ + 'like' => $this->prepareDocuments($this->like), + ]; } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php index f244259c94..6a1bab69d8 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Near.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\Geometry; use GeoJson\Geometry\Point; use MongoDB\BSON\UTCDateTime; @@ -29,9 +30,9 @@ class Near extends AbstractSearchOperator implements ScoredSearchOperator * @param int|float|UTCDateTime|array|Point|null $origin * @param int|float|null $pivot */ - public function __construct(Search $search, $origin = null, $pivot = null, string ...$path) + public function __construct(Search $search, DocumentPersister $persister, $origin = null, $pivot = null, string ...$path) { - parent::__construct($search); + parent::__construct($search, $persister); $this ->origin($origin) @@ -74,7 +75,7 @@ public function getOperatorParams(): object ? $this->origin->jsonSerialize() : $this->origin, 'pivot' => $this->pivot, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; return $this->appendScore($params); diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php index e397003bc0..e5e590a088 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Phrase.php @@ -52,7 +52,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->slop !== null) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php index 6b00ce5ed7..ea1e8cb61c 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/QueryString.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; /** * @internal @@ -18,9 +19,9 @@ class QueryString extends AbstractSearchOperator implements ScoredSearchOperator private string $query; private string $defaultPath; - public function __construct(Search $search, string $query = '', string $defaultPath = '') + public function __construct(Search $search, DocumentPersister $persister, string $query = '', string $defaultPath = '') { - parent::__construct($search); + parent::__construct($search, $persister); $this ->query($query) @@ -50,7 +51,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'defaultPath' => $this->defaultPath, + 'defaultPath' => $this->prepareFieldPath($this->defaultPath), ]; return $this->appendScore($params); diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php index af3fc0cdd9..6253bce2aa 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Range.php @@ -73,7 +73,7 @@ public function getOperatorName(): string public function getOperatorParams(): object { - $params = (object) ['path' => $this->path]; + $params = (object) ['path' => $this->prepareFieldPath($this->path)]; if ($this->gt !== null) { $name = $this->includeLowerBound ? 'gte' : 'gt'; diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php index 90fd1f8d8d..f547eac7ee 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Regex.php @@ -50,7 +50,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->allowAnalyzedField !== null) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php index ad8591d0e5..c408999d98 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsAllSearchOperatorsTrait.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\LineString; use GeoJson\Geometry\MultiPolygon; use GeoJson\Geometry\Point; @@ -15,6 +16,8 @@ /** @internal */ trait SupportsAllSearchOperatorsTrait { + abstract protected function getDocumentPersister(): DocumentPersister; + abstract protected function getSearchStage(): Search; /** @@ -28,45 +31,45 @@ abstract protected function addOperator(SearchOperator $operator): SearchOperato public function autocomplete(string $path = '', string ...$query): Autocomplete { - return $this->addOperator(new Autocomplete($this->getSearchStage(), $path, ...$query)); + return $this->addOperator(new Autocomplete($this->getSearchStage(), $this->getDocumentPersister(), $path, ...$query)); } public function compound(): Compound { - return $this->addOperator(new Compound($this->getSearchStage())); + return $this->addOperator(new Compound($this->getSearchStage(), $this->getDocumentPersister())); } public function embeddedDocument(string $path = ''): EmbeddedDocument { - return $this->addOperator(new EmbeddedDocument($this->getSearchStage(), $path)); + return $this->addOperator(new EmbeddedDocument($this->getSearchStage(), $this->getDocumentPersister(), $path)); } /** @param string|int|float|ObjectId|UTCDateTime|null $value */ public function equals(string $path = '', $value = null): Equals { - return $this->addOperator(new Equals($this->getSearchStage(), $path, $value)); + return $this->addOperator(new Equals($this->getSearchStage(), $this->getDocumentPersister(), $path, $value)); } public function exists(string $path): Exists { - return $this->addOperator(new Exists($this->getSearchStage(), $path)); + return $this->addOperator(new Exists($this->getSearchStage(), $this->getDocumentPersister(), $path)); } /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ public function geoShape($geometry = null, string $relation = '', string ...$path): GeoShape { - return $this->addOperator(new GeoShape($this->getSearchStage(), $geometry, $relation, ...$path)); + return $this->addOperator(new GeoShape($this->getSearchStage(), $this->getDocumentPersister(), $geometry, $relation, ...$path)); } public function geoWithin(string ...$path): GeoWithin { - return $this->addOperator(new GeoWithin($this->getSearchStage(), ...$path)); + return $this->addOperator(new GeoWithin($this->getSearchStage(), $this->getDocumentPersister(), ...$path)); } /** @param array|object $documents */ public function moreLikeThis(...$documents): MoreLikeThis { - return $this->addOperator(new MoreLikeThis($this->getSearchStage(), ...$documents)); + return $this->addOperator(new MoreLikeThis($this->getSearchStage(), $this->getDocumentPersister(), ...$documents)); } /** @@ -75,36 +78,36 @@ public function moreLikeThis(...$documents): MoreLikeThis */ public function near($origin = null, $pivot = null, string ...$path): Near { - return $this->addOperator(new Near($this->getSearchStage(), $origin, $pivot, ...$path)); + return $this->addOperator(new Near($this->getSearchStage(), $this->getDocumentPersister(), $origin, $pivot, ...$path)); } public function phrase(): Phrase { - return $this->addOperator(new Phrase($this->getSearchStage())); + return $this->addOperator(new Phrase($this->getSearchStage(), $this->getDocumentPersister())); } public function queryString(string $query = '', string $defaultPath = ''): QueryString { - return $this->addOperator(new QueryString($this->getSearchStage(), $query, $defaultPath)); + return $this->addOperator(new QueryString($this->getSearchStage(), $this->getDocumentPersister(), $query, $defaultPath)); } public function range(): Range { - return $this->addOperator(new Range($this->getSearchStage())); + return $this->addOperator(new Range($this->getSearchStage(), $this->getDocumentPersister())); } public function regex(): Regex { - return $this->addOperator(new Regex($this->getSearchStage())); + return $this->addOperator(new Regex($this->getSearchStage(), $this->getDocumentPersister())); } public function text(): Text { - return $this->addOperator(new Text($this->getSearchStage())); + return $this->addOperator(new Text($this->getSearchStage(), $this->getDocumentPersister())); } public function wildcard(): Wildcard { - return $this->addOperator(new Wildcard($this->getSearchStage())); + return $this->addOperator(new Wildcard($this->getSearchStage(), $this->getDocumentPersister())); } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php index 51dee61010..46bfd6efa7 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsCompoundableOperatorsTrait.php @@ -20,6 +20,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\Compound\CompoundedRegex; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\Compound\CompoundedText; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\Compound\CompoundedWildcard; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use GeoJson\Geometry\LineString; use GeoJson\Geometry\MultiPolygon; use GeoJson\Geometry\Point; @@ -30,6 +31,8 @@ /** @internal */ trait SupportsCompoundableOperatorsTrait { + abstract protected function getDocumentPersister(): DocumentPersister; + abstract protected function getSearchStage(): Search; abstract protected function getCompoundStage(): Compound; @@ -47,40 +50,40 @@ abstract protected function addOperator(SearchOperator $operator): SearchOperato public function autocomplete(string $path = '', string ...$query): Autocomplete&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedAutocomplete($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path, ...$query)); + return $this->addOperator(new CompoundedAutocomplete($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path, ...$query)); } public function embeddedDocument(string $path = ''): EmbeddedDocument&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedEmbeddedDocument($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path)); + return $this->addOperator(new CompoundedEmbeddedDocument($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path)); } /** @param string|int|float|ObjectId|UTCDateTime|null $value */ public function equals(string $path = '', $value = null): Equals&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedEquals($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path, $value)); + return $this->addOperator(new CompoundedEquals($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path, $value)); } public function exists(string $path): Exists&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedExists($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $path)); + return $this->addOperator(new CompoundedExists($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $path)); } /** @param LineString|Point|Polygon|MultiPolygon|array|null $geometry */ public function geoShape($geometry = null, string $relation = '', string ...$path): GeoShape&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedGeoShape($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $geometry, $relation, ...$path)); + return $this->addOperator(new CompoundedGeoShape($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $geometry, $relation, ...$path)); } public function geoWithin(string ...$path): GeoWithin&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedGeoWithin($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), ...$path)); + return $this->addOperator(new CompoundedGeoWithin($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), ...$path)); } /** @param array|object $documents */ public function moreLikeThis(...$documents): MoreLikeThis&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedMoreLikeThis($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), ...$documents)); + return $this->addOperator(new CompoundedMoreLikeThis($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), ...$documents)); } /** @@ -89,36 +92,36 @@ public function moreLikeThis(...$documents): MoreLikeThis&CompoundSearchOperator */ public function near($origin = null, $pivot = null, string ...$path): Near&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedNear($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $origin, $pivot, ...$path)); + return $this->addOperator(new CompoundedNear($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $origin, $pivot, ...$path)); } public function phrase(): Phrase&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedPhrase($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedPhrase($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } public function queryString(string $query = '', string $defaultPath = ''): QueryString&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedQueryString($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $query, $defaultPath)); + return $this->addOperator(new CompoundedQueryString($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister(), $query, $defaultPath)); } public function range(): Range&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedRange($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedRange($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } public function regex(): Regex&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedRegex($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedRegex($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } public function text(): Text&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedText($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedText($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } public function wildcard(): Wildcard&CompoundSearchOperatorInterface { - return $this->addOperator(new CompoundedWildcard($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage())); + return $this->addOperator(new CompoundedWildcard($this->getCompoundStage(), $this->getAddOperatorClosure(), $this->getSearchStage(), $this->getDocumentPersister())); } } diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php index b0b5d2b1c3..d645edcc21 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Text.php @@ -71,7 +71,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->fuzzy) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php index 43257ff355..6e067ec407 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Wildcard.php @@ -50,7 +50,7 @@ public function getOperatorParams(): object { $params = (object) [ 'query' => $this->query, - 'path' => $this->path, + 'path' => $this->prepareFieldPath($this->path), ]; if ($this->allowAnalyzedField !== null) { diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php index 257b1be931..06acf3ecb2 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Stage/VectorSearch.php @@ -6,6 +6,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; use Doctrine\ODM\MongoDB\Query\Expr; use MongoDB\BSON\Binary; use MongoDB\BSON\Decimal128; @@ -36,7 +37,7 @@ class VectorSearch extends Stage /** @phpstan-var Vector|null */ private array|Binary|null $queryVector = null; - public function __construct(Builder $builder) + public function __construct(Builder $builder, private DocumentPersister $persister) { parent::__construct($builder); } @@ -66,7 +67,7 @@ public function getExpression(): array } if ($this->path !== null) { - $params['path'] = $this->path; + $params['path'] = $this->persister->prepareFieldName($this->path); } if ($this->queryVector !== null) { diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php index 9d91681f19..ac75de1029 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php @@ -254,15 +254,16 @@ * @phpstan-type SearchIndexDefinition array{ * mappings: array{ * dynamic?: bool, - * fields?: array, + * fields?: array>, * }, * analyzer?: string, * searchAnalyzer?: string, - * analyzers?: array, + * analyzers?: list>, * storedSource?: SearchIndexStoredSource, * synonyms?: list, * } * @phpstan-type SearchIndexMapping array{ + * type: 'search'|'vectorSearch', * name: string, * definition: SearchIndexDefinition * } diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index 6148832d82..2d81620021 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -71,7 +71,7 @@ * * @internal * - * @template T of object + * @template T of object = object * * @phpstan-type CommitOptions array{ * fsync?: bool, diff --git a/lib/Doctrine/ODM/MongoDB/SchemaManager.php b/lib/Doctrine/ODM/MongoDB/SchemaManager.php index d7dea185db..6bc66cfe23 100644 --- a/lib/Doctrine/ODM/MongoDB/SchemaManager.php +++ b/lib/Doctrine/ODM/MongoDB/SchemaManager.php @@ -39,6 +39,7 @@ /** * @phpstan-import-type IndexMapping from ClassMetadata * @phpstan-import-type IndexOptions from ClassMetadata + * @phpstan-import-type SearchIndexMapping from ClassMetadata */ final class SchemaManager { @@ -355,7 +356,7 @@ public function createDocumentSearchIndexes(string $documentName): void throw new InvalidArgumentException('Cannot create search indexes for mapped super classes, embedded documents, query result documents, or views.'); } - $searchIndexes = $class->getSearchIndexes(); + $searchIndexes = $this->prepareSearchIndexes($class); if (empty($searchIndexes)) { return; @@ -367,7 +368,7 @@ public function createDocumentSearchIndexes(string $documentName): void /* createSearchIndexes builds indexes asynchronously but still reports * the names of created indexes. Report an error if any defined names - * were not actually created. */ + * were not created. */ $unprocessedNames = array_diff($definedNames, $createdNames); if (! empty($unprocessedNames)) { @@ -410,7 +411,7 @@ public function updateDocumentSearchIndexes(string $documentName): void throw new InvalidArgumentException('Cannot update search indexes for mapped super classes, embedded documents, query result documents, or views.'); } - $searchIndexes = $class->getSearchIndexes(); + $searchIndexes = $this->prepareSearchIndexes($class); $collection = $this->dm->getDocumentCollection($class->name); $definedNames = array_column($searchIndexes, 'name'); @@ -486,6 +487,59 @@ public function deleteDocumentSearchIndexes(string $documentName): void } } + /** + * @param ClassMetadata $class + * + * @phpstan-return list + */ + private function prepareSearchIndexes(ClassMetadata $class): array + { + $persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name); + $indexes = $class->getSearchIndexes(); + $newIndexes = []; + + foreach ($indexes as $index) { + $definition = $index['definition']; + if (is_array($definition['fields'] ?? null)) { + // Vector Search Index, field names in 'path' parameter + $fields = []; + foreach ($definition['fields'] as $field) { + $key = $persister->prepareFieldName($field['path']); + if ($class->hasField($key)) { + $field['path'] = $class->getFieldMapping($key)['name']; + } else { + $field['path'] = $key; + } + + $fields[] = $field; + } + + $definition['fields'] = $fields; + } elseif (is_array($definition['mappings']['fields'] ?? null)) { + // Search Index with fields mappings, field names as keys + $fields = []; + foreach ($definition['mappings']['fields'] as $name => $field) { + $key = $persister->prepareFieldName($name); + if ($class->hasField($key)) { + $fields[$class->getFieldMapping($key)['name']] = $field; + } else { + $fields[$key] = $field; + } + } + + $definition['mappings']['fields'] = $fields; + } + + $newIndexes[] = [ + 'type' => $index['type'], + 'name' => $index['name'], + 'definition' => $definition, + ]; + } + + return $newIndexes; + } + /** * Ensure collection validators are up to date for all mapped document classes. */ diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php index 7e67db19c6..f97588ec6f 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php @@ -5,11 +5,15 @@ namespace Doctrine\ODM\MongoDB\Tests\Aggregation\Stage; use Closure; +use DateTime; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\AbstractSearchOperator; use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\CompoundSearchOperatorInterface; +use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsEmbeddableSearchOperators; use Doctrine\ODM\MongoDB\Tests\Aggregation\AggregationTestTrait; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; +use Documents\CmsArticle; +use Documents\User; use Generator; use GeoJson\Geometry\Point; use GeoJson\Geometry\Polygon; @@ -31,13 +35,12 @@ public static function provideAutocompleteBuilders(): Generator 'expectedOperator' => [ 'autocomplete' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => 'content', + 'path' => 'article_title', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete('content', 'MongoDB', 'Aggregation', 'Pipeline') - ->path('content'); + ->path('title'); }, ]; @@ -49,8 +52,7 @@ public static function provideAutocompleteBuilders(): Generator 'tokenOrder' => 'any', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -62,17 +64,16 @@ public static function provideAutocompleteBuilders(): Generator 'expectedOperator' => [ 'autocomplete' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => 'content', + 'path' => 'article_title', 'score' => (object) [ 'boost' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') - ->path('content') + ->path('title') ->boostScore(1.5); }, ]; @@ -87,8 +88,7 @@ public static function provideAutocompleteBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -108,8 +108,7 @@ public static function provideAutocompleteBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->autocomplete() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -134,8 +133,7 @@ public static function provideCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->compound() ->must() ->text() @@ -166,8 +164,7 @@ public static function provideCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->compound() ->must() ->text() @@ -197,8 +194,7 @@ public static function provideCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->compound() ->must() ->text() @@ -226,8 +222,7 @@ public static function provideEmbeddedDocumentBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->embeddedDocument('items') ->text() ->path('items.content') @@ -270,8 +265,7 @@ public static function provideEmbeddedDocumentCompoundBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search $stage) { return $stage->embeddedDocument('items') ->compound() ->must() @@ -295,8 +289,7 @@ public static function provideEqualsBuilders(): Generator 'value' => 'MongoDB Aggregation Pipeline', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->equals('content', 'MongoDB Aggregation Pipeline'); }, ]; @@ -311,8 +304,7 @@ public static function provideEqualsBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->equals() ->path('content') ->value('MongoDB Aggregation Pipeline') @@ -330,8 +322,7 @@ public static function provideEqualsBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->equals() ->path('content') ->value('MongoDB Aggregation Pipeline') @@ -344,11 +335,10 @@ public static function provideExistsBuilders(): Generator { yield 'Exists required only' => [ 'expectedOperator' => [ - 'exists' => (object) ['path' => 'content'], + 'exists' => (object) ['path' => 'article_title'], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->exists('content'); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->exists('title'); }, ]; } @@ -358,17 +348,16 @@ public static function provideGeoShapeBuilders(): Generator yield 'CompoundedGeoShape required only' => [ 'expectedOperator' => [ 'geoShape' => (object) [ - 'path' => ['location1', 'location2'], + 'path' => ['article_title', 'location2'], 'relation' => 'contains', 'geometry' => ['coordinates' => [12.345, 23.456], 'type' => 'Point'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoShape( new Point([12.345, 23.456]), 'contains', - 'location1', + 'title', 'location2', ); }, @@ -385,8 +374,7 @@ public static function provideGeoShapeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoShape() ->path('location') ->relation('contains') @@ -406,8 +394,7 @@ public static function provideGeoShapeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoShape() ->path('location') ->relation('contains') @@ -422,16 +409,15 @@ public static function provideGeoWithinBuilders(): Generator yield 'GeoWithin box' => [ 'expectedOperator' => [ 'geoWithin' => (object) [ - 'path' => ['location1', 'location2'], + 'path' => ['article_title', 'location2'], 'box' => (object) [ 'bottomLeft' => ['coordinates' => [-12.345, -23.456], 'type' => 'Point'], 'topRight' => ['coordinates' => [12.345, 23.456], 'type' => 'Point'], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->geoWithin('location1', 'location2') + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->geoWithin('title', 'location2') ->box(new Point([-12.345, -23.456]), new Point([12.345, 23.456])); }, ]; @@ -446,8 +432,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->circle(new Point([12.345, 23.456]), 3.14); @@ -467,8 +452,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->geometry(new Polygon([ @@ -491,8 +475,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->circle(new Point([12.345, 23.456]), 3.14) @@ -513,8 +496,7 @@ public static function provideGeoWithinBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->geoWithin() ->path('location') ->circle(new Point([12.345, 23.456]), 3.14) @@ -527,10 +509,9 @@ public static function provideMoreLikeThisBuilders(): Generator { yield 'MoreLikeThis with single like' => [ 'expectedOperator' => [ - 'moreLikeThis' => (object) ['like' => [['title' => 'The Godfather']]], + 'moreLikeThis' => (object) ['like' => [['article_title' => 'The Godfather']]], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->moreLikeThis(['title' => 'The Godfather']); }, ]; @@ -539,16 +520,29 @@ public static function provideMoreLikeThisBuilders(): Generator 'expectedOperator' => [ 'moreLikeThis' => (object) [ 'like' => [ - ['title' => 'The Godfather'], - ['title' => 'The Green Mile'], + ['article_title' => 'The Godfather', 'not_mapped_field' => 'Some value'], + ['article_title' => 'The Green Mile'], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->moreLikeThis(['title' => 'The Godfather'], ['title' => 'The Green Mile']); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->moreLikeThis(['title' => 'The Godfather', 'not_mapped_field' => 'Some value'], ['title' => 'The Green Mile']); }, ]; + + yield 'MoreLikeThis with field names mapping' => [ + 'expectedOperator' => [ + 'moreLikeThis' => (object) [ + 'like' => [ + ['disable-at' => new UTCDateTime(new DateTime('2020-01-01T00:00:00Z'))], + ], + ], + ], + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->moreLikeThis(['disabledAt' => new DateTime('2020-01-01T00:00:00Z')]); + }, + 'className' => User::class, + ]; } public static function provideNearBuilders(): Generator @@ -558,12 +552,11 @@ public static function provideNearBuilders(): Generator 'near' => (object) [ 'origin' => 5, 'pivot' => 3, - 'path' => ['value1', 'value2'], + 'path' => ['article_title', 'value2'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->near(5, 3, 'value1', 'value2'); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->near(5, 3, 'title', 'value2'); }, ]; @@ -577,8 +570,7 @@ public static function provideNearBuilders(): Generator 'path' => ['createdAt'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) use ($date) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) use ($date) { return $stage->near() ->path('createdAt') ->origin($date) @@ -594,8 +586,7 @@ public static function provideNearBuilders(): Generator 'path' => ['createdAt'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->near() ->path('createdAt') ->origin(new Point([12.345, 23.456])) @@ -610,11 +601,10 @@ public static function providePhraseBuilders(): Generator 'expectedOperator' => [ 'phrase' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -629,8 +619,7 @@ public static function providePhraseBuilders(): Generator 'slop' => 3, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -648,8 +637,7 @@ public static function providePhraseBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -667,8 +655,7 @@ public static function providePhraseBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->phrase() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -683,12 +670,11 @@ public static function provideQueryStringBuilders(): Generator 'expectedOperator' => [ 'queryString' => (object) [ 'query' => 'MongoDB Aggregation Pipeline', - 'defaultPath' => 'content', + 'defaultPath' => 'article_title', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { - return $stage->queryString('MongoDB Aggregation Pipeline', 'content'); + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { + return $stage->queryString('MongoDB Aggregation Pipeline', 'title'); }, ]; @@ -702,8 +688,7 @@ public static function provideQueryStringBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->queryString() ->query('content:pipeline OR title:pipeline') ->defaultPath('content') @@ -721,8 +706,7 @@ public static function provideQueryStringBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->queryString() ->query('content:pipeline OR title:pipeline') ->defaultPath('content') @@ -736,14 +720,13 @@ public static function provideRangeBuilders(): Generator yield 'Range gt only' => [ 'expectedOperator' => [ 'range' => (object) [ - 'path' => ['field1', 'field2'], + 'path' => ['article_title', 'field2'], 'gt' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() - ->path('field1', 'field2') + ->path('title', 'field2') ->gt(5); }, ]; @@ -755,8 +738,7 @@ public static function provideRangeBuilders(): Generator 'gte' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->gte(5); @@ -770,8 +752,7 @@ public static function provideRangeBuilders(): Generator 'lt' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lt(5); @@ -785,8 +766,7 @@ public static function provideRangeBuilders(): Generator 'lte' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(5); @@ -801,8 +781,7 @@ public static function provideRangeBuilders(): Generator 'gte' => 5, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(10) @@ -821,8 +800,7 @@ public static function provideRangeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(10) @@ -842,8 +820,7 @@ public static function provideRangeBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->range() ->path('field1', 'field2') ->lte(10) @@ -859,11 +836,10 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -874,12 +850,11 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => true, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -891,12 +866,11 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => false, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -908,14 +882,13 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'boost' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -927,14 +900,13 @@ public static function provideRegexBuilders(): Generator 'expectedOperator' => [ 'regex' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'constant' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->regex() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -949,11 +921,10 @@ public static function provideTextBuilders(): Generator 'expectedOperator' => [ 'text' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -968,8 +939,7 @@ public static function provideTextBuilders(): Generator 'synonyms' => 'mySynonyms', ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -987,8 +957,7 @@ public static function provideTextBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -1006,8 +975,7 @@ public static function provideTextBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -1027,8 +995,7 @@ public static function provideTextBuilders(): Generator ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->text() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('content') @@ -1043,11 +1010,10 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content'); @@ -1058,12 +1024,11 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => true, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1075,12 +1040,11 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'allowAnalyzedField' => false, ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1092,14 +1056,13 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'boost' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1111,14 +1074,13 @@ public static function provideWildcardBuilders(): Generator 'expectedOperator' => [ 'wildcard' => (object) [ 'query' => ['MongoDB', 'Aggregation', 'Pipeline'], - 'path' => ['title', 'content'], + 'path' => ['article_title', 'content'], 'score' => (object) [ 'constant' => (object) ['value' => 1.5], ], ], ], - /** @param Search|CompoundSearchOperatorInterface $stage */ - 'createOperator' => static function ($stage) { + 'createOperator' => static function (Search|CompoundSearchOperatorInterface|SupportsEmbeddableSearchOperators $stage) { return $stage->wildcard() ->query('MongoDB', 'Aggregation', 'Pipeline') ->path('title', 'content') @@ -1143,7 +1105,7 @@ public static function provideWildcardBuilders(): Generator #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchOperators(array $expectedOperator, Closure $createOperator): void + public function testSearchOperators(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1159,7 +1121,7 @@ public function testSearchOperators(array $expectedOperator, Closure $createOper 'returnStoredSource' => true, ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index'); @@ -1196,7 +1158,7 @@ public function testSearchOperators(array $expectedOperator, Closure $createOper #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchOperatorsWithSort(array $expectedOperator, Closure $createOperator): void + public function testSearchOperatorsWithSort(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1207,7 +1169,7 @@ public function testSearchOperatorsWithSort(array $expectedOperator, Closure $cr ], ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index'); @@ -1242,9 +1204,9 @@ public function testSearchOperatorsWithSort(array $expectedOperator, Closure $cr #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchCompoundOperators(array $expectedOperator, Closure $createOperator): void + public function testSearchCompoundOperators(array $expectedOperator, Closure $createOperator, ?string $className = null): void { - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $compound = $searchStage ->index('my_search_index') ->compound(); @@ -1291,9 +1253,9 @@ public function testSearchCompoundOperators(array $expectedOperator, Closure $cr #[DataProvider('provideRegexBuilders')] #[DataProvider('provideTextBuilders')] #[DataProvider('provideWildcardBuilders')] - public function testSearchEmbeddedDocumentOperators(array $expectedOperator, Closure $createOperator): void + public function testSearchEmbeddedDocumentOperators(array $expectedOperator, Closure $createOperator, ?string $className = null): void { - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $embedded = $searchStage ->index('my_search_index') ->embeddedDocument('foo'); @@ -1315,7 +1277,7 @@ public function testSearchEmbeddedDocumentOperators(array $expectedOperator, Clo } #[DataProvider('provideAutocompleteBuilders')] - public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Closure $createOperator): void + public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1332,7 +1294,7 @@ public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Clo 'searchBefore' => 'marker', ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index') ->searchBefore('marker'); @@ -1356,7 +1318,7 @@ public function testSearchOperatorsWithSearchBefore(array $expectedOperator, Clo } #[DataProvider('provideAutocompleteBuilders')] - public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Closure $createOperator): void + public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Closure $createOperator, ?string $className = null): void { $baseExpected = [ 'index' => 'my_search_index', @@ -1373,7 +1335,7 @@ public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Clos 'searchAfter' => 'marker', ]; - $searchStage = new Search($this->getTestAggregationBuilder()); + $searchStage = $this->createSearchStage($className); $searchStage ->index('my_search_index') ->searchAfter('marker'); @@ -1395,4 +1357,12 @@ public function testSearchOperatorsWithSearchAfter(array $expectedOperator, Clos $searchStage->getExpression(), ); } + + /** @param class-string $className */ + private function createSearchStage(?string $className = null): Search + { + $className ??= CmsArticle::class; + + return new Search($this->getTestAggregationBuilder($className), $this->dm->getUnitOfWork()->getDocumentPersister($className)); + } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/VectorSearchTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/VectorSearchTest.php index af3628c09f..5d83c407c6 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/VectorSearchTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/VectorSearchTest.php @@ -4,9 +4,12 @@ namespace Doctrine\ODM\MongoDB\Tests\Aggregation\Stage; +use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\VectorSearch; use Doctrine\ODM\MongoDB\Tests\Aggregation\AggregationTestTrait; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; +use Documents\User; +use Documents\VectorEmbedding; use MongoDB\BSON\Binary; class VectorSearchTest extends BaseTestCase @@ -15,63 +18,69 @@ class VectorSearchTest extends BaseTestCase public function testEmptyStage(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage] = $this->createVectorSearchStage(); self::assertSame(['$vectorSearch' => []], $stage->getExpression()); } public function testExact(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage, $builder] = $this->createVectorSearchStage(); $stage->exact(true); self::assertSame(['$vectorSearch' => ['exact' => true]], $stage->getExpression()); } public function testFilter(): void { - $builder = $this->getTestAggregationBuilder(); - $stage = new VectorSearch($builder); + [$stage, $builder] = $this->createVectorSearchStage(); $stage->filter($builder->matchExpr()->field('status')->notEqual('inactive')); self::assertSame(['$vectorSearch' => ['filter' => ['status' => ['$ne' => 'inactive']]]], $stage->getExpression()); } public function testIndex(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage] = $this->createVectorSearchStage(); $stage->index('myIndex'); self::assertSame(['$vectorSearch' => ['index' => 'myIndex']], $stage->getExpression()); } public function testLimit(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage] = $this->createVectorSearchStage(); $stage->limit(10); self::assertSame(['$vectorSearch' => ['limit' => 10]], $stage->getExpression()); } public function testNumCandidates(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage] = $this->createVectorSearchStage(); $stage->numCandidates(5); self::assertSame(['$vectorSearch' => ['numCandidates' => 5]], $stage->getExpression()); } public function testPath(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage] = $this->createVectorSearchStage(); $stage->path('vectorField'); self::assertSame(['$vectorSearch' => ['path' => 'vectorField']], $stage->getExpression()); } + public function testPathIsPrepared(): void + { + [$stage] = $this->createVectorSearchStage(VectorEmbedding::class); + $stage->path('vectorFloat'); + self::assertSame(['$vectorSearch' => ['path' => 'db_vector_float']], $stage->getExpression()); + } + public function testQueryVector(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage] = $this->createVectorSearchStage(); $stage->queryVector([1, 2, 3]); self::assertSame(['$vectorSearch' => ['queryVector' => [1, 2, 3]]], $stage->getExpression()); } public function testQueryVectorAcceptsBinary(): void { - $stage = new VectorSearch($this->getTestAggregationBuilder()); + [$stage] = $this->createVectorSearchStage(); $binaryVector = new Binary("\x01\x02\x03", 9); $stage->queryVector($binaryVector); self::assertSame(['$vectorSearch' => ['queryVector' => $binaryVector]], $stage->getExpression()); @@ -79,8 +88,8 @@ public function testQueryVectorAcceptsBinary(): void public function testChainingAllOptions(): void { - $builder = $this->getTestAggregationBuilder(); - $stage = (new VectorSearch($builder)) + [$stage, $builder] = $this->createVectorSearchStage(); + $stage ->exact(false) ->filter($builder->matchExpr()->field('status')->notEqual('inactive')) ->index('idx') @@ -100,4 +109,17 @@ public function testChainingAllOptions(): void ], ], $stage->getExpression()); } + + /** + * @param class-string $className + * + * @return array{0: VectorSearch, 1: Builder} + */ + private function createVectorSearchStage(string $className = User::class): array + { + return [ + new VectorSearch($builder = $this->getTestAggregationBuilder($className), $this->dm->getUnitOfWork()->getDocumentPersister($className)), + $builder, + ]; + } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/AtlasSearchTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/AtlasSearchTest.php index 7dc805bbcc..636007c019 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/AtlasSearchTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/AtlasSearchTest.php @@ -53,7 +53,7 @@ public function testAtlasSearch(): void $results = $this->dm->createAggregationBuilder(CmsArticle::class) ->search() ->index('search_articles') - ->text() + ->autocomplete() ->query('Mongo') ->path('title') ->fuzzy(2, 2) diff --git a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php index caec09b847..4797be6470 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php @@ -410,22 +410,49 @@ public function testCreateSearchIndexes(): void $this->schemaManager->createSearchIndexes(); } + public function testCreateDocumentSearchIndexesNotCreatedError(): void + { + $this->documentCollections['CmsArticle'] + ->expects($this->once()) + ->method('createSearchIndexes') + ->with($this->anything()) + ->willReturn(['foo']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The following search indexes for Documents\CmsArticle were not created: search_articles'); + + $this->schemaManager->createDocumentSearchIndexes(CmsArticle::class); + } + public function testCreateDocumentSearchIndexes(): void { $expectedCollectionName = $this->dm->getClassMetadata(CmsArticle::class)->getCollection(); foreach ($this->documentCollections as $collectionName => $collection) { if ($collectionName === $expectedCollectionName) { - $collection + $this->documentCollections['CmsArticle'] ->expects($this->once()) ->method('createSearchIndexes') - ->with([ - [ - 'definition' => ['mappings' => ['dynamic' => true]], - 'name' => 'search_articles', - 'type' => 'search', - ], - ]) - ->willReturn(['search_articles']); + ->with($this->anything()) + ->willReturnCallback(function (array $indexes) { + $this->assertSame([ + [ + 'type' => 'search', + 'name' => 'search_articles', + 'definition' => [ + 'mappings' => [ + 'dynamic' => true, + 'fields' => [ + 'article_title' => ['type' => 'autocomplete'], + 'text' => ['type' => 'string'], + 'not_mapped_field' => ['type' => 'token'], + ], + ], + ], + ], + ], $indexes); + + return ['search_articles']; + }); } else { $collection->expects($this->never())->method('createSearchIndexes'); } @@ -434,6 +461,49 @@ public function testCreateDocumentSearchIndexes(): void $this->schemaManager->createDocumentSearchIndexes(CmsArticle::class); } + public function testCreateVectorSearchIndex(): void + { + $expectedCollectionName = $this->dm->getClassMetadata(VectorEmbedding::class)->getCollection(); + foreach ($this->documentCollections as $collectionName => $collection) { + if ($collectionName === $expectedCollectionName) { + $this->documentCollections['vector_embeddings'] + ->expects($this->once()) + ->method('createSearchIndexes') + ->with($this->anything()) + ->willReturnCallback(function (array $indexes) { + $this->assertSame([ + [ + 'type' => 'vectorSearch', + 'name' => 'default', + 'definition' => [ + 'fields' => [ + ['type' => 'vector', 'path' => 'db_vector_float', 'numDimensions' => 3, 'similarity' => 'dotProduct'], + ], + ], + ], + [ + 'type' => 'vectorSearch', + 'name' => 'vector_int', + 'definition' => [ + 'fields' => [ + ['type' => 'vector', 'path' => 'vectorInt', 'numDimensions' => 3, 'similarity' => 'cosine'], + ['type' => 'filter', 'path' => 'filterField'], + ['type' => 'filter', 'path' => 'not_mapped_filter'], + ], + ], + ], + ], $indexes); + + return ['default', 'vector_int']; + }); + } else { + $collection->expects($this->never())->method('createSearchIndexes'); + } + } + + $this->schemaManager->createDocumentSearchIndexes(VectorEmbedding::class); + } + public function testCreateDocumentSearchIndexesNotSupported(): void { $exception = $this->createSearchIndexCommandException(); diff --git a/tests/Documents/CmsArticle.php b/tests/Documents/CmsArticle.php index 00deb5d90d..cd059c9543 100644 --- a/tests/Documents/CmsArticle.php +++ b/tests/Documents/CmsArticle.php @@ -8,7 +8,15 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ODM\Index(keys: ['topic' => 'asc'])] -#[ODM\SearchIndex(name: 'search_articles', dynamic: true)] +#[ODM\SearchIndex( + name: 'search_articles', + dynamic: true, + fields: [ + 'title' => ['type' => 'autocomplete'], + 'text' => ['type' => 'string'], + 'not_mapped_field' => ['type' => 'token'], + ], +)] #[ODM\Document] class CmsArticle { @@ -19,7 +27,7 @@ class CmsArticle #[ODM\Field(type: 'string')] public $topic; /** @var string|null */ - #[ODM\Field(type: 'string')] + #[ODM\Field(type: 'string', name: 'article_title')] public $title; /** @var string|null */ #[ODM\Field(type: 'string')] diff --git a/tests/Documents/VectorEmbedding.php b/tests/Documents/VectorEmbedding.php index b68fd0d039..184d650771 100644 --- a/tests/Documents/VectorEmbedding.php +++ b/tests/Documents/VectorEmbedding.php @@ -22,6 +22,7 @@ fields: [ ['type' => 'vector', 'path' => 'vectorInt', 'numDimensions' => 3, 'similarity' => ClassMetadata::VECTOR_SIMILARITY_COSINE], ['type' => 'filter', 'path' => 'filterField'], + ['type' => 'filter', 'path' => 'not_mapped_filter'], ], )] class VectorEmbedding @@ -30,7 +31,7 @@ class VectorEmbedding public ?string $id = null; /** @var list */ - #[Field(type: Type::COLLECTION)] + #[Field(type: Type::COLLECTION, name: 'db_vector_float')] public array $vectorFloat = []; /** @var list */