diff --git a/.gitattributes b/.gitattributes index 75618ae42..7309afd20 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,5 +22,6 @@ rector.php export-ignore /src/Builder/Accumulator/*.php linguist-generated=true /src/Builder/Expression/*.php linguist-generated=true /src/Builder/Query/*.php linguist-generated=true +/src/Builder/Search/*.php linguist-generated=true /src/Builder/Stage/*.php linguist-generated=true /tests/Builder/*/Pipelines.php linguist-generated=true diff --git a/generator/README.md b/generator/README.md index 3e18976f9..02a3be4ec 100644 --- a/generator/README.md +++ b/generator/README.md @@ -42,6 +42,8 @@ Each operator can contain a `tests` section with a list if pipelines. To represe | Int64 | `!bson_int64 '123456789'` | | Decimal128 | `!bson_decimal128 '0.9'` | | UTCDateTime | `!bson_utcdatetime 0` | +| ObjectId | `!bson_ObjectId '5a9427648b0beebeb69589a1` | | Binary | `!bson_binary 'IA=='` | +| Binary UUID | `!bson_uuid 'fac32260-b511-4c69-8485-a2be5b7dda9e'` | To add new test cases to operators, you can get inspiration from the official MongoDB documentation and use the `generator/js2yaml.html` web page to manually convert a pipeline array from JS to Yaml. diff --git a/generator/composer.json b/generator/composer.json index 2deb4ae95..c4dedcfb7 100644 --- a/generator/composer.json +++ b/generator/composer.json @@ -4,7 +4,7 @@ "repositories": [ { "type": "path", - "url": "../", + "url": "..", "symlink": true } ], diff --git a/generator/config/definitions.php b/generator/config/definitions.php index a4f952d39..06d9b44ed 100644 --- a/generator/config/definitions.php +++ b/generator/config/definitions.php @@ -58,4 +58,16 @@ OperatorTestGenerator::class, ], ], + + // Search Operators + [ + 'configFiles' => __DIR__ . '/search', + 'namespace' => 'MongoDB\\Builder\\Search', + 'classNameSuffix' => 'Operator', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + OperatorTestGenerator::class, + ], + ], ]; diff --git a/generator/config/expressions.php b/generator/config/expressions.php index f731234b2..a2c649e8e 100644 --- a/generator/config/expressions.php +++ b/generator/config/expressions.php @@ -109,6 +109,10 @@ 'implements' => [ResolvesToAny::class], 'acceptedTypes' => ['string'], ], + 'searchOperator' => [ + 'returnType' => Type\SearchOperatorInterface::class, + 'acceptedTypes' => [Type\SearchOperatorInterface::class, ...$bsonTypes['object']], + ], 'geometry' => [ 'returnType' => Type\GeometryInterface::class, 'acceptedTypes' => [Type\GeometryInterface::class, ...$bsonTypes['object']], @@ -168,4 +172,12 @@ 'GeoPoint' => [ 'acceptedTypes' => [...$bsonTypes['object']], ], + + // Search + 'searchPath' => [ + 'acceptedTypes' => ['string', 'array'], + ], + 'searchScore' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], ]; diff --git a/generator/config/schema.json b/generator/config/schema.json index 8e2f5f320..c92e6ddb7 100644 --- a/generator/config/schema.json +++ b/generator/config/schema.json @@ -46,7 +46,8 @@ "resolvesToInt", "resolvesToTimestamp", "resolvesToLong", - "resolvesToDecimal" + "resolvesToDecimal", + "searchOperator" ] } }, @@ -60,7 +61,8 @@ "enum": [ "array", "object", - "single" + "single", + "search" ] }, "description": { @@ -135,7 +137,8 @@ "resolvesToInt", "intFieldPath", "int", "resolvesToTimestamp", "timestampFieldPath", "timestamp", "resolvesToLong", "longFieldPath", "long", - "resolvesToDecimal", "decimalFieldPath", "decimal" + "resolvesToDecimal", "decimalFieldPath", "decimal", + "searchPath", "searchScore", "searchOperator" ] } }, diff --git a/generator/config/search/autocomplete.yaml b/generator/config/search/autocomplete.yaml new file mode 100644 index 000000000..a984b9a39 --- /dev/null +++ b/generator/config/search/autocomplete.yaml @@ -0,0 +1,152 @@ +# $schema: ../schema.json +name: autocomplete +link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/' +type: + - searchOperator +encode: object +description: | + The autocomplete operator performs a search for a word or phrase that + contains a sequence of characters from an incomplete input string. The + fields that you intend to query with the autocomplete operator must be + indexed with the autocomplete data type in the collection's index definition. +arguments: + - + name: path + type: + - searchPath + - + name: query + type: + - string + - + name: tokenOrder + optional: true + type: + - string # any|sequential + - + name: fuzzy + optional: true + type: + - object + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Basic' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#basic-example' + pipeline: + - + $search: + autocomplete: + query: 'off' + path: 'title' + - + $limit: 10 + - + $project: + _id: 0 + title: 1 + + - + name: 'Fuzzy' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#fuzzy-example' + pipeline: + - + $search: + autocomplete: + query: 'pre' + path: 'title' + fuzzy: + maxEdits: 1 + prefixLength: 1 + maxExpansions: 256 + - + $limit: 10 + - + $project: + _id: 0 + title: 1 + + - + name: 'Token Order any' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#simple-any-example' + pipeline: + - + $search: + autocomplete: + query: 'men with' + path: 'title' + tokenOrder: 'any' + - + $limit: 4 + - + $project: + _id: 0 + title: 1 + + - + name: 'Token Order sequential' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#simple-sequential-example' + pipeline: + - + $search: + autocomplete: + query: 'men with' + path: 'title' + tokenOrder: 'sequential' + - + $limit: 4 + - + $project: + _id: 0 + title: 1 + + - + name: 'Highlighting' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#highlighting-example' + pipeline: + - + $search: + autocomplete: + query: 'ger' + path: 'title' + highlight: + path: 'title' + - + $limit: 5 + - + $project: + score: + $meta: 'searchScore' + _id: 0 + title: 1 + highlights: + $meta: 'searchHighlights' + + - + name: 'Across Multiple Fields' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#search-across-multiple-fields' + pipeline: + - + $search: + compound: + should: + - + autocomplete: + query: 'inter' + path: 'title' + - + autocomplete: + query: 'inter' + path: 'plot' + minimumShouldMatch: 1 + - + $limit: 10 + - + $project: + _id: 0 + title: 1 + plot: 1 diff --git a/generator/config/search/compound.yaml b/generator/config/search/compound.yaml new file mode 100644 index 000000000..7a1d9f419 --- /dev/null +++ b/generator/config/search/compound.yaml @@ -0,0 +1,156 @@ +# $schema: ../schema.json +name: compound +link: 'https://www.mongodb.com/docs/atlas/atlas-search/compound/' +type: + - searchOperator +encode: object +description: | + The compound operator combines two or more operators into a single query. + Each element of a compound query is called a clause, and each clause + consists of one or more sub-queries. +arguments: + - + name: must + optional: true + type: + - searchOperator + - array # of searchOperator + - + name: mustNot + optional: true + type: + - searchOperator + - array # of searchOperator + - + name: should + optional: true + type: + - searchOperator + - array # of searchOperator + - + name: filter + optional: true + type: + - searchOperator + - array # of searchOperator + - + name: minimumShouldMatch + optional: true + type: + - int + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'must and mustNot' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/compound/#must-and-mustnot-example' + pipeline: + - + $search: + compound: + must: + - + text: + query: 'varieties' + path: 'description' + mustNot: + - + text: + query: 'apples' + path: 'description' + + - + name: 'must and should' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/compound/#must-and-should-example' + pipeline: + - + $search: + compound: + must: + - + text: + query: 'varieties' + path: 'description' + should: + - + text: + query: 'Fuji' + path: 'description' + - + $project: + score: + $meta: 'searchScore' + + - + name: 'minimumShouldMatch' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/compound/#minimumshouldmatch-example' + pipeline: + - + $search: + compound: + must: + - + text: + query: 'varieties' + path: 'description' + should: + - + text: + query: 'Fuji' + path: 'description' + - + text: + query: 'Golden Delicious' + path: 'description' + minimumShouldMatch: 1 + + - + name: 'Filter' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/compound/#filter-examples' + pipeline: + - + $search: + compound: + must: + - + text: + query: 'varieties' + path: 'description' + should: + - + text: + query: 'banana' + path: 'description' + filter: + - + text: + query: 'granny' + path: 'description' + + - + name: 'Nested' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/compound/#nested-example' + pipeline: + - + $search: + compound: + should: + - + text: + query: 'apple' + path: 'type' + - + compound: + must: + - + text: + query: 'organic' + path: 'category' + - + equals: + value: true + path: 'in_stock' + minimumShouldMatch: 1 diff --git a/generator/config/search/embeddedDocument.yaml b/generator/config/search/embeddedDocument.yaml new file mode 100644 index 000000000..19c804625 --- /dev/null +++ b/generator/config/search/embeddedDocument.yaml @@ -0,0 +1,155 @@ +# $schema: ../schema.json +name: embeddedDocument +link: 'https://www.mongodb.com/docs/atlas/atlas-search/embedded-document/' +type: + - searchOperator +encode: object +description: | + The embeddedDocument operator is similar to $elemMatch operator. + It constrains multiple query predicates to be satisfied from a single + element of an array of embedded documents. embeddedDocument can be used only + for queries over fields of the embeddedDocuments +arguments: + - + name: path + type: + - searchPath + - + name: operator + type: + - searchOperator + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Basic' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/embedded-document/#index-definition' + pipeline: + - + $search: + embeddedDocument: + path: 'items' + operator: + compound: + must: + - + text: + path: 'items.tags' + query: 'school' + should: + - + text: + path: 'items.name' + query: 'backpack' + score: + embedded: + aggregate: 'mean' + - + $limit: 5 + - + $project: + _id: 0 + items.name: 1 + items.tags: 1 + score: + $meta: 'searchScore' + + - + name: 'Facet' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/embedded-document/#facet-query' + pipeline: + - + $searchMeta: + facet: + operator: + embeddedDocument: + path: 'items' + operator: + compound: + must: + - + text: + path: 'items.tags' + query: 'school' + should: + - + text: + path: 'items.name' + query: 'backpack' + facets: + purchaseMethodFacet: + type: 'string' + path: 'purchaseMethod' + + - + name: 'Query and Sort' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/embedded-document/#query-and-sort' + pipeline: + - + $search: + embeddedDocument: + path: 'items' + operator: + text: + path: 'items.name' + query: 'laptop' + sort: + items.tags: 1 + - + $limit: 5 + - + $project: + _id: 0 + items.name: 1 + items.tags: 1 + score: + $meta: 'searchScore' + + - + name: 'Query for Matching Embedded Documents Only' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/embedded-document/#query-for-matching-embedded-documents-only' + pipeline: + - + $search: + embeddedDocument: + path: 'items' + operator: + compound: + must: + - + range: + path: 'items.quantity' + gt: 2 + - + exists: + path: 'items.price' + - + text: + path: 'items.tags' + query: 'school' + - + $limit: 2 + - + $project: + _id: 0 + storeLocation: 1 + items: + $filter: + input: '$items' + cond: + $and: + - + $ifNull: + - '$$this.price' + - 'false' + - + $gt: + - '$$this.quantity' + - 2 + - + $in: + - 'office' + - '$$this.tags' diff --git a/generator/config/search/equals.yaml b/generator/config/search/equals.yaml new file mode 100644 index 000000000..b3e50c641 --- /dev/null +++ b/generator/config/search/equals.yaml @@ -0,0 +1,104 @@ +# $schema: ../schema.json +name: equals +link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/' +type: + - searchOperator +encode: object +description: | + The equals operator checks whether a field matches a value you specify. +arguments: + - + name: path + type: + - searchPath + - + name: value + type: + - binData + - bool + - date + - objectId + - 'null' + - number + - string + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Boolean' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/#boolean-examples' + pipeline: + - + $search: + equals: + path: 'verified_user' + value: true + - + $project: + name: 1 + _id: 0 + score: + $meta: 'searchScore' + + - + name: 'ObjectId' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/#objectid-example' + pipeline: + - + $search: + equals: + path: 'teammates' + value: !bson_objectId '5a9427648b0beebeb69589a1' + + - + name: 'Date' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/#date-example' + pipeline: + - + $search: + equals: + path: 'account_created' + value: !bson_utcdatetime '2022-05-04T05:01:08.000+00:00' + + - + name: 'Number' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/#number-example' + pipeline: + - + $search: + equals: + path: 'employee_number' + value: 259 + + - + name: 'String' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/#string-example' + pipeline: + - + $search: + equals: + path: 'name' + value: 'jim hall' + + - + name: 'UUID' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/#uuid-example' + pipeline: + - + $search: + equals: + path: 'uuid' + value: !bson_uuid 'fac32260-b511-4c69-8485-a2be5b7dda9e' + + - + name: 'Null' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/equals/#null-example' + pipeline: + - + $search: + equals: + path: 'job_title' + value: ~ diff --git a/generator/config/search/exists.yaml b/generator/config/search/exists.yaml new file mode 100644 index 000000000..062e8ba59 --- /dev/null +++ b/generator/config/search/exists.yaml @@ -0,0 +1,56 @@ +# $schema: ../schema.json +name: exists +link: 'https://www.mongodb.com/docs/atlas/atlas-search/exists/' +type: + - searchOperator +encode: object +description: | + The exists operator tests if a path to a specified indexed field name exists in a document. +arguments: + - + name: path + type: + - searchPath + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Basic' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/exists/#basic-example' + pipeline: + - + $search: + exists: + path: 'type' + + - + name: 'Embedded' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/exists/#embedded-example' + pipeline: + - + $search: + exists: + path: 'quantities.lemons' + + - + name: 'Compound' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/exists/#compound-example' + pipeline: + - + $search: + compound: + must: + - + exists: + path: 'type' + - + text: + query: 'apple' + path: 'type' + should: + text: + query: 'fuji' + path: 'description' diff --git a/generator/config/search/facet.yaml b/generator/config/search/facet.yaml new file mode 100644 index 000000000..53dc8cba9 --- /dev/null +++ b/generator/config/search/facet.yaml @@ -0,0 +1,56 @@ +# $schema: ../schema.json +name: facet +link: 'https://www.mongodb.com/docs/atlas/atlas-search/facet/' +type: + - searchOperator # should be searchCollector +encode: object +description: | + The facet collector groups results by values or ranges in the specified + faceted fields and returns the count for each of those groups. +arguments: + - + name: facets + type: + - object # map of facetDefinition + - + name: operator + optional: true + type: + - searchOperator +tests: + - + name: 'Facet' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/facet/#examples' + pipeline: + - + $search: + facet: + operator: + near: + path: 'released' + origin: !bson_utcdatetime '1999-07-01T00:00:00.000+00:00' + pivot: 7776000000 + facets: + genresFacet: + type: 'string' + path: 'genres' + - + $limit: 2 + - + $facet: + docs: + - + $project: + title: 1 + released: 1 + meta: + - + $replaceWith: '$$SEARCH_META' + - + $limit: 1 + - + $set: + meta: + $arrayElemAt: + - '$meta' + - 0 diff --git a/generator/config/search/geoShape.yaml b/generator/config/search/geoShape.yaml new file mode 100644 index 000000000..4da121e45 --- /dev/null +++ b/generator/config/search/geoShape.yaml @@ -0,0 +1,123 @@ +# $schema: ../schema.json +name: geoShape +link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoShape/' +type: + - searchOperator +encode: object +description: | + The geoShape operator supports querying shapes with a relation to a given + geometry if indexShapes is set to true in the index definition. +arguments: + - + name: path + type: + - searchPath + - + name: relation + type: + - string # contains | disjoint | intersects | within + - + name: geometry + type: + - geometry + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Disjoint' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoShape/#disjoint-example' + pipeline: + - + $search: + geoShape: + relation: 'disjoint' + geometry: + type: 'Polygon' + coordinates: + - + - [-161.323242, 22.512557] + - [-152.446289, 22.065278] + - [-156.09375, 17.811456] + - [-161.323242, 22.512557] + path: 'address.location' + - + $limit: 3 + - + $project: + _id: 0 + name: 1 + address: 1 + score: + $meta: 'searchScore' + + - + name: 'Intersect' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoShape/#intersects-example' + pipeline: + - + $search: + geoShape: + relation: 'intersects' + geometry: + type: 'MultiPolygon' + coordinates: + - + - + - [2.16942, 41.40082] + - [2.17963, 41.40087] + - [2.18146, 41.39716] + - [2.15533, 41.40686] + - [2.14596, 41.38475] + - [2.17519, 41.41035] + - [2.16942, 41.40082] + - + - + - [2.16365, 41.39416] + - [2.16963, 41.39726] + - [2.15395, 41.38005] + - [2.17935, 41.43038] + - [2.16365, 41.39416] + path: 'address.location' + - + $limit: 3 + - + $project: + _id: 0 + name: 1 + address: 1 + score: + $meta: 'searchScore' + + - + name: 'Within' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoShape/#within-example' + pipeline: + - + $search: + geoShape: + relation: 'within' + geometry: + type: 'Polygon' + coordinates: + - + - [-74.3994140625, 40.5305017757] + - [-74.7290039063, 40.5805846641] + - [-74.7729492188, 40.9467136651] + - [-74.0698242188, 41.1290213475] + - [-73.65234375, 40.9964840144] + - [-72.6416015625, 40.9467136651] + - [-72.3559570313, 40.7971774152] + - [-74.3994140625, 40.5305017757] + path: 'address.location' + - + $limit: 3 + - + $project: + _id: 0 + name: 1 + address: 1 + score: + $meta: 'searchScore' diff --git a/generator/config/search/geoWithin.yaml b/generator/config/search/geoWithin.yaml new file mode 100644 index 000000000..1739f1997 --- /dev/null +++ b/generator/config/search/geoWithin.yaml @@ -0,0 +1,103 @@ +# $schema: ../schema.json +name: geoWithin +link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoWithin/' +type: + - searchOperator +encode: object +description: | + The geoWithin operator supports querying geographic points within a given + geometry. Only points are returned, even if indexShapes value is true in + the index definition. +arguments: + - + name: path + type: + - searchPath + - + name: box + optional: true + type: + - object + - + name: circle + optional: true + type: + - object + - + name: geometry + optional: true + type: + - geometry + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'box' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoWithin/#box-example' + pipeline: + - + $search: + geoWithin: + path: 'address.location' + box: + bottomLeft: + type: 'Point' + coordinates: [112.467, -55.05] + topRight: + type: 'Point' + coordinates: [168, -9.133] + - + $limit: 3 + - + $project: + _id: 0 + name: 1 + address: 1 + + - + name: 'circle' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoWithin/#circle-example' + pipeline: + - + $search: + geoWithin: + circle: + center: + type: 'Point' + coordinates: [-73.54, 45.54] + radius: 1600 + path: 'address.location' + - + $limit: 3 + - + $project: + _id: 0 + name: 1 + address: 1 + + - + name: 'geometry' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/geoWithin/#geometry-examples' + pipeline: + - + $search: + geoWithin: + geometry: + type: 'Polygon' + coordinates: + - + - [-161.323242, 22.512557] + - [-152.446289, 22.065278] + - [-156.09375, 17.811456] + - [-161.323242, 22.512557] + path: 'address.location' + - + $limit: 3 + - + $project: + _id: 0 + name: 1 + address: 1 diff --git a/generator/config/search/in.yaml b/generator/config/search/in.yaml new file mode 100644 index 000000000..cc1aa6c33 --- /dev/null +++ b/generator/config/search/in.yaml @@ -0,0 +1,89 @@ +# $schema: ../schema.json +name: in +link: 'https://www.mongodb.com/docs/atlas/atlas-search/in/' +type: + - searchOperator +encode: object +description: | + The in operator performs a search for an array of BSON values in a field. +arguments: + - + name: path + type: + - searchPath + - + name: value + type: + - any + - array # of any + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Single Value Field Match' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/in/#examples' + pipeline: + - + $search: + in: + path: 'birthdate' + value: + - !bson_utcdatetime '1977-03-02T02:20:31.000+00:00' + - !bson_utcdatetime '1977-03-01T00:00:00.000+00:00' + - !bson_utcdatetime '1977-05-06T21:57:35.000+00:00' + - + $project: + _id: 0 + name: 1 + birthdate: 1 + + - + name: 'Array Value Field Match' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/in/#examples' + pipeline: + - + $search: + in: + path: 'accounts' + value: + - 371138 + - 371139 + - 371140 + - + $project: + _id: 0 + name: 1 + accounts: 1 + + - + name: 'Compound Query Match' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/in/#examples' + pipeline: + - + $search: + compound: + must: + - + in: + path: 'name' + value: + - 'james sanchez' + - 'jennifer lawrence' + should: + - + in: + path: '_id' + value: + - !bson_objectId '5ca4bbcea2dd94ee58162a72' + - !bson_objectId '5ca4bbcea2dd94ee58162a91' + - + $limit: 5 + - + $project: + _id: 1 + name: 1 + score: + $meta: 'searchScore' diff --git a/generator/config/search/moreLikeThis.yaml b/generator/config/search/moreLikeThis.yaml new file mode 100644 index 000000000..8c4803bdd --- /dev/null +++ b/generator/config/search/moreLikeThis.yaml @@ -0,0 +1,99 @@ +# $schema: ../schema.json +name: moreLikeThis +link: 'https://www.mongodb.com/docs/atlas/atlas-search/moreLikeThis/' +type: + - searchOperator +encode: object +description: | + The moreLikeThis operator returns documents similar to input documents. + The moreLikeThis operator allows you to build features for your applications + that display similar or alternative results based on one or more given documents. +arguments: + - + name: like + type: + - object + - array # of object + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Single Document with Multiple Fields' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/morelikethis/#example-1--single-document-with-multiple-fields' + pipeline: + - + $search: + moreLikeThis: + like: + title: 'The Godfather' + genres: 'action' + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + released: 1 + genres: 1 + + - + name: 'Input Document Excluded in Results' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/morelikethis/#example-2--input-document-excluded-in-results' + pipeline: + - + $search: + compound: + must: + - + moreLikeThis: + like: + _id: !bson_objectId '573a1396f29313caabce4a9a' + genres: + - 'Crime' + - 'Drama' + title: 'The Godfather' + mustNot: + - + equals: + path: '_id' + value: !bson_objectId '573a1396f29313caabce4a9a' + - + $limit: 5 + - + $project: + _id: 1 + title: 1 + released: 1 + genres: 1 + + - + name: 'Multiple Analyzers' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/morelikethis/#example-3--multiple-analyzers' + pipeline: + - + $search: + compound: + should: + - + moreLikeThis: + like: + _id: !bson_objectId '573a1396f29313caabce4a9a' + genres: + - 'Crime' + - 'Drama' + title: 'The Godfather' + mustNot: + - + equals: + path: '_id' + value: !bson_objectId '573a1394f29313caabcde9ef' + - + $limit: 10 + - + $project: + title: 1 + genres: 1 + _id: 1 diff --git a/generator/config/search/near.yaml b/generator/config/search/near.yaml new file mode 100644 index 000000000..bd4119cf9 --- /dev/null +++ b/generator/config/search/near.yaml @@ -0,0 +1,124 @@ +# $schema: ../schema.json +name: near +link: 'https://www.mongodb.com/docs/atlas/atlas-search/near/' +type: + - searchOperator +encode: object +description: | + The near operator supports querying and scoring numeric, date, and GeoJSON point values. +arguments: + - + name: path + type: + - searchPath + - + name: origin + type: + - date + - number + - geometry + - + name: pivot + type: + - number + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Number' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/near/#number-example' + pipeline: + - + $search: + index: 'runtimes' + near: + path: 'runtime' + origin: 279 + pivot: 2 + - + $limit: 7 + - + $project: + _id: 0 + title: 1 + runtime: 1 + score: + $meta: 'searchScore' + + - + name: 'Date' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/near/#date-example' + pipeline: + - + $search: + index: 'releaseddate' + near: + path: 'released' + origin: !bson_utcdatetime '1915-09-13T00:00:00.000+00:00' + pivot: 7776000000 + - + $limit: 3 + - + $project: + _id: 0 + title: 1 + released: 1 + score: + $meta: 'searchScore' + + - + name: 'GeoJSON Point' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/near/#geojson-point-examples' + pipeline: + - + $search: + near: + origin: + type: 'Point' + coordinates: + - -8.61308 + - 41.1413 + pivot: 1000 + path: 'address.location' + - + $limit: 3 + - + $project: + _id: 0 + name: 1 + address: 1 + score: + $meta: 'searchScore' + + - + name: 'Compound' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/near/#compound-example' + pipeline: + - + $search: + compound: + must: + text: + query: 'Apartment' + path: 'property_type' + should: + near: + origin: + type: 'Point' + coordinates: + - 114.15027 + - 22.28158 + pivot: 1000 + path: 'address.location' + - + $limit: 3 + - + $project: + _id: 0 + property_type: 1 + address: 1 + score: + $meta: 'searchScore' diff --git a/generator/config/search/phrase.yaml b/generator/config/search/phrase.yaml new file mode 100644 index 000000000..4d9b75c4e --- /dev/null +++ b/generator/config/search/phrase.yaml @@ -0,0 +1,109 @@ +# $schema: ../schema.json +name: phrase +link: 'https://www.mongodb.com/docs/atlas/atlas-search/phrase/' +type: + - searchOperator +encode: object +description: | + The phrase operator performs search for documents containing an ordered sequence of terms using the analyzer specified in the index configuration. +arguments: + - + name: path + type: + - searchPath + - + name: query + type: + - string + - array # of string + - + name: slop + optional: true + type: + - int + - + name: synonyms + optional: true + type: + - string + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Single Phrase' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/phrase/#single-phrase-example' + pipeline: + - + $search: + phrase: + path: 'title' + query: 'new york' + - + $limit: 10 + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Multiple Phrase' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/phrase/#multiple-phrases-example' + pipeline: + - + $search: + phrase: + path: 'title' + query: + - 'the man' + - 'the moon' + - + $limit: 10 + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Phrase Slop' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/phrase/#slop-example' + pipeline: + - + $search: + phrase: + path: 'title' + query: 'men women' + slop: 5 + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Phrase Synonyms' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/phrase/#synonyms-example' + pipeline: + - + $search: + phrase: + path: 'plot' + query: 'automobile race' + slop: 5 + synonyms: 'my_synonyms' + - + $limit: 5 + - + $project: + _id: 0 + plot: 1 + title: 1 + score: + $meta: 'searchScore' diff --git a/generator/config/search/queryString.yaml b/generator/config/search/queryString.yaml new file mode 100644 index 000000000..8202771c9 --- /dev/null +++ b/generator/config/search/queryString.yaml @@ -0,0 +1,35 @@ +# $schema: ../schema.json +name: queryString +link: 'https://www.mongodb.com/docs/atlas/atlas-search/queryString/' +type: + - searchOperator +encode: object +description: | + +arguments: + - + name: defaultPath + type: + - searchPath + - + name: query + type: + - string + +# The various example from the doc are variations of the "query" parameter +# this is not pertinent for testing the aggregation builder, unless we create +# a queryString builder. +tests: + - + name: 'Boolean Operator Queries' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/queryString/#boolean-operator-queries' + pipeline: + - + $search: + queryString: + defaultPath: 'title' + query: 'Rocky AND (IV OR 4 OR Four)' + - + $project: + _id: 0 + title: 1 diff --git a/generator/config/search/range.yaml b/generator/config/search/range.yaml new file mode 100644 index 000000000..f42c69176 --- /dev/null +++ b/generator/config/search/range.yaml @@ -0,0 +1,139 @@ +# $schema: ../schema.json +name: range +link: 'https://www.mongodb.com/docs/atlas/atlas-search/range/' +type: + - searchOperator +encode: object +description: | + The range operator supports querying and scoring numeric, date, and string values. + You can use this operator to find results that are within a given numeric, date, objectId, or letter (from the English alphabet) range. +arguments: + - + name: path + type: + - searchPath + - + name: gt + optional: true + type: + - date + - number + - string + - objectId + - + name: gte + optional: true + type: + - date + - number + - string + - objectId + - + name: lt + optional: true + type: + - date + - number + - string + - objectId + - + name: lte + optional: true + type: + - date + - number + - string + - objectId + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Number gte lte' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/range/#number-example' + pipeline: + - + $search: + range: + path: 'runtime' + gte: 2 + lte: 3 + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + runtime: 1 + + - + name: 'Number lte' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/range/#number-example' + pipeline: + - + $search: + range: + path: 'runtime' + lte: 2 + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + runtime: 1 + score: + $meta: 'searchScore' + + - + name: 'Date' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/range/#date-example' + pipeline: + - + $search: + range: + path: 'released' + gt: !bson_utcdatetime '2010-01-01T00:00:00.000Z' + lt: !bson_utcdatetime '2015-01-01T00:00:00.000Z' + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + released: 1 + + - + name: 'ObjectId' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/range/#objectid-example' + pipeline: + - + $search: + range: + path: '_id' + gte: !bson_objectId '573a1396f29313caabce4a9a' + lte: !bson_objectId '573a1396f29313caabce4ae7' + - + $project: + _id: 1 + title: 1 + released: 1 + + - + name: 'String' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/range/#string-example' + pipeline: + - + $search: + range: + path: 'title' + gt: 'city' + lt: 'country' + - + $limit: 5 + - + $project: + _id: 0 + title: 1 diff --git a/generator/config/search/regex.yaml b/generator/config/search/regex.yaml new file mode 100644 index 000000000..869ffabde --- /dev/null +++ b/generator/config/search/regex.yaml @@ -0,0 +1,42 @@ +# $schema: ../schema.json +name: regex +link: 'https://www.mongodb.com/docs/atlas/atlas-search/regex/' +type: + - searchOperator +encode: object +description: | + regex interprets the query field as a regular expression. + regex is a term-level operator, meaning that the query field isn't analyzed. +arguments: + - + name: path + type: + - searchPath + - + name: query + type: + - string + - + name: allowAnalyzedField + optional: true + type: + - bool + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Regex' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/regex/#examples' + pipeline: + - + $search: + regex: + path: 'title' + query: '[0-9]{2} (.){4}s' + - + $project: + _id: 0 + title: 1 diff --git a/generator/config/search/text.yaml b/generator/config/search/text.yaml new file mode 100644 index 000000000..dbd48cdd0 --- /dev/null +++ b/generator/config/search/text.yaml @@ -0,0 +1,194 @@ +# $schema: ../schema.json +name: text +link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/' +type: + - searchOperator +encode: object +description: | + The text operator performs a full-text search using the analyzer that you specify in the index configuration. + If you omit an analyzer, the text operator uses the default standard analyzer. +arguments: + - + name: path + type: + - searchPath + - + name: query + type: + - string + - + name: fuzzy + optional: true + type: + - object + - + name: matchCriteria + optional: true + type: + - string # "any" | "all" + - + name: synonyms + optional: true + type: + - string + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Basic' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/#basic-example' + pipeline: + - + $search: + text: + path: 'title' + query: 'surfer' + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' + - + name: 'Fuzzy Default' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/#fuzzy-examples' + pipeline: + - + $search: + text: + path: 'title' + query: 'naw yark' + fuzzy: {} + - + $limit: 10 + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Fuzzy maxExpansions' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/#fuzzy-examples' + pipeline: + - + $search: + text: + path: 'title' + query: 'naw yark' + fuzzy: + maxEdits: 1 + maxExpansions: 100 + - + $limit: 10 + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Fuzzy prefixLength' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/#fuzzy-examples' + pipeline: + - + $search: + text: + path: 'title' + query: 'naw yark' + fuzzy: + maxEdits: 1 + prefixLength: 2 + - + $limit: 8 + - + $project: + _id: 1 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Match any Using equivalent Mapping' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/#match-any-using-equivalent-mapping' + pipeline: + - + $search: + text: + path: 'plot' + query: 'attire' + synonyms: 'my_synonyms' + matchCriteria: 'any' + - + $limit: 5 + - + $project: + _id: 0 + plot: 1 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Match any Using explicit Mapping' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/#match-any-using-explicit-mapping' + pipeline: + - + $search: + text: + path: 'plot' + query: 'boat race' + synonyms: 'my_synonyms' + matchCriteria: 'any' + - + $limit: 10 + - + $project: + _id: 0 + plot: 1 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Match all Using Synonyms' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/#match-all-using-synonyms' + pipeline: + - + $search: + text: + path: 'plot' + query: 'automobile race' + matchCriteria: 'all' + synonyms: 'my_synonyms' + - + $limit: 20 + - + $project: + _id: 0 + plot: 1 + title: 1 + score: + $meta: 'searchScore' + + - + name: 'Wildcard Path' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/text/' + pipeline: + - + $search: + text: + path: + wildcard: '*' + query: 'surfer' + - + $project: + _id: 0 + title: 1 + score: + $meta: 'searchScore' diff --git a/generator/config/search/wildcard.yaml b/generator/config/search/wildcard.yaml new file mode 100644 index 000000000..d17fb4803 --- /dev/null +++ b/generator/config/search/wildcard.yaml @@ -0,0 +1,60 @@ +# $schema: ../schema.json +name: wildcard +link: 'https://www.mongodb.com/docs/atlas/atlas-search/wildcard/' +type: + - searchOperator +encode: object +description: | + The wildcard operator enables queries which use special characters in the search string that can match any character. +arguments: + - + name: path + type: + - searchPath + - + name: query + type: + - string + - + name: allowAnalyzedField + optional: true + type: + - bool + - + name: score + optional: true + type: + - searchScore +tests: + - + name: 'Wildcard Path' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/wildcard/#index-definition' + pipeline: + - + $search: + wildcard: + query: 'Wom?n *' + path: + wildcard: '*' + - + $limit: 5 + - + $project: + _id: 0 + title: 1 + + - + name: 'Escape Character Example' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/wildcard/#escape-character-example' + pipeline: + - + $search: + wildcard: + query: '*\?' + path: 'title' + - + $limit: 5 + - + $project: + _id: 0 + title: 1 diff --git a/generator/config/stage/search.yaml b/generator/config/stage/search.yaml index 2531e75f6..34bf57a34 100644 --- a/generator/config/stage/search.yaml +++ b/generator/config/stage/search.yaml @@ -3,16 +3,93 @@ name: $search link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/search/' type: - stage -encode: single +encode: object description: | Performs a full-text search of the field or fields in an Atlas collection. NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments. arguments: - - name: search + name: operator + mergeObject: true + type: + - searchOperator + description: | + Operator to search with. You can provide a specific operator or use + the compound operator to run a compound query with multiple operators. + - + name: index + optional: true + type: + - string + description: | + Name of the Atlas Search index to use. If omitted, defaults to "default". + - + name: highlight + optional: true + type: + # @todo support "highlight" type object + # https://www.mongodb.com/docs/atlas/atlas-search/highlighting/ + - object + description: | + Specifies the highlighting options for displaying search terms in their original context. + - + name: concurrent + optional: true + type: + - bool + description: | + 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. + - + name: count + optional: true + type: + - string + description: | + Document that specifies the count options for retrieving a count of the results. + - + name: searchAfter + optional: true + type: + - string + description: | + Reference point for retrieving results. searchAfter returns documents starting immediately following the specified reference point. + - + name: searchBefore + optional: true + type: + - string + description: | + Reference point for retrieving results. searchBefore returns documents starting immediately before the specified reference point. + - + name: scoreDetails + optional: true + type: + - bool + description: | + Flag that specifies whether to retrieve a detailed breakdown of the score for the documents in the results. If omitted, defaults to false. + - + name: sort + optional: true + type: + - object + description: | + Document that specifies the fields to sort the Atlas Search results by in ascending or descending order. + - + name: returnStoredSource + optional: true + type: + - bool + description: | + Flag that specifies whether to perform a full document lookup on the backend database or return only stored source fields directly from Atlas Search. + - + name: tracking + optional: true type: - object - + description: | + Document that specifies the tracking option to retrieve analytics information on the search terms. tests: - name: 'Example' diff --git a/generator/config/stage/searchMeta.yaml b/generator/config/stage/searchMeta.yaml index 322d048eb..a7d92c272 100644 --- a/generator/config/stage/searchMeta.yaml +++ b/generator/config/stage/searchMeta.yaml @@ -3,16 +3,34 @@ name: $searchMeta link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/searchMeta/' type: - stage -encode: single +encode: object description: | Returns different types of metadata result documents for the Atlas Search query against an Atlas collection. NOTE: $searchMeta is only available for MongoDB Atlas clusters running MongoDB v4.4.9 or higher, and is not available for self-managed deployments. arguments: - - name: meta + name: operator + mergeObject: true type: - - object + - searchOperator + description: | + Operator to search with. You can provide a specific operator or use + the compound operator to run a compound query with multiple operators. + - + name: index + optional: true + type: + - string + description: | + Name of the Atlas Search index to use. If omitted, defaults to default. + - + name: count + optional: true + type: + - object + description: | + Document that specifies the count options for retrieving a count of the results. tests: - name: 'Example' @@ -26,3 +44,89 @@ tests: lt: 1999 count: type: 'total' + + - + name: 'Year Facet' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/facet/#example-1' + pipeline: + - $searchMeta: + facet: + operator: + range: + path: 'year' + gte: 1980 + lte: 2000 + facets: + yearFacet: + type: 'number' + path: 'year' + boundaries: + - 1980 + - 1990 + - 2000 + default: 'other' + + - + name: 'Date Facet' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/facet/#example-2' + pipeline: + - + $searchMeta: + facet: + operator: + range: + path: 'released' + gte: !bson_utcdatetime '2000-01-01T00:00:00.000Z' + lte: !bson_utcdatetime '2015-01-31T00:00:00.000Z' + facets: + yearFacet: + type: 'date' + path: 'released' + boundaries: + - !bson_utcdatetime '2000-01-01' + - !bson_utcdatetime '2005-01-01' + - !bson_utcdatetime '2010-01-01' + - !bson_utcdatetime '2015-01-01' + default: 'other' + + - + name: 'Metadata Results' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/facet/#examples' + pipeline: + - + $searchMeta: + facet: + operator: + range: + path: 'released' + gte: !bson_utcdatetime '2000-01-01T00:00:00.000Z' + lte: !bson_utcdatetime '2015-01-31T00:00:00.000Z' + facets: + directorsFacet: + type: 'string' + path: 'directors' + numBuckets: 7 + yearFacet: + type: 'number' + path: 'year' + boundaries: + - 2000 + - 2005 + - 2010 + - 2015 + + - + name: 'Autocomplete Bucket Results through Facet Queries' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#bucket-results-through-facet-queries' + pipeline: + - + $searchMeta: + facet: + operator: + autocomplete: + path: 'title' + query: 'Gravity' + facets: + titleFacet: + type: 'string' + path: 'title' diff --git a/generator/config/stage/vectorSearch.yaml b/generator/config/stage/vectorSearch.yaml new file mode 100644 index 000000000..ec4b4ff4b --- /dev/null +++ b/generator/config/stage/vectorSearch.yaml @@ -0,0 +1,119 @@ +# $schema: ../schema.json +name: $vectorSearch +link: 'https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/' +type: + - stage +encode: object +description: | + The $vectorSearch stage performs an ANN or ENN search on a vector in the specified field. +arguments: + - + name: index + type: + - string + description: | + Name of the Atlas Vector Search index to use. + - + name: limit + type: + - int + description: | + Number of documents to return in the results. This value can't exceed the value of numCandidates if you specify numCandidates. + - + name: path + type: + - searchPath + description: | + Indexed vector type field to search. + - + name: queryVector + type: + - array # of numbers + description: | + Array of numbers that represent the query vector. The number type must match the indexed field value type. + - + name: exact + optional: true + type: + - bool + description: | + This is required if numCandidates is omitted. false to run ANN search. true to run ENN search. + - + name: filter + optional: true + type: + - query + description: | + Any match query that compares an indexed field with a boolean, date, objectId, number (not decimals), string, or UUID to use as a pre-filter. + - + name: numCandidates + optional: true + type: + - int + description: | + This field is required if exact is false or omitted. + Number of nearest neighbors to use during the search. Value must be less than or equal to (<=) 10000. You can't specify a number less than the number of documents to return (limit). + +tests: + - + name: 'ANN Basic' + link: 'https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#ann-examples' + pipeline: + - + $vectorSearch: + index: 'vector_index' + path: 'plot_embedding' + queryVector: [-0.0016261312, -0.028070757, -0.011342932] # skip other numbers, not relevant to the test + numCandidates: 150 + limit: 10 + - + $project: + _id: 0 + plot: 1 + title: 1 + score: + $meta: 'vectorSearchScore' + + - + name: 'ANN Filter' + link: 'https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#ann-examples' + pipeline: + - + $vectorSearch: + index: 'vector_index' + path: 'plot_embedding' + filter: + $and: + - + year: + $lt: 1975 + queryVector: [0.02421053, -0.022372592, -0.006231137] # skip other numbers, not relevant to the test + numCandidates: 150 + limit: 10 + - + $project: + _id: 0 + title: 1 + plot: 1 + year: 1 + score: + $meta: 'vectorSearchScore' + + - + name: 'ENN' + link: 'https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#enn-examples' + pipeline: + - + $vectorSearch: + index: 'vector_index' + path: 'plot_embedding' + queryVector: [-0.006954097, -0.009932499, -0.001311474] # skip other numbers, not relevant to the test + exact: true + limit: 10 + - + $project: + _id: 0 + plot: 1 + title: 1 + score: + $meta: 'vectorSearchScore' diff --git a/generator/js2yaml.html b/generator/js2yaml.html index a0f8654af..31241adb1 100644 --- a/generator/js2yaml.html +++ b/generator/js2yaml.html @@ -28,6 +28,9 @@

