diff --git a/generator/config/stage/search.yaml b/generator/config/stage/search.yaml index 34bf57a34..44756ce23 100644 --- a/generator/config/stage/search.yaml +++ b/generator/config/stage/search.yaml @@ -45,7 +45,7 @@ arguments: name: count optional: true type: - - string + - object description: | Document that specifies the count options for retrieving a count of the results. - @@ -116,3 +116,150 @@ tests: $replaceWith: '$$SEARCH_META' - $limit: 1 + - + name: 'Date Search and Sort' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/sort/#date-search-and-sort' + pipeline: + - + $search: + range: + path: 'released' + gt: !bson_utcdatetime '2010-01-01T00:00:00.000Z' + lt: !bson_utcdatetime '2015-01-01T00:00:00.000Z' + sort: + released: -1 + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + released: 1 + - + name: 'Number Search and Sort' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/sort/#number-search-and-sort' + pipeline: + - + $search: + range: + path: 'awards.wins' + gt: 3 + sort: + awards.wins: -1 + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + awards.wins: 1 + - + name: 'Sort by score' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/sort/#sort-by-score' + pipeline: + - + $search: + text: + path: 'title' + query: 'story' + sort: + score: + $meta: 'searchScore' + order: 1 + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' + - + name: 'Paginate results after a token' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/paginate-results/#search-after-the-reference-point' + pipeline: + - + $search: + text: + path: 'title' + query: 'war' + sort: + score: + $meta: 'searchScore' + released: 1 + searchAfter: 'CMtJGgYQuq+ngwgaCSkAjBYH7AAAAA==' + - + name: 'Paginate results before a token' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/paginate-results/#search-before-the-reference-point' + pipeline: + - + $search: + text: + path: 'title' + query: 'war' + sort: + score: + $meta: 'searchScore' + released: 1 + searchBefore: 'CJ6kARoGELqvp4MIGgkpACDA3U8BAAA=' + - + name: 'Count results' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/counting/#count-results' + pipeline: + - + $search: + near: + path: 'released' + origin: !bson_utcdatetime '2011-09-01T00:00:00.000+00:00' + pivot: 7776000000 + count: + type: 'total' + - + $project: + meta: '$$SEARCH_META' + title: 1 + released: 1 + - + $limit: 2 + - + name: 'Track Search terms' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/tracking/#examples' + pipeline: + - + $search: + text: + query: 'summer' + path: 'title' + tracking: + searchTerms: 'summer' + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + - + name: 'Return Stored Source Fields' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/return-stored-source/#examples' + pipeline: + - + $search: + text: + query: 'baseball' + path: 'title' + returnStoredSource: true + - + $match: + $or: + - + imdb.rating: + $gt: 8.2 + - + imdb.votes: + $gte: 4500 + - + $lookup: + from: 'movies' + localField: '_id' + foreignField: '_id' + as: 'document' diff --git a/src/Builder/Stage/FactoryTrait.php b/src/Builder/Stage/FactoryTrait.php index 95b0e6e37..7ef8fb62b 100644 --- a/src/Builder/Stage/FactoryTrait.php +++ b/src/Builder/Stage/FactoryTrait.php @@ -545,7 +545,7 @@ public static function sample(int $size): SampleStage * @param Optional|bool $concurrent Parallelize search across segments on dedicated search nodes. * If you don't have separate search nodes on your cluster, * Atlas Search ignores this flag. If omitted, defaults to false. - * @param Optional|string $count Document that specifies the count options for retrieving a count of the results. + * @param Optional|Document|Serializable|array|stdClass $count Document that specifies the count options for retrieving a count of the results. * @param Optional|string $searchAfter Reference point for retrieving results. searchAfter returns documents starting immediately following the specified reference point. * @param Optional|string $searchBefore Reference point for retrieving results. searchBefore returns documents starting immediately before the specified reference point. * @param Optional|bool $scoreDetails Flag that specifies whether to retrieve a detailed breakdown of the score for the documents in the results. If omitted, defaults to false. @@ -558,7 +558,7 @@ public static function search( Optional|string $index = Optional::Undefined, Optional|Document|Serializable|stdClass|array $highlight = Optional::Undefined, Optional|bool $concurrent = Optional::Undefined, - Optional|string $count = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $count = Optional::Undefined, Optional|string $searchAfter = Optional::Undefined, Optional|string $searchBefore = Optional::Undefined, Optional|bool $scoreDetails = Optional::Undefined, diff --git a/src/Builder/Stage/FluentFactoryTrait.php b/src/Builder/Stage/FluentFactoryTrait.php index 757a09a0b..803ca9905 100644 --- a/src/Builder/Stage/FluentFactoryTrait.php +++ b/src/Builder/Stage/FluentFactoryTrait.php @@ -613,7 +613,7 @@ public function sample(int $size): static * @param Optional|bool $concurrent Parallelize search across segments on dedicated search nodes. * If you don't have separate search nodes on your cluster, * Atlas Search ignores this flag. If omitted, defaults to false. - * @param Optional|string $count Document that specifies the count options for retrieving a count of the results. + * @param Optional|Document|Serializable|array|stdClass $count Document that specifies the count options for retrieving a count of the results. * @param Optional|string $searchAfter Reference point for retrieving results. searchAfter returns documents starting immediately following the specified reference point. * @param Optional|string $searchBefore Reference point for retrieving results. searchBefore returns documents starting immediately before the specified reference point. * @param Optional|bool $scoreDetails Flag that specifies whether to retrieve a detailed breakdown of the score for the documents in the results. If omitted, defaults to false. @@ -626,7 +626,7 @@ public function search( Optional|string $index = Optional::Undefined, Optional|Document|Serializable|stdClass|array $highlight = Optional::Undefined, Optional|bool $concurrent = Optional::Undefined, - Optional|string $count = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $count = Optional::Undefined, Optional|string $searchAfter = Optional::Undefined, Optional|string $searchBefore = Optional::Undefined, Optional|bool $scoreDetails = Optional::Undefined, diff --git a/src/Builder/Stage/SearchStage.php b/src/Builder/Stage/SearchStage.php index bd224dba9..1aac4a423 100644 --- a/src/Builder/Stage/SearchStage.php +++ b/src/Builder/Stage/SearchStage.php @@ -62,8 +62,8 @@ final class SearchStage implements StageInterface, OperatorInterface */ public readonly Optional|bool $concurrent; - /** @var Optional|string $count Document that specifies the count options for retrieving a count of the results. */ - public readonly Optional|string $count; + /** @var Optional|Document|Serializable|array|stdClass $count Document that specifies the count options for retrieving a count of the results. */ + public readonly Optional|Document|Serializable|stdClass|array $count; /** @var Optional|string $searchAfter Reference point for retrieving results. searchAfter returns documents starting immediately following the specified reference point. */ public readonly Optional|string $searchAfter; @@ -91,7 +91,7 @@ final class SearchStage implements StageInterface, OperatorInterface * @param Optional|bool $concurrent Parallelize search across segments on dedicated search nodes. * If you don't have separate search nodes on your cluster, * Atlas Search ignores this flag. If omitted, defaults to false. - * @param Optional|string $count Document that specifies the count options for retrieving a count of the results. + * @param Optional|Document|Serializable|array|stdClass $count Document that specifies the count options for retrieving a count of the results. * @param Optional|string $searchAfter Reference point for retrieving results. searchAfter returns documents starting immediately following the specified reference point. * @param Optional|string $searchBefore Reference point for retrieving results. searchBefore returns documents starting immediately before the specified reference point. * @param Optional|bool $scoreDetails Flag that specifies whether to retrieve a detailed breakdown of the score for the documents in the results. If omitted, defaults to false. @@ -104,7 +104,7 @@ public function __construct( Optional|string $index = Optional::Undefined, Optional|Document|Serializable|stdClass|array $highlight = Optional::Undefined, Optional|bool $concurrent = Optional::Undefined, - Optional|string $count = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $count = Optional::Undefined, Optional|string $searchAfter = Optional::Undefined, Optional|string $searchBefore = Optional::Undefined, Optional|bool $scoreDetails = Optional::Undefined, diff --git a/tests/Builder/Stage/Pipelines.php b/tests/Builder/Stage/Pipelines.php index 850013c3e..81f5b8fe3 100644 --- a/tests/Builder/Stage/Pipelines.php +++ b/tests/Builder/Stage/Pipelines.php @@ -2612,6 +2612,324 @@ enum Pipelines: string ] JSON; + /** + * Date Search and Sort + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/sort/#date-search-and-sort + */ + case SearchDateSearchAndSort = <<<'JSON' + [ + { + "$search": { + "range": { + "path": "released", + "gt": { + "$date": { + "$numberLong": "1262304000000" + } + }, + "lt": { + "$date": { + "$numberLong": "1420070400000" + } + } + }, + "sort": { + "released": { + "$numberInt": "-1" + } + } + } + }, + { + "$limit": { + "$numberInt": "5" + } + }, + { + "$project": { + "_id": { + "$numberInt": "0" + }, + "title": { + "$numberInt": "1" + }, + "released": { + "$numberInt": "1" + } + } + } + ] + JSON; + + /** + * Number Search and Sort + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/sort/#number-search-and-sort + */ + case SearchNumberSearchAndSort = <<<'JSON' + [ + { + "$search": { + "range": { + "path": "awards.wins", + "gt": { + "$numberInt": "3" + } + }, + "sort": { + "awards.wins": { + "$numberInt": "-1" + } + } + } + }, + { + "$limit": { + "$numberInt": "5" + } + }, + { + "$project": { + "_id": { + "$numberInt": "0" + }, + "title": { + "$numberInt": "1" + }, + "awards.wins": { + "$numberInt": "1" + } + } + } + ] + JSON; + + /** + * Sort by score + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/sort/#sort-by-score + */ + case SearchSortByScore = <<<'JSON' + [ + { + "$search": { + "text": { + "path": "title", + "query": "story" + }, + "sort": { + "score": { + "$meta": "searchScore", + "order": { + "$numberInt": "1" + } + } + } + } + }, + { + "$limit": { + "$numberInt": "5" + } + }, + { + "$project": { + "_id": { + "$numberInt": "0" + }, + "title": { + "$numberInt": "1" + }, + "score": { + "$meta": "searchScore" + } + } + } + ] + JSON; + + /** + * Paginate results after a token + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/paginate-results/#search-after-the-reference-point + */ + case SearchPaginateResultsAfterAToken = <<<'JSON' + [ + { + "$search": { + "text": { + "path": "title", + "query": "war" + }, + "sort": { + "score": { + "$meta": "searchScore" + }, + "released": { + "$numberInt": "1" + } + }, + "searchAfter": "CMtJGgYQuq+ngwgaCSkAjBYH7AAAAA==" + } + } + ] + JSON; + + /** + * Paginate results before a token + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/paginate-results/#search-before-the-reference-point + */ + case SearchPaginateResultsBeforeAToken = <<<'JSON' + [ + { + "$search": { + "text": { + "path": "title", + "query": "war" + }, + "sort": { + "score": { + "$meta": "searchScore" + }, + "released": { + "$numberInt": "1" + } + }, + "searchBefore": "CJ6kARoGELqvp4MIGgkpACDA3U8BAAA=" + } + } + ] + JSON; + + /** + * Count results + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/counting/#count-results + */ + case SearchCountResults = <<<'JSON' + [ + { + "$search": { + "near": { + "path": "released", + "origin": { + "$date": { + "$numberLong": "1314835200000" + } + }, + "pivot": { + "$numberLong": "7776000000" + } + }, + "count": { + "type": "total" + } + } + }, + { + "$project": { + "meta": "$$SEARCH_META", + "title": { + "$numberInt": "1" + }, + "released": { + "$numberInt": "1" + } + } + }, + { + "$limit": { + "$numberInt": "2" + } + } + ] + JSON; + + /** + * Track Search terms + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/tracking/#examples + */ + case SearchTrackSearchTerms = <<<'JSON' + [ + { + "$search": { + "text": { + "query": "summer", + "path": "title" + }, + "tracking": { + "searchTerms": "summer" + } + } + }, + { + "$limit": { + "$numberInt": "5" + } + }, + { + "$project": { + "_id": { + "$numberInt": "0" + }, + "title": { + "$numberInt": "1" + } + } + } + ] + JSON; + + /** + * Return Stored Source Fields + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/return-stored-source/#examples + */ + case SearchReturnStoredSourceFields = <<<'JSON' + [ + { + "$search": { + "text": { + "query": "baseball", + "path": "title" + }, + "returnStoredSource": true + } + }, + { + "$match": { + "$or": [ + { + "imdb.rating": { + "$gt": { + "$numberDouble": "8.1999999999999992895" + } + } + }, + { + "imdb.votes": { + "$gte": { + "$numberInt": "4500" + } + } + } + ] + } + }, + { + "$lookup": { + "from": "movies", + "localField": "_id", + "foreignField": "_id", + "as": "document" + } + } + ] + JSON; + /** * Example * diff --git a/tests/Builder/Stage/SearchStageTest.php b/tests/Builder/Stage/SearchStageTest.php index faae2ab11..0af9d7742 100644 --- a/tests/Builder/Stage/SearchStageTest.php +++ b/tests/Builder/Stage/SearchStageTest.php @@ -8,15 +8,65 @@ use MongoDB\BSON\UTCDateTime; use MongoDB\Builder\Expression; use MongoDB\Builder\Pipeline; +use MongoDB\Builder\Query; use MongoDB\Builder\Search; use MongoDB\Builder\Stage; +use MongoDB\Builder\Variable; use MongoDB\Tests\Builder\PipelineTestCase; +use function MongoDB\object; + /** * Test $search stage */ class SearchStageTest extends PipelineTestCase { + public function testCountResults(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::near( + path: 'released', + origin: new UTCDateTime(new DateTime('2011-09-01T00:00:00.000+00:00')), + pivot: 7776000000, + ), + count: object(type: 'total'), + ), + Stage::project( + meta: Variable::searchMeta(), + title: 1, + released: 1, + ), + Stage::limit(2), + ); + + $this->assertSamePipeline(Pipelines::SearchCountResults, $pipeline); + } + + public function testDateSearchAndSort(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::range( + path: 'released', + gt: new UTCDateTime(new DateTime('2010-01-01')), + lt: new UTCDateTime(new DateTime('2015-01-01')), + ), + sort: object( + released: -1, + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + released: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::SearchDateSearchAndSort, $pipeline); + } + public function testExample(): void { $pipeline = new Pipeline( @@ -40,4 +90,136 @@ public function testExample(): void $this->assertSamePipeline(Pipelines::SearchExample, $pipeline); } + + public function testNumberSearchAndSort(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::range( + path: 'awards.wins', + gt: 3, + ), + sort: ['awards.wins' => -1], + ), + Stage::limit(5), + Stage::project(...[ + '_id' => 0, + 'title' => 1, + 'awards.wins' => 1, + ]), + ); + + $this->assertSamePipeline(Pipelines::SearchNumberSearchAndSort, $pipeline); + } + + public function testPaginateResultsAfterAToken(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'title', + query: 'war', + ), + searchAfter: 'CMtJGgYQuq+ngwgaCSkAjBYH7AAAAA==', + sort: object( + score: ['$meta' => 'searchScore'], + released: 1, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SearchPaginateResultsAfterAToken, $pipeline); + } + + public function testPaginateResultsBeforeAToken(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'title', + query: 'war', + ), + searchBefore: 'CJ6kARoGELqvp4MIGgkpACDA3U8BAAA=', + sort: object( + score: ['$meta' => 'searchScore'], + released: 1, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SearchPaginateResultsBeforeAToken, $pipeline); + } + + public function testReturnStoredSourceFields(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'title', + query: 'baseball', + ), + returnStoredSource: true, + ), + Stage::match( + Query::or( + Query::query(...['imdb.rating' => Query::gt(8.2)]), + Query::query(...['imdb.votes' => Query::gte(4500)]), + ), + ), + Stage::lookup( + as: 'document', + from: 'movies', + localField: '_id', + foreignField: '_id', + ), + ); + + $this->assertSamePipeline(Pipelines::SearchReturnStoredSourceFields, $pipeline); + } + + public function testSortByScore(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'title', + query: 'story', + ), + sort: object( + score: [ + '$meta' => 'searchScore', + 'order' => 1, + ], + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::SearchSortByScore, $pipeline); + } + + public function testTrackSearchTerms(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + query: 'summer', + path: 'title', + ), + tracking: object(searchTerms: 'summer'), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::SearchTrackSearchTerms, $pipeline); + } }