Convert JS examples into Yaml

binary: BinData(0, "IA=="), empty_object: {}, empty_array: [], + objectId: ObjectId("5f0f1d9b1c9d440000f3f3f3"), + uuid: UUID("fac32260-b511-4c69-8485-a2be5b7dda9e"), + list: [1, 2, 3], } }, // First Stage @@ -90,6 +93,14 @@

Convert JS examples into Yaml

return new TaggedValue('bson_int64', value) } + function ObjectId(value) { + return new TaggedValue('bson_objectId', value) + } + + function UUID(value) { + return new TaggedValue('bson_uuid', value) + } + function convert(jsString) { try { return toYaml(eval(jsString), 1); @@ -109,6 +120,11 @@

Convert JS examples into Yaml

return ' []'; } + // Inline list of numbers + if (object.every(item => typeof item === 'number')) { + return ' [' + object.join(', ') + ']'; + } + return newline + '-' + object.map((item) => toYaml(item, indent + 1)).join(newline + '-'); } diff --git a/generator/src/OperatorTestGenerator.php b/generator/src/OperatorTestGenerator.php index a8e6e91cc..f665b9210 100644 --- a/generator/src/OperatorTestGenerator.php +++ b/generator/src/OperatorTestGenerator.php @@ -10,6 +10,7 @@ use MongoDB\BSON\Decimal128; use MongoDB\BSON\Document; use MongoDB\BSON\Int64; +use MongoDB\BSON\ObjectId; use MongoDB\BSON\Regex; use MongoDB\BSON\UTCDateTime; use MongoDB\Builder\Pipeline; @@ -27,6 +28,7 @@ use function base64_decode; use function basename; use function get_object_vars; +use function hex2bin; use function is_array; use function is_numeric; use function is_object; @@ -145,6 +147,8 @@ private function convertYamlTaggedValues(mixed $object): mixed 'bson_decimal128' => new Decimal128($value), 'bson_utcdatetime' => new UTCDateTime(is_numeric($value) ? $value : new DateTimeImmutable($value)), 'bson_binary' => new Binary(base64_decode($value)), + 'bson_objectId' => new ObjectId($value), + 'bson_uuid' => new Binary(hex2bin(str_replace('-', '', $value)), Binary::TYPE_UUID), default => throw new InvalidArgumentException(sprintf('Yaml tag "%s" is not supported.', $object->getTag())), }; } @@ -158,6 +162,7 @@ private function convertYamlTaggedValues(mixed $object): mixed } if (is_object($object)) { + $object = clone $object; foreach (get_object_vars($object) as $key => $value) { $object->{$key} = $this->convertYamlTaggedValues($value); } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 0d349c0d9..7b0912775 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -19,7 +19,7 @@ rector.php - src/Builder/(Accumulator|Expression|Query|Projection|Stage)/*\.php + src/Builder/(Accumulator|Expression|Query|Projection|Search|Stage)/*\.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 301c049ac..321bbc05a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -172,6 +172,11 @@ + + + + + diff --git a/src/Builder/Search.php b/src/Builder/Search.php new file mode 100644 index 000000000..7cab84641 --- /dev/null +++ b/src/Builder/Search.php @@ -0,0 +1,10 @@ + 'path', + 'query' => 'query', + 'tokenOrder' => 'tokenOrder', + 'fuzzy' => 'fuzzy', + 'score' => 'score', + ]; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var string $query */ + public readonly string $query; + + /** @var Optional|string $tokenOrder */ + public readonly Optional|string $tokenOrder; + + /** @var Optional|Document|Serializable|array|stdClass $fuzzy */ + public readonly Optional|Document|Serializable|stdClass|array $fuzzy; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param string $query + * @param Optional|string $tokenOrder + * @param Optional|Document|Serializable|array|stdClass $fuzzy + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + string $query, + Optional|string $tokenOrder = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $fuzzy = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->query = $query; + $this->tokenOrder = $tokenOrder; + $this->fuzzy = $fuzzy; + $this->score = $score; + } +} diff --git a/src/Builder/Search/CompoundOperator.php b/src/Builder/Search/CompoundOperator.php new file mode 100644 index 000000000..27b8c2aeb --- /dev/null +++ b/src/Builder/Search/CompoundOperator.php @@ -0,0 +1,84 @@ + 'must', + 'mustNot' => 'mustNot', + 'should' => 'should', + 'filter' => 'filter', + 'minimumShouldMatch' => 'minimumShouldMatch', + 'score' => 'score', + ]; + + /** @var Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $must */ + public readonly Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $must; + + /** @var Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $mustNot */ + public readonly Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $mustNot; + + /** @var Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $should */ + public readonly Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $should; + + /** @var Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $filter */ + public readonly Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $filter; + + /** @var Optional|int $minimumShouldMatch */ + public readonly Optional|int $minimumShouldMatch; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $must + * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $mustNot + * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $should + * @param Optional|BSONArray|Document|PackedArray|SearchOperatorInterface|Serializable|array|stdClass $filter + * @param Optional|int $minimumShouldMatch + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $must = Optional::Undefined, + Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $mustNot = Optional::Undefined, + Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $should = Optional::Undefined, + Optional|Document|PackedArray|Serializable|SearchOperatorInterface|BSONArray|stdClass|array $filter = Optional::Undefined, + Optional|int $minimumShouldMatch = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->must = $must; + $this->mustNot = $mustNot; + $this->should = $should; + $this->filter = $filter; + $this->minimumShouldMatch = $minimumShouldMatch; + $this->score = $score; + } +} diff --git a/src/Builder/Search/EmbeddedDocumentOperator.php b/src/Builder/Search/EmbeddedDocumentOperator.php new file mode 100644 index 000000000..91f6c1f96 --- /dev/null +++ b/src/Builder/Search/EmbeddedDocumentOperator.php @@ -0,0 +1,57 @@ + 'path', 'operator' => 'operator', 'score' => 'score']; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var Document|SearchOperatorInterface|Serializable|array|stdClass $operator */ + public readonly Document|Serializable|SearchOperatorInterface|stdClass|array $operator; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param Document|SearchOperatorInterface|Serializable|array|stdClass $operator + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + Document|Serializable|SearchOperatorInterface|stdClass|array $operator, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->operator = $operator; + $this->score = $score; + } +} diff --git a/src/Builder/Search/EqualsOperator.php b/src/Builder/Search/EqualsOperator.php new file mode 100644 index 000000000..bbfe75b8e --- /dev/null +++ b/src/Builder/Search/EqualsOperator.php @@ -0,0 +1,59 @@ + 'path', 'value' => 'value', 'score' => 'score']; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var Binary|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value */ + public readonly Binary|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param Binary|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + Binary|Decimal128|Int64|ObjectId|UTCDateTime|bool|float|int|null|string $value, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->value = $value; + $this->score = $score; + } +} diff --git a/src/Builder/Search/ExistsOperator.php b/src/Builder/Search/ExistsOperator.php new file mode 100644 index 000000000..4330f130f --- /dev/null +++ b/src/Builder/Search/ExistsOperator.php @@ -0,0 +1,48 @@ + 'path', 'score' => 'score']; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->score = $score; + } +} diff --git a/src/Builder/Search/FacetOperator.php b/src/Builder/Search/FacetOperator.php new file mode 100644 index 000000000..15fc10830 --- /dev/null +++ b/src/Builder/Search/FacetOperator.php @@ -0,0 +1,49 @@ + 'facets', 'operator' => 'operator']; + + /** @var Document|Serializable|array|stdClass $facets */ + public readonly Document|Serializable|stdClass|array $facets; + + /** @var Optional|Document|SearchOperatorInterface|Serializable|array|stdClass $operator */ + public readonly Optional|Document|Serializable|SearchOperatorInterface|stdClass|array $operator; + + /** + * @param Document|Serializable|array|stdClass $facets + * @param Optional|Document|SearchOperatorInterface|Serializable|array|stdClass $operator + */ + public function __construct( + Document|Serializable|stdClass|array $facets, + Optional|Document|Serializable|SearchOperatorInterface|stdClass|array $operator = Optional::Undefined, + ) { + $this->facets = $facets; + $this->operator = $operator; + } +} diff --git a/src/Builder/Search/FactoryTrait.php b/src/Builder/Search/FactoryTrait.php new file mode 100644 index 000000000..3a7762521 --- /dev/null +++ b/src/Builder/Search/FactoryTrait.php @@ -0,0 +1,345 @@ + 'path', 'relation' => 'relation', 'geometry' => 'geometry', 'score' => 'score']; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var string $relation */ + public readonly string $relation; + + /** @var Document|GeometryInterface|Serializable|array|stdClass $geometry */ + public readonly Document|Serializable|GeometryInterface|stdClass|array $geometry; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param string $relation + * @param Document|GeometryInterface|Serializable|array|stdClass $geometry + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + string $relation, + Document|Serializable|GeometryInterface|stdClass|array $geometry, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->relation = $relation; + $this->geometry = $geometry; + $this->score = $score; + } +} diff --git a/src/Builder/Search/GeoWithinOperator.php b/src/Builder/Search/GeoWithinOperator.php new file mode 100644 index 000000000..dcf605697 --- /dev/null +++ b/src/Builder/Search/GeoWithinOperator.php @@ -0,0 +1,76 @@ + 'path', + 'box' => 'box', + 'circle' => 'circle', + 'geometry' => 'geometry', + 'score' => 'score', + ]; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var Optional|Document|Serializable|array|stdClass $box */ + public readonly Optional|Document|Serializable|stdClass|array $box; + + /** @var Optional|Document|Serializable|array|stdClass $circle */ + public readonly Optional|Document|Serializable|stdClass|array $circle; + + /** @var Optional|Document|GeometryInterface|Serializable|array|stdClass $geometry */ + public readonly Optional|Document|Serializable|GeometryInterface|stdClass|array $geometry; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param Optional|Document|Serializable|array|stdClass $box + * @param Optional|Document|Serializable|array|stdClass $circle + * @param Optional|Document|GeometryInterface|Serializable|array|stdClass $geometry + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + Optional|Document|Serializable|stdClass|array $box = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $circle = Optional::Undefined, + Optional|Document|Serializable|GeometryInterface|stdClass|array $geometry = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->box = $box; + $this->circle = $circle; + $this->geometry = $geometry; + $this->score = $score; + } +} diff --git a/src/Builder/Search/InOperator.php b/src/Builder/Search/InOperator.php new file mode 100644 index 000000000..f050a1679 --- /dev/null +++ b/src/Builder/Search/InOperator.php @@ -0,0 +1,65 @@ + 'path', 'value' => 'value', 'score' => 'score']; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var BSONArray|PackedArray|Type|array|bool|float|int|null|stdClass|string $value */ + public readonly PackedArray|Type|BSONArray|stdClass|array|bool|float|int|null|string $value; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param BSONArray|PackedArray|Type|array|bool|float|int|null|stdClass|string $value + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + PackedArray|Type|BSONArray|stdClass|array|bool|float|int|null|string $value, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + if (is_array($value) && ! array_is_list($value)) { + throw new InvalidArgumentException('Expected $value argument to be a list, got an associative array.'); + } + + $this->value = $value; + $this->score = $score; + } +} diff --git a/src/Builder/Search/MoreLikeThisOperator.php b/src/Builder/Search/MoreLikeThisOperator.php new file mode 100644 index 000000000..689508d53 --- /dev/null +++ b/src/Builder/Search/MoreLikeThisOperator.php @@ -0,0 +1,52 @@ + 'like', 'score' => 'score']; + + /** @var BSONArray|Document|PackedArray|Serializable|array|stdClass $like */ + public readonly Document|PackedArray|Serializable|BSONArray|stdClass|array $like; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param BSONArray|Document|PackedArray|Serializable|array|stdClass $like + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + Document|PackedArray|Serializable|BSONArray|stdClass|array $like, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->like = $like; + $this->score = $score; + } +} diff --git a/src/Builder/Search/NearOperator.php b/src/Builder/Search/NearOperator.php new file mode 100644 index 000000000..ab1b9b0c1 --- /dev/null +++ b/src/Builder/Search/NearOperator.php @@ -0,0 +1,64 @@ + 'path', 'origin' => 'origin', 'pivot' => 'pivot', 'score' => 'score']; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var Decimal128|Document|GeometryInterface|Int64|Serializable|UTCDateTime|array|float|int|stdClass $origin */ + public readonly Decimal128|Document|Int64|Serializable|UTCDateTime|GeometryInterface|stdClass|array|float|int $origin; + + /** @var Decimal128|Int64|float|int $pivot */ + public readonly Decimal128|Int64|float|int $pivot; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param Decimal128|Document|GeometryInterface|Int64|Serializable|UTCDateTime|array|float|int|stdClass $origin + * @param Decimal128|Int64|float|int $pivot + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + Decimal128|Document|Int64|Serializable|UTCDateTime|GeometryInterface|stdClass|array|float|int $origin, + Decimal128|Int64|float|int $pivot, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->origin = $origin; + $this->pivot = $pivot; + $this->score = $score; + } +} diff --git a/src/Builder/Search/PhraseOperator.php b/src/Builder/Search/PhraseOperator.php new file mode 100644 index 000000000..2bbb707a9 --- /dev/null +++ b/src/Builder/Search/PhraseOperator.php @@ -0,0 +1,83 @@ + 'path', + 'query' => 'query', + 'slop' => 'slop', + 'synonyms' => 'synonyms', + 'score' => 'score', + ]; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var BSONArray|PackedArray|array|string $query */ + public readonly PackedArray|BSONArray|array|string $query; + + /** @var Optional|int $slop */ + public readonly Optional|int $slop; + + /** @var Optional|string $synonyms */ + public readonly Optional|string $synonyms; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param BSONArray|PackedArray|array|string $query + * @param Optional|int $slop + * @param Optional|string $synonyms + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + PackedArray|BSONArray|array|string $query, + Optional|int $slop = Optional::Undefined, + Optional|string $synonyms = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + if (is_array($query) && ! array_is_list($query)) { + throw new InvalidArgumentException('Expected $query argument to be a list, got an associative array.'); + } + + $this->query = $query; + $this->slop = $slop; + $this->synonyms = $synonyms; + $this->score = $score; + } +} diff --git a/src/Builder/Search/QueryStringOperator.php b/src/Builder/Search/QueryStringOperator.php new file mode 100644 index 000000000..edc4d3471 --- /dev/null +++ b/src/Builder/Search/QueryStringOperator.php @@ -0,0 +1,40 @@ + 'defaultPath', 'query' => 'query']; + + /** @var array|string $defaultPath */ + public readonly array|string $defaultPath; + + /** @var string $query */ + public readonly string $query; + + /** + * @param array|string $defaultPath + * @param string $query + */ + public function __construct(array|string $defaultPath, string $query) + { + $this->defaultPath = $defaultPath; + $this->query = $query; + } +} diff --git a/src/Builder/Search/RangeOperator.php b/src/Builder/Search/RangeOperator.php new file mode 100644 index 000000000..a728d7477 --- /dev/null +++ b/src/Builder/Search/RangeOperator.php @@ -0,0 +1,85 @@ + 'path', + 'gt' => 'gt', + 'gte' => 'gte', + 'lt' => 'lt', + 'lte' => 'lte', + 'score' => 'score', + ]; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gt */ + public readonly Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gt; + + /** @var Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gte */ + public readonly Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gte; + + /** @var Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt */ + public readonly Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt; + + /** @var Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte */ + public readonly Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gt + * @param Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gte + * @param Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt + * @param Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gt = Optional::Undefined, + Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $gte = Optional::Undefined, + Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lt = Optional::Undefined, + Optional|Decimal128|Int64|ObjectId|UTCDateTime|float|int|string $lte = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->gt = $gt; + $this->gte = $gte; + $this->lt = $lt; + $this->lte = $lte; + $this->score = $score; + } +} diff --git a/src/Builder/Search/RegexOperator.php b/src/Builder/Search/RegexOperator.php new file mode 100644 index 000000000..6b2c2d63a --- /dev/null +++ b/src/Builder/Search/RegexOperator.php @@ -0,0 +1,67 @@ + 'path', + 'query' => 'query', + 'allowAnalyzedField' => 'allowAnalyzedField', + 'score' => 'score', + ]; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var string $query */ + public readonly string $query; + + /** @var Optional|bool $allowAnalyzedField */ + public readonly Optional|bool $allowAnalyzedField; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param string $query + * @param Optional|bool $allowAnalyzedField + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + string $query, + Optional|bool $allowAnalyzedField = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->query = $query; + $this->allowAnalyzedField = $allowAnalyzedField; + $this->score = $score; + } +} diff --git a/src/Builder/Search/TextOperator.php b/src/Builder/Search/TextOperator.php new file mode 100644 index 000000000..4ade6b434 --- /dev/null +++ b/src/Builder/Search/TextOperator.php @@ -0,0 +1,81 @@ + 'path', + 'query' => 'query', + 'fuzzy' => 'fuzzy', + 'matchCriteria' => 'matchCriteria', + 'synonyms' => 'synonyms', + 'score' => 'score', + ]; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var string $query */ + public readonly string $query; + + /** @var Optional|Document|Serializable|array|stdClass $fuzzy */ + public readonly Optional|Document|Serializable|stdClass|array $fuzzy; + + /** @var Optional|string $matchCriteria */ + public readonly Optional|string $matchCriteria; + + /** @var Optional|string $synonyms */ + public readonly Optional|string $synonyms; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param string $query + * @param Optional|Document|Serializable|array|stdClass $fuzzy + * @param Optional|string $matchCriteria + * @param Optional|string $synonyms + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + string $query, + Optional|Document|Serializable|stdClass|array $fuzzy = Optional::Undefined, + Optional|string $matchCriteria = Optional::Undefined, + Optional|string $synonyms = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->query = $query; + $this->fuzzy = $fuzzy; + $this->matchCriteria = $matchCriteria; + $this->synonyms = $synonyms; + $this->score = $score; + } +} diff --git a/src/Builder/Search/WildcardOperator.php b/src/Builder/Search/WildcardOperator.php new file mode 100644 index 000000000..ee481910e --- /dev/null +++ b/src/Builder/Search/WildcardOperator.php @@ -0,0 +1,66 @@ + 'path', + 'query' => 'query', + 'allowAnalyzedField' => 'allowAnalyzedField', + 'score' => 'score', + ]; + + /** @var array|string $path */ + public readonly array|string $path; + + /** @var string $query */ + public readonly string $query; + + /** @var Optional|bool $allowAnalyzedField */ + public readonly Optional|bool $allowAnalyzedField; + + /** @var Optional|Document|Serializable|array|stdClass $score */ + public readonly Optional|Document|Serializable|stdClass|array $score; + + /** + * @param array|string $path + * @param string $query + * @param Optional|bool $allowAnalyzedField + * @param Optional|Document|Serializable|array|stdClass $score + */ + public function __construct( + array|string $path, + string $query, + Optional|bool $allowAnalyzedField = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $score = Optional::Undefined, + ) { + $this->path = $path; + $this->query = $query; + $this->allowAnalyzedField = $allowAnalyzedField; + $this->score = $score; + } +} diff --git a/src/Builder/Stage/FactoryTrait.php b/src/Builder/Stage/FactoryTrait.php index 199cf64ef..95b0e6e37 100644 --- a/src/Builder/Stage/FactoryTrait.php +++ b/src/Builder/Stage/FactoryTrait.php @@ -24,6 +24,7 @@ use MongoDB\Builder\Type\ExpressionInterface; use MongoDB\Builder\Type\Optional; use MongoDB\Builder\Type\QueryInterface; +use MongoDB\Builder\Type\SearchOperatorInterface; use MongoDB\Builder\Type\Sort; use MongoDB\Model\BSONArray; use stdClass; @@ -537,11 +538,35 @@ public static function sample(int $size): SampleStage * NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/search/ - * @param Document|Serializable|array|stdClass $search - */ - public static function search(Document|Serializable|stdClass|array $search): SearchStage - { - return new SearchStage($search); + * @param Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. + * @param Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to "default". + * @param Optional|Document|Serializable|array|stdClass $highlight Specifies the highlighting options for displaying search terms in their original context. + * @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|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. + * @param Optional|Document|Serializable|array|stdClass $sort Document that specifies the fields to sort the Atlas Search results by in ascending or descending order. + * @param Optional|bool $returnStoredSource Flag that specifies whether to perform a full document lookup on the backend database or return only stored source fields directly from Atlas Search. + * @param Optional|Document|Serializable|array|stdClass $tracking Document that specifies the tracking option to retrieve analytics information on the search terms. + */ + public static function search( + Document|Serializable|SearchOperatorInterface|stdClass|array $operator, + Optional|string $index = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $highlight = Optional::Undefined, + Optional|bool $concurrent = Optional::Undefined, + Optional|string $count = Optional::Undefined, + Optional|string $searchAfter = Optional::Undefined, + Optional|string $searchBefore = Optional::Undefined, + Optional|bool $scoreDetails = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $sort = Optional::Undefined, + Optional|bool $returnStoredSource = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $tracking = Optional::Undefined, + ): SearchStage { + return new SearchStage($operator, $index, $highlight, $concurrent, $count, $searchAfter, $searchBefore, $scoreDetails, $sort, $returnStoredSource, $tracking); } /** @@ -549,11 +574,17 @@ public static function search(Document|Serializable|stdClass|array $search): Sea * NOTE: $searchMeta is only available for MongoDB Atlas clusters running MongoDB v4.4.9 or higher, and is not available for self-managed deployments. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/searchMeta/ - * @param Document|Serializable|array|stdClass $meta - */ - public static function searchMeta(Document|Serializable|stdClass|array $meta): SearchMetaStage - { - return new SearchMetaStage($meta); + * @param Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. + * @param Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to default. + * @param Optional|Document|Serializable|array|stdClass $count Document that specifies the count options for retrieving a count of the results. + */ + public static function searchMeta( + Document|Serializable|SearchOperatorInterface|stdClass|array $operator, + Optional|string $index = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $count = Optional::Undefined, + ): SearchMetaStage { + return new SearchMetaStage($operator, $index, $count); } /** @@ -678,4 +709,29 @@ public static function unwind( ): UnwindStage { return new UnwindStage($path, $includeArrayIndex, $preserveNullAndEmptyArrays); } + + /** + * The $vectorSearch stage performs an ANN or ENN search on a vector in the specified field. + * + * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ + * @param string $index Name of the Atlas Vector Search index to use. + * @param int $limit Number of documents to return in the results. This value can't exceed the value of numCandidates if you specify numCandidates. + * @param array|string $path Indexed vector type field to search. + * @param BSONArray|PackedArray|array $queryVector Array of numbers that represent the query vector. The number type must match the indexed field value type. + * @param Optional|bool $exact This is required if numCandidates is omitted. false to run ANN search. true to run ENN search. + * @param Optional|QueryInterface|array $filter Any match query that compares an indexed field with a boolean, date, objectId, number (not decimals), string, or UUID to use as a pre-filter. + * @param Optional|int $numCandidates This field is required if exact is false or omitted. + * Number of nearest neighbors to use during the search. Value must be less than or equal to (<=) 10000. You can't specify a number less than the number of documents to return (limit). + */ + public static function vectorSearch( + string $index, + int $limit, + array|string $path, + PackedArray|BSONArray|array $queryVector, + Optional|bool $exact = Optional::Undefined, + Optional|QueryInterface|array $filter = Optional::Undefined, + Optional|int $numCandidates = Optional::Undefined, + ): VectorSearchStage { + return new VectorSearchStage($index, $limit, $path, $queryVector, $exact, $filter, $numCandidates); + } } diff --git a/src/Builder/Stage/FluentFactoryTrait.php b/src/Builder/Stage/FluentFactoryTrait.php index 6f468322d..757a09a0b 100644 --- a/src/Builder/Stage/FluentFactoryTrait.php +++ b/src/Builder/Stage/FluentFactoryTrait.php @@ -26,6 +26,7 @@ use MongoDB\Builder\Type\FieldQueryInterface; use MongoDB\Builder\Type\Optional; use MongoDB\Builder\Type\QueryInterface; +use MongoDB\Builder\Type\SearchOperatorInterface; use MongoDB\Builder\Type\Sort; use MongoDB\Builder\Type\StageInterface; use MongoDB\Model\BSONArray; @@ -605,11 +606,35 @@ public function sample(int $size): static * NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/search/ - * @param Document|Serializable|array|stdClass $search - */ - public function search(Document|Serializable|stdClass|array $search): static - { - $this->pipeline[] = Stage::search($search); + * @param Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. + * @param Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to "default". + * @param Optional|Document|Serializable|array|stdClass $highlight Specifies the highlighting options for displaying search terms in their original context. + * @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|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. + * @param Optional|Document|Serializable|array|stdClass $sort Document that specifies the fields to sort the Atlas Search results by in ascending or descending order. + * @param Optional|bool $returnStoredSource Flag that specifies whether to perform a full document lookup on the backend database or return only stored source fields directly from Atlas Search. + * @param Optional|Document|Serializable|array|stdClass $tracking Document that specifies the tracking option to retrieve analytics information on the search terms. + */ + public function search( + Document|Serializable|SearchOperatorInterface|stdClass|array $operator, + Optional|string $index = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $highlight = Optional::Undefined, + Optional|bool $concurrent = Optional::Undefined, + Optional|string $count = Optional::Undefined, + Optional|string $searchAfter = Optional::Undefined, + Optional|string $searchBefore = Optional::Undefined, + Optional|bool $scoreDetails = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $sort = Optional::Undefined, + Optional|bool $returnStoredSource = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $tracking = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::search($operator, $index, $highlight, $concurrent, $count, $searchAfter, $searchBefore, $scoreDetails, $sort, $returnStoredSource, $tracking); return $this; } @@ -619,11 +644,17 @@ public function search(Document|Serializable|stdClass|array $search): static * NOTE: $searchMeta is only available for MongoDB Atlas clusters running MongoDB v4.4.9 or higher, and is not available for self-managed deployments. * * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/searchMeta/ - * @param Document|Serializable|array|stdClass $meta - */ - public function searchMeta(Document|Serializable|stdClass|array $meta): static - { - $this->pipeline[] = Stage::searchMeta($meta); + * @param Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. + * @param Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to default. + * @param Optional|Document|Serializable|array|stdClass $count Document that specifies the count options for retrieving a count of the results. + */ + public function searchMeta( + Document|Serializable|SearchOperatorInterface|stdClass|array $operator, + Optional|string $index = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $count = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::searchMeta($operator, $index, $count); return $this; } @@ -767,4 +798,31 @@ public function unwind( return $this; } + + /** + * The $vectorSearch stage performs an ANN or ENN search on a vector in the specified field. + * + * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ + * @param string $index Name of the Atlas Vector Search index to use. + * @param int $limit Number of documents to return in the results. This value can't exceed the value of numCandidates if you specify numCandidates. + * @param array|string $path Indexed vector type field to search. + * @param BSONArray|PackedArray|array $queryVector Array of numbers that represent the query vector. The number type must match the indexed field value type. + * @param Optional|bool $exact This is required if numCandidates is omitted. false to run ANN search. true to run ENN search. + * @param Optional|QueryInterface|array $filter Any match query that compares an indexed field with a boolean, date, objectId, number (not decimals), string, or UUID to use as a pre-filter. + * @param Optional|int $numCandidates This field is required if exact is false or omitted. + * Number of nearest neighbors to use during the search. Value must be less than or equal to (<=) 10000. You can't specify a number less than the number of documents to return (limit). + */ + public function vectorSearch( + string $index, + int $limit, + array|string $path, + PackedArray|BSONArray|array $queryVector, + Optional|bool $exact = Optional::Undefined, + Optional|QueryInterface|array $filter = Optional::Undefined, + Optional|int $numCandidates = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::vectorSearch($index, $limit, $path, $queryVector, $exact, $filter, $numCandidates); + + return $this; + } } diff --git a/src/Builder/Stage/SearchMetaStage.php b/src/Builder/Stage/SearchMetaStage.php index 88f05bec1..14687161b 100644 --- a/src/Builder/Stage/SearchMetaStage.php +++ b/src/Builder/Stage/SearchMetaStage.php @@ -12,6 +12,8 @@ use MongoDB\BSON\Serializable; use MongoDB\Builder\Type\Encode; use MongoDB\Builder\Type\OperatorInterface; +use MongoDB\Builder\Type\Optional; +use MongoDB\Builder\Type\SearchOperatorInterface; use MongoDB\Builder\Type\StageInterface; use stdClass; @@ -24,18 +26,35 @@ */ final class SearchMetaStage implements StageInterface, OperatorInterface { - public const ENCODE = Encode::Single; + public const ENCODE = Encode::Object; public const NAME = '$searchMeta'; - public const PROPERTIES = ['meta' => 'meta']; + public const PROPERTIES = ['operator' => null, 'index' => 'index', 'count' => 'count']; - /** @var Document|Serializable|array|stdClass $meta */ - public readonly Document|Serializable|stdClass|array $meta; + /** + * @var Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. + */ + public readonly Document|Serializable|SearchOperatorInterface|stdClass|array $operator; + + /** @var Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to default. */ + public readonly Optional|string $index; + + /** @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; /** - * @param Document|Serializable|array|stdClass $meta + * @param Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. + * @param Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to default. + * @param Optional|Document|Serializable|array|stdClass $count Document that specifies the count options for retrieving a count of the results. */ - public function __construct(Document|Serializable|stdClass|array $meta) - { - $this->meta = $meta; + public function __construct( + Document|Serializable|SearchOperatorInterface|stdClass|array $operator, + Optional|string $index = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $count = Optional::Undefined, + ) { + $this->operator = $operator; + $this->index = $index; + $this->count = $count; } } diff --git a/src/Builder/Stage/SearchStage.php b/src/Builder/Stage/SearchStage.php index 188a2ece4..bd224dba9 100644 --- a/src/Builder/Stage/SearchStage.php +++ b/src/Builder/Stage/SearchStage.php @@ -12,6 +12,8 @@ use MongoDB\BSON\Serializable; use MongoDB\Builder\Type\Encode; use MongoDB\Builder\Type\OperatorInterface; +use MongoDB\Builder\Type\Optional; +use MongoDB\Builder\Type\SearchOperatorInterface; use MongoDB\Builder\Type\StageInterface; use stdClass; @@ -24,18 +26,102 @@ */ final class SearchStage implements StageInterface, OperatorInterface { - public const ENCODE = Encode::Single; + public const ENCODE = Encode::Object; public const NAME = '$search'; - public const PROPERTIES = ['search' => 'search']; - /** @var Document|Serializable|array|stdClass $search */ - public readonly Document|Serializable|stdClass|array $search; + public const PROPERTIES = [ + 'operator' => null, + 'index' => 'index', + 'highlight' => 'highlight', + 'concurrent' => 'concurrent', + 'count' => 'count', + 'searchAfter' => 'searchAfter', + 'searchBefore' => 'searchBefore', + 'scoreDetails' => 'scoreDetails', + 'sort' => 'sort', + 'returnStoredSource' => 'returnStoredSource', + 'tracking' => 'tracking', + ]; /** - * @param Document|Serializable|array|stdClass $search + * @var Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. */ - public function __construct(Document|Serializable|stdClass|array $search) - { - $this->search = $search; + public readonly Document|Serializable|SearchOperatorInterface|stdClass|array $operator; + + /** @var Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to "default". */ + public readonly Optional|string $index; + + /** @var Optional|Document|Serializable|array|stdClass $highlight Specifies the highlighting options for displaying search terms in their original context. */ + public readonly Optional|Document|Serializable|stdClass|array $highlight; + + /** + * @var 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. + */ + 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|string $searchAfter Reference point for retrieving results. searchAfter returns documents starting immediately following the specified reference point. */ + public readonly Optional|string $searchAfter; + + /** @var Optional|string $searchBefore Reference point for retrieving results. searchBefore returns documents starting immediately before the specified reference point. */ + public readonly Optional|string $searchBefore; + + /** @var 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. */ + public readonly Optional|bool $scoreDetails; + + /** @var Optional|Document|Serializable|array|stdClass $sort Document that specifies the fields to sort the Atlas Search results by in ascending or descending order. */ + public readonly Optional|Document|Serializable|stdClass|array $sort; + + /** @var Optional|bool $returnStoredSource Flag that specifies whether to perform a full document lookup on the backend database or return only stored source fields directly from Atlas Search. */ + public readonly Optional|bool $returnStoredSource; + + /** @var Optional|Document|Serializable|array|stdClass $tracking Document that specifies the tracking option to retrieve analytics information on the search terms. */ + public readonly Optional|Document|Serializable|stdClass|array $tracking; + + /** + * @param Document|SearchOperatorInterface|Serializable|array|stdClass $operator Operator to search with. You can provide a specific operator or use + * the compound operator to run a compound query with multiple operators. + * @param Optional|string $index Name of the Atlas Search index to use. If omitted, defaults to "default". + * @param Optional|Document|Serializable|array|stdClass $highlight Specifies the highlighting options for displaying search terms in their original context. + * @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|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. + * @param Optional|Document|Serializable|array|stdClass $sort Document that specifies the fields to sort the Atlas Search results by in ascending or descending order. + * @param Optional|bool $returnStoredSource Flag that specifies whether to perform a full document lookup on the backend database or return only stored source fields directly from Atlas Search. + * @param Optional|Document|Serializable|array|stdClass $tracking Document that specifies the tracking option to retrieve analytics information on the search terms. + */ + public function __construct( + Document|Serializable|SearchOperatorInterface|stdClass|array $operator, + Optional|string $index = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $highlight = Optional::Undefined, + Optional|bool $concurrent = Optional::Undefined, + Optional|string $count = Optional::Undefined, + Optional|string $searchAfter = Optional::Undefined, + Optional|string $searchBefore = Optional::Undefined, + Optional|bool $scoreDetails = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $sort = Optional::Undefined, + Optional|bool $returnStoredSource = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $tracking = Optional::Undefined, + ) { + $this->operator = $operator; + $this->index = $index; + $this->highlight = $highlight; + $this->concurrent = $concurrent; + $this->count = $count; + $this->searchAfter = $searchAfter; + $this->searchBefore = $searchBefore; + $this->scoreDetails = $scoreDetails; + $this->sort = $sort; + $this->returnStoredSource = $returnStoredSource; + $this->tracking = $tracking; } } diff --git a/src/Builder/Stage/VectorSearchStage.php b/src/Builder/Stage/VectorSearchStage.php new file mode 100644 index 000000000..e8f7738cc --- /dev/null +++ b/src/Builder/Stage/VectorSearchStage.php @@ -0,0 +1,104 @@ + 'index', + 'limit' => 'limit', + 'path' => 'path', + 'queryVector' => 'queryVector', + 'exact' => 'exact', + 'filter' => 'filter', + 'numCandidates' => 'numCandidates', + ]; + + /** @var string $index Name of the Atlas Vector Search index to use. */ + public readonly string $index; + + /** @var int $limit Number of documents to return in the results. This value can't exceed the value of numCandidates if you specify numCandidates. */ + public readonly int $limit; + + /** @var array|string $path Indexed vector type field to search. */ + public readonly array|string $path; + + /** @var BSONArray|PackedArray|array $queryVector Array of numbers that represent the query vector. The number type must match the indexed field value type. */ + public readonly PackedArray|BSONArray|array $queryVector; + + /** @var Optional|bool $exact This is required if numCandidates is omitted. false to run ANN search. true to run ENN search. */ + public readonly Optional|bool $exact; + + /** @var Optional|QueryInterface|array $filter Any match query that compares an indexed field with a boolean, date, objectId, number (not decimals), string, or UUID to use as a pre-filter. */ + public readonly Optional|QueryInterface|array $filter; + + /** + * @var Optional|int $numCandidates This field is required if exact is false or omitted. + * Number of nearest neighbors to use during the search. Value must be less than or equal to (<=) 10000. You can't specify a number less than the number of documents to return (limit). + */ + public readonly Optional|int $numCandidates; + + /** + * @param string $index Name of the Atlas Vector Search index to use. + * @param int $limit Number of documents to return in the results. This value can't exceed the value of numCandidates if you specify numCandidates. + * @param array|string $path Indexed vector type field to search. + * @param BSONArray|PackedArray|array $queryVector Array of numbers that represent the query vector. The number type must match the indexed field value type. + * @param Optional|bool $exact This is required if numCandidates is omitted. false to run ANN search. true to run ENN search. + * @param Optional|QueryInterface|array $filter Any match query that compares an indexed field with a boolean, date, objectId, number (not decimals), string, or UUID to use as a pre-filter. + * @param Optional|int $numCandidates This field is required if exact is false or omitted. + * Number of nearest neighbors to use during the search. Value must be less than or equal to (<=) 10000. You can't specify a number less than the number of documents to return (limit). + */ + public function __construct( + string $index, + int $limit, + array|string $path, + PackedArray|BSONArray|array $queryVector, + Optional|bool $exact = Optional::Undefined, + Optional|QueryInterface|array $filter = Optional::Undefined, + Optional|int $numCandidates = Optional::Undefined, + ) { + $this->index = $index; + $this->limit = $limit; + $this->path = $path; + if (is_array($queryVector) && ! array_is_list($queryVector)) { + throw new InvalidArgumentException('Expected $queryVector argument to be a list, got an associative array.'); + } + + $this->queryVector = $queryVector; + $this->exact = $exact; + if (is_array($filter)) { + $filter = QueryObject::create($filter); + } + + $this->filter = $filter; + $this->numCandidates = $numCandidates; + } +} diff --git a/src/Builder/Type/Encode.php b/src/Builder/Type/Encode.php index d03491623..120c2ee46 100644 --- a/src/Builder/Type/Encode.php +++ b/src/Builder/Type/Encode.php @@ -29,6 +29,11 @@ enum Encode */ case Single; + /** + * Specific for $group stage + */ + case Group; + /** * Default case used in the interface; implementing classes are expected to override this value */ diff --git a/src/Builder/Type/SearchOperatorInterface.php b/src/Builder/Type/SearchOperatorInterface.php new file mode 100644 index 000000000..c144d4ebe --- /dev/null +++ b/src/Builder/Type/SearchOperatorInterface.php @@ -0,0 +1,7 @@ +assertSamePipeline(Pipelines::AutocompleteAcrossMultipleFields, $pipeline); + } + + public function testBasic(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::autocomplete( + query: 'off', + path: 'title', + ), + ), + Stage::limit(10), + Stage::project(_id: 0, title: 1), + ); + + $this->assertSamePipeline(Pipelines::AutocompleteBasic, $pipeline); + } + + public function testFuzzy(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::autocomplete( + query: 'pre', + path: 'title', + fuzzy: object( + maxEdits: 1, + prefixLength: 1, + maxExpansions: 256, + ), + ), + ), + Stage::limit(10), + Stage::project(_id: 0, title: 1), + ); + + $this->assertSamePipeline(Pipelines::AutocompleteFuzzy, $pipeline); + } + + public function testHighlighting(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::autocomplete( + query: 'ger', + path: 'title', + ), + highlight: object( + path: 'title', + ), + ), + Stage::limit(5), + Stage::project( + score: ['$meta' => 'searchScore'], + _id: 0, + title: 1, + highlights: ['$meta' => 'searchHighlights'], + ), + ); + + $this->assertSamePipeline(Pipelines::AutocompleteHighlighting, $pipeline); + } + + public function testTokenOrderAny(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::autocomplete( + query: 'men with', + path: 'title', + tokenOrder: 'any', + ), + ), + Stage::limit(4), + Stage::project(_id: 0, title: 1), + ); + + $this->assertSamePipeline(Pipelines::AutocompleteTokenOrderAny, $pipeline); + } + + public function testTokenOrderSequential(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::autocomplete( + query: 'men with', + path: 'title', + tokenOrder: 'sequential', + ), + ), + Stage::limit(4), + Stage::project(_id: 0, title: 1), + ); + + $this->assertSamePipeline(Pipelines::AutocompleteTokenOrderSequential, $pipeline); + } +} diff --git a/tests/Builder/Search/CompoundOperatorTest.php b/tests/Builder/Search/CompoundOperatorTest.php new file mode 100644 index 000000000..8d5d69aed --- /dev/null +++ b/tests/Builder/Search/CompoundOperatorTest.php @@ -0,0 +1,157 @@ +assertSamePipeline(Pipelines::CompoundFilter, $pipeline); + } + + public function testMinimumShouldMatch(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::compound( + must: [ + Search::text( + path: 'description', + query: 'varieties', + ), + ], + should: [ + Search::text( + path: 'description', + query: 'Fuji', + ), + Search::text( + path: 'description', + query: 'Golden Delicious', + ), + ], + minimumShouldMatch: 1, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::CompoundMinimumShouldMatch, $pipeline); + } + + public function testMustAndMustNot(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::compound( + must: [ + Search::text( + path: 'description', + query: 'varieties', + ), + ], + mustNot: [ + Search::text( + path: 'description', + query: 'apples', + ), + ], + ), + ), + ); + + $this->assertSamePipeline(Pipelines::CompoundMustAndMustNot, $pipeline); + } + + public function testMustAndShould(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::compound( + must: [ + Search::text( + path: 'description', + query: 'varieties', + ), + ], + should: [ + Search::text( + path: 'description', + query: 'Fuji', + ), + ], + ), + ), + Stage::project( + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::CompoundMustAndShould, $pipeline); + } + + public function testNested(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::compound( + should: [ + Search::text( + path: 'type', + query: 'apple', + ), + Search::compound( + must: [ + Search::text( + path: 'category', + query: 'organic', + ), + Search::equals( + path: 'in_stock', + value: true, + ), + ], + ), + ], + minimumShouldMatch: 1, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::CompoundNested, $pipeline); + } +} diff --git a/tests/Builder/Search/EmbeddedDocumentOperatorTest.php b/tests/Builder/Search/EmbeddedDocumentOperatorTest.php new file mode 100644 index 000000000..b83790c83 --- /dev/null +++ b/tests/Builder/Search/EmbeddedDocumentOperatorTest.php @@ -0,0 +1,167 @@ + 1, + 'items.tags' => 1, + ], + _id: 0, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::EmbeddedDocumentBasic, $pipeline); + } + + public function testFacet(): void + { + $pipeline = new Pipeline( + Stage::searchMeta( + Search::facet( + facets: object( + purchaseMethodFacet: object( + type: 'string', + path: 'purchaseMethod', + ), + ), + operator: Search::embeddedDocument( + path: 'items', + operator: Search::compound( + must: [ + Search::text( + path: 'items.tags', + query: 'school', + ), + ], + should: [ + Search::text( + path: 'items.name', + query: 'backpack', + ), + ], + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EmbeddedDocumentFacet, $pipeline); + } + + public function testQueryAndSort(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::embeddedDocument( + path: 'items', + operator: + Search::text( + path: 'items.name', + query: 'laptop', + ), + ), + sort: ['items.tags' => Sort::Asc], + ), + Stage::limit(5), + Stage::project( + ...[ + 'items.name' => 1, + 'items.tags' => 1, + ], + _id: 0, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::EmbeddedDocumentQueryAndSort, $pipeline); + } + + public function testQueryForMatchingEmbeddedDocumentsOnly(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::embeddedDocument( + path: 'items', + operator: + Search::compound( + must: [ + Search::range( + path: 'items.quantity', + gt: 2, + ), + Search::exists( + path: 'items.price', + ), + Search::text( + path: 'items.tags', + query: 'school', + ), + ], + ), + ), + ), + Stage::limit(2), + Stage::project( + _id: 0, + storeLocation: 1, + items: Expression::filter( + input: Expression::arrayFieldPath('items'), + cond: Expression::and( + Expression::ifNull('$$this.price', 'false'), + Expression::gt(Expression::variable('this.quantity'), 2), + Expression::in('office', Expression::variable('this.tags')), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EmbeddedDocumentQueryForMatchingEmbeddedDocumentsOnly, $pipeline); + } +} diff --git a/tests/Builder/Search/EqualsOperatorTest.php b/tests/Builder/Search/EqualsOperatorTest.php new file mode 100644 index 000000000..6cd8ae9e1 --- /dev/null +++ b/tests/Builder/Search/EqualsOperatorTest.php @@ -0,0 +1,125 @@ + 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::EqualsBoolean, $pipeline); + } + + public function testDate(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::equals( + path: 'account_created', + value: new UTCDateTime(new DateTimeImmutable('2022-05-04T05:01:08')), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EqualsDate, $pipeline); + } + + public function testNull(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::equals( + path: 'job_title', + value: null, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EqualsNull, $pipeline); + } + + public function testNumber(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::equals( + path: 'employee_number', + value: 259, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EqualsNumber, $pipeline); + } + + public function testObjectId(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::equals( + path: 'teammates', + value: new ObjectId('5a9427648b0beebeb69589a1'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EqualsObjectId, $pipeline); + } + + public function testString(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::equals( + path: 'name', + value: 'jim hall', + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EqualsString, $pipeline); + } + + public function testUUID(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::equals( + path: 'uuid', + value: new Binary(hex2bin('fac32260b5114c698485a2be5b7dda9e'), Binary::TYPE_UUID), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::EqualsUUID, $pipeline); + } +} diff --git a/tests/Builder/Search/ExistsOperatorTest.php b/tests/Builder/Search/ExistsOperatorTest.php new file mode 100644 index 000000000..5666a1099 --- /dev/null +++ b/tests/Builder/Search/ExistsOperatorTest.php @@ -0,0 +1,67 @@ +assertSamePipeline(Pipelines::ExistsBasic, $pipeline); + } + + public function testCompound(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::compound( + must: [ + Search::exists( + path: 'type', + ), + Search::text( + path: 'type', + query: 'apple', + ), + ], + should: Search::text( + path: 'description', + query: 'fuji', + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ExistsCompound, $pipeline); + } + + public function testEmbedded(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::exists( + path: 'quantities.lemons', + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ExistsEmbedded, $pipeline); + } +} diff --git a/tests/Builder/Search/FacetOperatorTest.php b/tests/Builder/Search/FacetOperatorTest.php new file mode 100644 index 000000000..3be124bec --- /dev/null +++ b/tests/Builder/Search/FacetOperatorTest.php @@ -0,0 +1,61 @@ +assertSamePipeline(Pipelines::FacetFacet, $pipeline); + } +} diff --git a/tests/Builder/Search/GeoShapeOperatorTest.php b/tests/Builder/Search/GeoShapeOperatorTest.php new file mode 100644 index 000000000..b5e748660 --- /dev/null +++ b/tests/Builder/Search/GeoShapeOperatorTest.php @@ -0,0 +1,204 @@ + 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::GeoShapeDisjoint, $pipeline); + } + + public function testIntersect(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::geoShape( + relation: 'intersects', + geometry: object( + type: 'MultiPolygon', + coordinates: [ + [ + [ + [ + 2.16942, + 41.40082, + ], + [ + 2.17963, + 41.40087, + ], + [ + 2.18146, + 41.39716, + ], + [ + 2.15533, + 41.40686, + ], + [ + 2.14596, + 41.38475, + ], + [ + 2.17519, + 41.41035, + ], + [ + 2.16942, + 41.40082, + ], + ], + ], + [ + [ + [ + 2.16365, + 41.39416, + ], + [ + 2.16963, + 41.39726, + ], + [ + 2.15395, + 41.38005, + ], + [ + 2.17935, + 41.43038, + ], + [ + 2.16365, + 41.39416, + ], + ], + ], + ], + ), + path: 'address.location', + ), + ), + Stage::limit(3), + Stage::project( + _id: 0, + name: 1, + address: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::GeoShapeIntersect, $pipeline); + } + + public function testWithin(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::geoShape( + relation: 'within', + geometry: object( + type: 'Polygon', + coordinates: [ + [ + [ + -74.3994140625, + 40.5305017757, + ], + [ + -74.7290039063, + 40.5805846641, + ], + [ + -74.7729492188, + 40.9467136651, + ], + [ + -74.0698242188, + 41.1290213475, + ], + [ + -73.65234375, + 40.9964840144, + ], + [ + -72.6416015625, + 40.9467136651, + ], + [ + -72.3559570313, + 40.7971774152, + ], + [ + -74.3994140625, + 40.5305017757, + ], + ], + ], + ), + path: 'address.location', + ), + ), + Stage::limit(3), + Stage::project( + _id: 0, + name: 1, + address: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::GeoShapeWithin, $pipeline); + } +} diff --git a/tests/Builder/Search/GeoWithinOperatorTest.php b/tests/Builder/Search/GeoWithinOperatorTest.php new file mode 100644 index 000000000..3a6a6b0af --- /dev/null +++ b/tests/Builder/Search/GeoWithinOperatorTest.php @@ -0,0 +1,124 @@ +assertSamePipeline(Pipelines::GeoWithinBox, $pipeline); + } + + public function testCircle(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::geoWithin( + path: 'address.location', + circle: object( + center: object( + type: 'Point', + coordinates: [ + -73.54, + 45.54, + ], + ), + radius: 1600, + ), + ), + ), + Stage::limit(3), + Stage::project( + _id: 0, + name: 1, + address: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::GeoWithinCircle, $pipeline); + } + + public function testGeometry(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::geoWithin( + path: 'address.location', + geometry: object( + type: 'Polygon', + coordinates: [ + [ + [ + -161.323242, + 22.512557, + ], + [ + -152.446289, + 22.065278, + ], + [ + -156.09375, + 17.811456, + ], + [ + -161.323242, + 22.512557, + ], + ], + ], + ), + ), + ), + Stage::limit(3), + Stage::project( + _id: 0, + name: 1, + address: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::GeoWithinGeometry, $pipeline); + } +} diff --git a/tests/Builder/Search/InOperatorTest.php b/tests/Builder/Search/InOperatorTest.php new file mode 100644 index 000000000..e6d1af57c --- /dev/null +++ b/tests/Builder/Search/InOperatorTest.php @@ -0,0 +1,101 @@ +assertSamePipeline(Pipelines::InArrayValueFieldMatch, $pipeline); + } + + public function testCompoundQueryMatch(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::compound( + must: [ + Search::in( + path: 'name', + value: [ + 'james sanchez', + 'jennifer lawrence', + ], + ), + ], + should: [ + Search::in( + path: '_id', + value: [ + new ObjectId('5ca4bbcea2dd94ee58162a72'), + new ObjectId('5ca4bbcea2dd94ee58162a91'), + ], + ), + ], + ), + ), + Stage::limit(5), + Stage::project( + _id: 1, + name: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::InCompoundQueryMatch, $pipeline); + } + + public function testSingleValueFieldMatch(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::in( + path: 'birthdate', + value: [ + new UTCDateTime(new DateTimeImmutable('1977-03-02T02:20:31')), + new UTCDateTime(new DateTimeImmutable('1977-03-01T00:00:00')), + new UTCDateTime(new DateTimeImmutable('1977-05-06T21:57:35')), + ], + ), + ), + Stage::project( + _id: 0, + name: 1, + birthdate: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::InSingleValueFieldMatch, $pipeline); + } +} diff --git a/tests/Builder/Search/MoreLikeThisOperatorTest.php b/tests/Builder/Search/MoreLikeThisOperatorTest.php new file mode 100644 index 000000000..a83c03559 --- /dev/null +++ b/tests/Builder/Search/MoreLikeThisOperatorTest.php @@ -0,0 +1,115 @@ +assertSamePipeline(Pipelines::MoreLikeThisInputDocumentExcludedInResults, $pipeline); + } + + public function testMultipleAnalyzers(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::compound( + mustNot: [ + Search::equals( + path: '_id', + value: new ObjectId('573a1394f29313caabcde9ef'), + ), + ], + should: [ + Search::moreLikeThis( + like: object( + _id: new ObjectId('573a1396f29313caabce4a9a'), + genres: [ + 'Crime', + 'Drama', + ], + title: 'The Godfather', + ), + ), + ], + ), + ), + Stage::limit(10), + Stage::project( + title: 1, + genres: 1, + _id: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::MoreLikeThisMultipleAnalyzers, $pipeline); + } + + public function testSingleDocumentWithMultipleFields(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::moreLikeThis( + like: object( + title: 'The Godfather', + genres: 'action', + ), + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + released: 1, + genres: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::MoreLikeThisSingleDocumentWithMultipleFields, $pipeline); + } +} diff --git a/tests/Builder/Search/NearOperatorTest.php b/tests/Builder/Search/NearOperatorTest.php new file mode 100644 index 000000000..45a48e0d5 --- /dev/null +++ b/tests/Builder/Search/NearOperatorTest.php @@ -0,0 +1,128 @@ + 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::NearCompound, $pipeline); + } + + public function testDate(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::near( + path: 'released', + origin: new UTCDateTime(new DateTimeImmutable('1915-09-13T00:00:00.000+00:00')), + pivot: 7776000000, + ), + index: 'releaseddate', + ), + Stage::limit(3), + Stage::project( + _id: 0, + title: 1, + released: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::NearDate, $pipeline); + } + + public function testGeoJSONPoint(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::near( + path: 'address.location', + origin: object( + type: 'Point', + coordinates: [ + -8.61308, + 41.1413, + ], + ), + pivot: 1000, + ), + ), + Stage::limit(3), + Stage::project( + _id: 0, + name: 1, + address: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::NearGeoJSONPoint, $pipeline); + } + + public function testNumber(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::near( + path: 'runtime', + origin: 279, + pivot: 2, + ), + index: 'runtimes', + ), + Stage::limit(7), + Stage::project( + _id: 0, + title: 1, + runtime: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::NearNumber, $pipeline); + } +} diff --git a/tests/Builder/Search/PhraseOperatorTest.php b/tests/Builder/Search/PhraseOperatorTest.php new file mode 100644 index 000000000..a045e9c12 --- /dev/null +++ b/tests/Builder/Search/PhraseOperatorTest.php @@ -0,0 +1,102 @@ + 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::PhraseMultiplePhrase, $pipeline); + } + + public function testPhraseSlop(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::phrase( + path: 'title', + query: 'men women', + slop: 5, + ), + ), + Stage::project( + _id: 0, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::PhrasePhraseSlop, $pipeline); + } + + public function testPhraseSynonyms(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::phrase( + path: 'plot', + query: 'automobile race', + slop: 5, + synonyms: 'my_synonyms', + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + plot: 1, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::PhrasePhraseSynonyms, $pipeline); + } + + public function testSinglePhrase(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::phrase( + path: 'title', + query: 'new york', + ), + ), + Stage::limit(10), + Stage::project( + _id: 0, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::PhraseSinglePhrase, $pipeline); + } +} diff --git a/tests/Builder/Search/Pipelines.php b/tests/Builder/Search/Pipelines.php new file mode 100644 index 000000000..fa94f8c9a --- /dev/null +++ b/tests/Builder/Search/Pipelines.php @@ -0,0 +1,2814 @@ +assertSamePipeline(Pipelines::QueryStringBooleanOperatorQueries, $pipeline); + } +} diff --git a/tests/Builder/Search/RangeOperatorTest.php b/tests/Builder/Search/RangeOperatorTest.php new file mode 100644 index 000000000..9606014ed --- /dev/null +++ b/tests/Builder/Search/RangeOperatorTest.php @@ -0,0 +1,122 @@ +assertSamePipeline(Pipelines::RangeDate, $pipeline); + } + + public function testNumberGteLte(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::range( + path: 'runtime', + gte: 2, + lte: 3, + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + runtime: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::RangeNumberGteLte, $pipeline); + } + + public function testNumberLte(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::range( + path: 'runtime', + lte: 2, + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + runtime: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::RangeNumberLte, $pipeline); + } + + public function testObjectId(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::range( + path: '_id', + gte: new ObjectId('573a1396f29313caabce4a9a'), + lte: new ObjectId('573a1396f29313caabce4ae7'), + ), + ), + Stage::project( + _id: 1, + title: 1, + released: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::RangeObjectId, $pipeline); + } + + public function testString(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::range( + path: 'title', + gt: 'city', + lt: 'country', + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::RangeString, $pipeline); + } +} diff --git a/tests/Builder/Search/RegexOperatorTest.php b/tests/Builder/Search/RegexOperatorTest.php new file mode 100644 index 000000000..b95f518a0 --- /dev/null +++ b/tests/Builder/Search/RegexOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::RegexRegex, $pipeline); + } +} diff --git a/tests/Builder/Search/TextOperatorTest.php b/tests/Builder/Search/TextOperatorTest.php new file mode 100644 index 000000000..12709740e --- /dev/null +++ b/tests/Builder/Search/TextOperatorTest.php @@ -0,0 +1,194 @@ + 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextBasic, $pipeline); + } + + public function testFuzzyDefault(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'title', + query: 'naw yark', + fuzzy: object(), + ), + ), + Stage::limit(10), + Stage::project( + _id: 0, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextFuzzyDefault, $pipeline); + } + + public function testFuzzyMaxExpansions(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'title', + query: 'naw yark', + fuzzy: object( + maxEdits: 1, + maxExpansions: 100, + ), + ), + ), + Stage::limit(10), + Stage::project( + _id: 0, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextFuzzyMaxExpansions, $pipeline); + } + + public function testFuzzyPrefixLength(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'title', + query: 'naw yark', + fuzzy: object( + maxEdits: 1, + prefixLength: 2, + ), + ), + ), + Stage::limit(8), + Stage::project( + _id: 1, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextFuzzyPrefixLength, $pipeline); + } + + public function testMatchAllUsingSynonyms(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'plot', + query: 'automobile race', + matchCriteria: 'all', + synonyms: 'my_synonyms', + ), + ), + Stage::limit(20), + Stage::project( + _id: 0, + plot: 1, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextMatchAllUsingSynonyms, $pipeline); + } + + public function testMatchAnyUsingEquivalentMapping(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'plot', + query: 'attire', + synonyms: 'my_synonyms', + matchCriteria: 'any', + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + plot: 1, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextMatchAnyUsingEquivalentMapping, $pipeline); + } + + public function testMatchAnyUsingExplicitMapping(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: 'plot', + query: 'boat race', + synonyms: 'my_synonyms', + matchCriteria: 'any', + ), + ), + Stage::limit(10), + Stage::project( + _id: 0, + plot: 1, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextMatchAnyUsingExplicitMapping, $pipeline); + } + + public function testWildcardPath(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::text( + path: ['wildcard' => '*'], + query: 'surfer', + ), + ), + Stage::project( + _id: 0, + title: 1, + score: ['$meta' => 'searchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::TextWildcardPath, $pipeline); + } +} diff --git a/tests/Builder/Search/WildcardOperatorTest.php b/tests/Builder/Search/WildcardOperatorTest.php new file mode 100644 index 000000000..18103b75d --- /dev/null +++ b/tests/Builder/Search/WildcardOperatorTest.php @@ -0,0 +1,54 @@ +assertSamePipeline(Pipelines::WildcardEscapeCharacterExample, $pipeline); + } + + public function testWildcardPath(): void + { + $pipeline = new Pipeline( + Stage::search( + Search::wildcard( + query: 'Wom?n *', + path: ['wildcard' => '*'], + ), + ), + Stage::limit(5), + Stage::project( + _id: 0, + title: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::WildcardWildcardPath, $pipeline); + } +} diff --git a/tests/Builder/Stage/Pipelines.php b/tests/Builder/Stage/Pipelines.php index 8b76342ba..850013c3e 100644 --- a/tests/Builder/Stage/Pipelines.php +++ b/tests/Builder/Stage/Pipelines.php @@ -2638,6 +2638,197 @@ enum Pipelines: string ] JSON; + /** + * Year Facet + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/facet/#example-1 + */ + case SearchMetaYearFacet = <<<'JSON' + [ + { + "$searchMeta": { + "facet": { + "operator": { + "range": { + "path": "year", + "gte": { + "$numberInt": "1980" + }, + "lte": { + "$numberInt": "2000" + } + } + }, + "facets": { + "yearFacet": { + "type": "number", + "path": "year", + "boundaries": [ + { + "$numberInt": "1980" + }, + { + "$numberInt": "1990" + }, + { + "$numberInt": "2000" + } + ], + "default": "other" + } + } + } + } + } + ] + JSON; + + /** + * Date Facet + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/facet/#example-2 + */ + case SearchMetaDateFacet = <<<'JSON' + [ + { + "$searchMeta": { + "facet": { + "operator": { + "range": { + "path": "released", + "gte": { + "$date": { + "$numberLong": "946684800000" + } + }, + "lte": { + "$date": { + "$numberLong": "1422662400000" + } + } + } + }, + "facets": { + "yearFacet": { + "type": "date", + "path": "released", + "boundaries": [ + { + "$date": { + "$numberLong": "946684800000" + } + }, + { + "$date": { + "$numberLong": "1104537600000" + } + }, + { + "$date": { + "$numberLong": "1262304000000" + } + }, + { + "$date": { + "$numberLong": "1420070400000" + } + } + ], + "default": "other" + } + } + } + } + } + ] + JSON; + + /** + * Metadata Results + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/facet/#examples + */ + case SearchMetaMetadataResults = <<<'JSON' + [ + { + "$searchMeta": { + "facet": { + "operator": { + "range": { + "path": "released", + "gte": { + "$date": { + "$numberLong": "946684800000" + } + }, + "lte": { + "$date": { + "$numberLong": "1422662400000" + } + } + } + }, + "facets": { + "directorsFacet": { + "type": "string", + "path": "directors", + "numBuckets": { + "$numberInt": "7" + } + }, + "yearFacet": { + "type": "number", + "path": "year", + "boundaries": [ + { + "$numberInt": "2000" + }, + { + "$numberInt": "2005" + }, + { + "$numberInt": "2010" + }, + { + "$numberInt": "2015" + } + ] + } + } + } + } + } + ] + JSON; + + /** + * Autocomplete Bucket Results through Facet Queries + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/#bucket-results-through-facet-queries + */ + case SearchMetaAutocompleteBucketResultsThroughFacetQueries = <<<'JSON' + [ + { + "$searchMeta": { + "facet": { + "operator": { + "autocomplete": { + "path": "title", + "query": "Gravity" + } + }, + "facets": { + "titleFacet": { + "type": "string", + "path": "title" + } + } + } + } + } + ] + JSON; + /** * Using Two $set Stages * @@ -3362,4 +3553,163 @@ enum Pipelines: string } ] JSON; + + /** + * ANN Basic + * + * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#ann-examples + */ + case VectorSearchANNBasic = <<<'JSON' + [ + { + "$vectorSearch": { + "index": "vector_index", + "path": "plot_embedding", + "queryVector": [ + { + "$numberDouble": "-0.0016261311999999999121" + }, + { + "$numberDouble": "-0.028070756999999998266" + }, + { + "$numberDouble": "-0.011342932000000000015" + } + ], + "numCandidates": { + "$numberInt": "150" + }, + "limit": { + "$numberInt": "10" + } + } + }, + { + "$project": { + "_id": { + "$numberInt": "0" + }, + "plot": { + "$numberInt": "1" + }, + "title": { + "$numberInt": "1" + }, + "score": { + "$meta": "vectorSearchScore" + } + } + } + ] + JSON; + + /** + * ANN Filter + * + * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#ann-examples + */ + case VectorSearchANNFilter = <<<'JSON' + [ + { + "$vectorSearch": { + "index": "vector_index", + "path": "plot_embedding", + "filter": { + "$and": [ + { + "year": { + "$lt": { + "$numberInt": "1975" + } + } + } + ] + }, + "queryVector": [ + { + "$numberDouble": "0.024210530000000000939" + }, + { + "$numberDouble": "-0.022372592000000000173" + }, + { + "$numberDouble": "-0.0062311370000000003075" + } + ], + "numCandidates": { + "$numberInt": "150" + }, + "limit": { + "$numberInt": "10" + } + } + }, + { + "$project": { + "_id": { + "$numberInt": "0" + }, + "title": { + "$numberInt": "1" + }, + "plot": { + "$numberInt": "1" + }, + "year": { + "$numberInt": "1" + }, + "score": { + "$meta": "vectorSearchScore" + } + } + } + ] + JSON; + + /** + * ENN + * + * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/#enn-examples + */ + case VectorSearchENN = <<<'JSON' + [ + { + "$vectorSearch": { + "index": "vector_index", + "path": "plot_embedding", + "queryVector": [ + { + "$numberDouble": "-0.0069540970000000002296" + }, + { + "$numberDouble": "-0.009932498999999999148" + }, + { + "$numberDouble": "-0.0013114739999999999731" + } + ], + "exact": true, + "limit": { + "$numberInt": "10" + } + } + }, + { + "$project": { + "_id": { + "$numberInt": "0" + }, + "plot": { + "$numberInt": "1" + }, + "title": { + "$numberInt": "1" + }, + "score": { + "$meta": "vectorSearchScore" + } + } + } + ] + JSON; } diff --git a/tests/Builder/Stage/SearchMetaStageTest.php b/tests/Builder/Stage/SearchMetaStageTest.php index 3b8283424..6ded31935 100644 --- a/tests/Builder/Stage/SearchMetaStageTest.php +++ b/tests/Builder/Stage/SearchMetaStageTest.php @@ -4,7 +4,10 @@ namespace MongoDB\Tests\Builder\Stage; +use DateTimeImmutable; +use MongoDB\BSON\UTCDateTime; use MongoDB\Builder\Pipeline; +use MongoDB\Builder\Search; use MongoDB\Builder\Stage; use MongoDB\Tests\Builder\PipelineTestCase; @@ -15,19 +18,134 @@ */ class SearchMetaStageTest extends PipelineTestCase { + public function testAutocompleteBucketResultsThroughFacetQueries(): void + { + $pipeline = new Pipeline( + Stage::searchMeta( + Search::facet( + facets: object( + titleFacet: object( + type: 'string', + path: 'title', + ), + ), + operator: Search::autocomplete( + path: 'title', + query: 'Gravity', + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SearchMetaAutocompleteBucketResultsThroughFacetQueries, $pipeline); + } + + public function testDateFacet(): void + { + $pipeline = new Pipeline( + Stage::searchMeta( + Search::facet( + operator: Search::range( + path: 'released', + gte: new UTCDateTime(new DateTimeImmutable('2000-01-01')), + lte: new UTCDateTime(new DateTimeImmutable('2015-01-31')), + ), + facets: object( + yearFacet: object( + type: 'date', + path: 'released', + boundaries: [ + new UTCDateTime(new DateTimeImmutable('2000-01-01')), + new UTCDateTime(new DateTimeImmutable('2005-01-01')), + new UTCDateTime(new DateTimeImmutable('2010-01-01')), + new UTCDateTime(new DateTimeImmutable('2015-01-01')), + ], + default: 'other', + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SearchMetaDateFacet, $pipeline); + } + public function testExample(): void { $pipeline = new Pipeline( - Stage::searchMeta(object( - range: object( + Stage::searchMeta( + Search::range( path: 'year', gte: 1998, lt: 1999, ), count: object(type: 'total'), - )), + ), ); $this->assertSamePipeline(Pipelines::SearchMetaExample, $pipeline); } + + public function testMetadataResults(): void + { + $pipeline = new Pipeline( + Stage::searchMeta( + Search::facet( + operator: Search::range( + path: 'released', + gte: new UTCDateTime(new DateTimeImmutable('2000-01-01')), + lte: new UTCDateTime(new DateTimeImmutable('2015-01-31')), + ), + facets: object( + directorsFacet: object( + type: 'string', + path: 'directors', + numBuckets: 7, + ), + yearFacet: object( + type: 'number', + path: 'year', + boundaries: [ + 2000, + 2005, + 2010, + 2015, + ], + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SearchMetaMetadataResults, $pipeline); + } + + public function testYearFacet(): void + { + $pipeline = new Pipeline( + Stage::searchMeta( + Search::facet( + operator: Search::range( + path: 'year', + gte: 1980, + lte: 2000, + ), + facets: object( + yearFacet: object( + type: 'number', + path: 'year', + boundaries: [ + 1980, + 1990, + 2000, + ], + default: 'other', + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SearchMetaYearFacet, $pipeline); + } } diff --git a/tests/Builder/Stage/SearchStageTest.php b/tests/Builder/Stage/SearchStageTest.php index 656b9b9ee..faae2ab11 100644 --- a/tests/Builder/Stage/SearchStageTest.php +++ b/tests/Builder/Stage/SearchStageTest.php @@ -8,11 +8,10 @@ use MongoDB\BSON\UTCDateTime; use MongoDB\Builder\Expression; use MongoDB\Builder\Pipeline; +use MongoDB\Builder\Search; use MongoDB\Builder\Stage; use MongoDB\Tests\Builder\PipelineTestCase; -use function MongoDB\object; - /** * Test $search stage */ @@ -21,13 +20,13 @@ class SearchStageTest extends PipelineTestCase public function testExample(): void { $pipeline = new Pipeline( - Stage::search(object( - near: object( + Stage::search( + Search::near( path: 'released', origin: new UTCDateTime(new DateTime('2011-09-01T00:00:00.000+00:00')), pivot: 7776000000, ), - )), + ), Stage::project(_id: 0, title: 1, released: 1), Stage::limit(5), Stage::facet( diff --git a/tests/Builder/Stage/VectorSearchStageTest.php b/tests/Builder/Stage/VectorSearchStageTest.php new file mode 100644 index 000000000..f6259510a --- /dev/null +++ b/tests/Builder/Stage/VectorSearchStageTest.php @@ -0,0 +1,85 @@ + 'vectorSearchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::VectorSearchANNBasic, $pipeline); + } + + public function testANNFilter(): void + { + $pipeline = new Pipeline( + Stage::vectorSearch( + index: 'vector_index', + limit: 10, + path: 'plot_embedding', + queryVector: [0.02421053, -0.022372592, -0.006231137], + filter: Query::and( + Query::query( + year: Query::lt(1975), + ), + ), + numCandidates: 150, + ), + Stage::project( + _id: 0, + title: 1, + plot: 1, + year: 1, + score: ['$meta' => 'vectorSearchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::VectorSearchANNFilter, $pipeline); + } + + public function testENN(): void + { + $pipeline = new Pipeline( + Stage::vectorSearch( + index: 'vector_index', + limit: 10, + path: 'plot_embedding', + queryVector: [-0.006954097, -0.009932499, -0.001311474], + exact: true, + ), + Stage::project( + _id: 0, + title: 1, + plot: 1, + score: ['$meta' => 'vectorSearchScore'], + ), + ); + + $this->assertSamePipeline(Pipelines::VectorSearchENN, $pipeline); + } +}