From f10f346dc4d0a0a3fcccda6591dc4f85310b8d78 Mon Sep 17 00:00:00 2001 From: Rea Rustagi <85902999+rustagir@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:26:01 -0500 Subject: [PATCH] DOCSP-44554: add more aggregation examples (#3272) * DOCSP-44554: add more agg exs * import model fps * fix formatting * CI errors * language formatting * MW PR fixes 1 * JT small fix --- docs/fundamentals/aggregation-builder.txt | 196 +++++++++++--- .../aggregation/AggregationsBuilderTest.php | 244 +++++++++++++++++- .../fundamentals/aggregation/Inventory.php | 12 + .../fundamentals/aggregation/Order.php | 11 + .../fundamentals/aggregation/Sale.php | 11 + 5 files changed, 420 insertions(+), 54 deletions(-) create mode 100644 docs/includes/fundamentals/aggregation/Inventory.php create mode 100644 docs/includes/fundamentals/aggregation/Order.php create mode 100644 docs/includes/fundamentals/aggregation/Sale.php diff --git a/docs/fundamentals/aggregation-builder.txt b/docs/fundamentals/aggregation-builder.txt index 0dbcd3823..3169acfeb 100644 --- a/docs/fundamentals/aggregation-builder.txt +++ b/docs/fundamentals/aggregation-builder.txt @@ -39,6 +39,7 @@ aggregation builder to create the stages of an aggregation pipeline: - :ref:`laravel-add-aggregation-dependency` - :ref:`laravel-build-aggregation` +- :ref:`laravel-aggregation-examples` - :ref:`laravel-create-custom-operator-factory` .. tip:: @@ -71,12 +72,13 @@ includes the following line in the ``require`` object: .. _laravel-build-aggregation: -Create an Aggregation Pipeline ------------------------------- +Create Aggregation Stages +------------------------- To start an aggregation pipeline, call the ``Model::aggregate()`` method. -Then, chain the aggregation stage methods in the sequence you want them to -run. +Then, chain aggregation stage methods and specify the necessary +parameters for the stage. For example, you can call the ``sort()`` +operator method to build a ``$sort`` stage. The aggregation builder includes the following namespaces that you can import to build aggregation stages: @@ -88,17 +90,17 @@ to build aggregation stages: .. tip:: - To learn more about builder classes, see the `mongodb/mongodb-php-builder `__ + To learn more about builder classes, see the + :github:`mongodb/mongodb-php-builder ` GitHub repository. -This section features the following examples, which show how to use common -aggregation stages and combine stages to build an aggregation pipeline: +This section features the following examples that show how to use common +aggregation stages: - :ref:`laravel-aggregation-match-stage-example` - :ref:`laravel-aggregation-group-stage-example` - :ref:`laravel-aggregation-sort-stage-example` - :ref:`laravel-aggregation-project-stage-example` -- :ref:`laravel-aggregation-pipeline-example` To learn more about MongoDB aggregation operators, see :manual:`Aggregation Stages ` in @@ -112,10 +114,10 @@ by the ``User`` model. You can add the sample data by running the following ``insert()`` method: .. literalinclude:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php - :language: php - :dedent: - :start-after: begin aggregation builder sample data - :end-before: end aggregation builder sample data + :language: php + :dedent: + :start-after: begin aggregation builder sample data + :end-before: end aggregation builder sample data .. _laravel-aggregation-match-stage-example: @@ -151,6 +153,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the documents returned by running the code: .. io-code-block:: + :copyable: true .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php :language: php @@ -226,6 +229,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the documents returned by running the code: .. io-code-block:: + :copyable: true .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php :language: php @@ -270,6 +274,7 @@ alphabetical order. Click the :guilabel:`{+code-output-label+}` button to see the documents returned by running the code: .. io-code-block:: + :copyable: true .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php :language: php @@ -370,6 +375,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the data returned by running the code: .. io-code-block:: + :copyable: true .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php :language: php @@ -390,56 +396,166 @@ running the code: { "name": "Ellis Lee" } ] +.. _laravel-aggregation-examples: -.. _laravel-aggregation-pipeline-example: +Build Aggregation Pipelines +--------------------------- -Aggregation Pipeline Example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To build an aggregation pipeline, call the ``Model::aggregate()`` method, +then chain the aggregation stages in the sequence you want them to +run. The examples in this section are adapted from the {+server-docs-name+}. +Each example provides a link to the sample data that you can insert into +your database to test the aggregation operation. + +This section features the following examples, which show how to use common +aggregation stages: -This aggregation pipeline example chains multiple stages. Each stage runs -on the output retrieved from each preceding stage. In this example, the -stages perform the following operations sequentially: +- :ref:`laravel-aggregation-filter-group-example` +- :ref:`laravel-aggregation-unwind-example` +- :ref:`laravel-aggregation-lookup-example` -- Add the ``birth_year`` field to the documents and set the value to the year - extracted from the ``birthday`` field. -- Group the documents by the value of the ``occupation`` field and compute - the average value of ``birth_year`` for each group by using the - ``Accumulator::avg()`` function. Assign the result of the computation to - the ``birth_year_avg`` field. -- Sort the documents by the group key field in ascending order. -- Create the ``profession`` field from the value of the group key field, - include the ``birth_year_avg`` field, and omit the ``_id`` field. +.. _laravel-aggregation-filter-group-example: + +Filter and Group Example +~~~~~~~~~~~~~~~~~~~~~~~~ + +This example uses the sample data given in the :manual:`Calculate Count, +Sum, and Average ` +section of the ``$group`` stage reference in the {+server-docs-name+}. + +The following code example calculates the total sales amount, average +sales quantity, and sale count for each day in the year 2014. To do so, +it uses an aggregation pipeline that contains the following stages: + +1. :manual:`$match ` stage to + filter for documents that contain a ``date`` field in which the year is + 2014 + +#. :manual:`$group ` stage to + group the documents by date and calculate the total sales amount, + average sales quantity, and sale count for each group + +#. :manual:`$sort ` stage to + sort the results by the total sale amount for each group in descending + order Click the :guilabel:`{+code-output-label+}` button to see the data returned by running the code: .. io-code-block:: + :copyable: true .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php :language: php :dedent: - :start-after: begin pipeline example - :end-before: end pipeline example + :start-after: start-builder-match-group + :end-before: end-builder-match-group .. output:: :language: json :visible: false [ - { - "birth_year_avg": 1991.5, - "profession": "designer" - }, - { - "birth_year_avg": 1995.5, - "profession": "engineer" - } + { "_id": "2014-04-04", "totalSaleAmount": { "$numberDecimal": "200" }, "averageQuantity": 15, "count": 2 }, + { "_id": "2014-03-15", "totalSaleAmount": { "$numberDecimal": "50" }, "averageQuantity": 10, "count": 1 }, + { "_id": "2014-03-01", "totalSaleAmount": { "$numberDecimal": "40" }, "averageQuantity": 1.5, "count": 2 } + ] + +.. _laravel-aggregation-unwind-example: + +Unwind Embedded Arrays Example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example uses the sample data given in the :manual:`Unwind Embedded Arrays +` +section of the ``$unwind`` stage reference in the {+server-docs-name+}. + +The following code example groups sold items by their tags and +calculates the total sales amount for each tag. To do so, +it uses an aggregation pipeline that contains the following stages: + +1. :manual:`$unwind ` stage to + output a separate document for each element in the ``items`` array + +#. :manual:`$unwind ` stage to + output a separate document for each element in the ``items.tags`` arrays + +#. :manual:`$group ` stage to + group the documents by the tag value and calculate the total sales + amount of items that have each tag + +Click the :guilabel:`{+code-output-label+}` button to see the data returned by +running the code: + +.. io-code-block:: + :copyable: true + + .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php + :start-after: start-builder-unwind + :end-before: end-builder-unwind + :language: php + :dedent: + + .. output:: + :language: json + :visible: false + + [ + { "_id": "school", "totalSalesAmount": { "$numberDecimal": "104.85" } }, + { "_id": "electronics", "totalSalesAmount": { "$numberDecimal": "800.00" } }, + { "_id": "writing", "totalSalesAmount": { "$numberDecimal": "60.00" } }, + { "_id": "office", "totalSalesAmount": { "$numberDecimal": "1019.60" } }, + { "_id": "stationary", "totalSalesAmount": { "$numberDecimal": "264.45" } } ] -.. note:: +.. _laravel-aggregation-lookup-example: + +Single Equality Join Example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example uses the sample data given in the :manual:`Perform a Single +Equality Join with $lookup +` +section of the ``$lookup`` stage reference in the {+server-docs-name+}. + +The following code example joins the documents from the ``orders`` +collection with the documents from the ``inventory`` collection by using +the ``item`` field from the ``orders`` collection and the ``sku`` field +from the ``inventory`` collection. - Since this pipeline omits the ``match()`` stage, the input for the initial - stage consists of all the documents in the collection. +To do so, the example uses an aggregation pipeline that contains a +:manual:`$lookup ` stage that +specifies the collection to retrieve data from and the local and +foreign field names. + +Click the :guilabel:`{+code-output-label+}` button to see the data returned by +running the code: + +.. io-code-block:: + :copyable: true + + .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php + :start-after: start-builder-lookup + :end-before: end-builder-lookup + :language: php + :dedent: + + .. output:: + :language: json + :visible: false + + [ + { "_id": 1, "item": "almonds", "price": 12, "quantity": 2, "inventory_docs": [ + { "_id": 1, "sku": "almonds", "description": "product 1", "instock": 120 } + ] }, + { "_id": 2, "item": "pecans", "price": 20, "quantity": 1, "inventory_docs": [ + { "_id": 4, "sku": "pecans", "description": "product 4", "instock": 70 } + ] }, + { "_id": 3, "inventory_docs": [ + { "_id": 5, "sku": null, "description": "Incomplete" }, + { "_id": 6 } + ] } + ] .. _laravel-create-custom-operator-factory: diff --git a/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php b/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php index 4880ee75f..49f7d5c8f 100644 --- a/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php +++ b/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php @@ -4,7 +4,11 @@ namespace App\Http\Controllers; +use App\Models\Inventory; +use App\Models\Order; +use App\Models\Sale; use DateTimeImmutable; +use MongoDB\BSON\Decimal128; use MongoDB\BSON\UTCDateTime; use MongoDB\Builder\Accumulator; use MongoDB\Builder\Expression; @@ -18,6 +22,10 @@ class AggregationsBuilderTest extends TestCase { protected function setUp(): void { + require_once __DIR__ . '/Sale.php'; + require_once __DIR__ . '/Order.php'; + require_once __DIR__ . '/Inventory.php'; + parent::setUp(); User::truncate(); @@ -84,27 +92,235 @@ public function testAggregationBuilderProjectStage(): void $this->assertArrayNotHasKey('_id', $result->first()); } - public function testAggregationBuilderPipeline(): void + public function testAggregationBuilderMatchGroup(): void { - // begin pipeline example - $pipeline = User::aggregate() - ->addFields( - birth_year: Expression::year( - Expression::dateFieldPath('birthday'), - ), + Sale::truncate(); + + Sale::insert([ + [ + '_id' => 1, + 'item' => 'abc', + 'price' => new Decimal128('10'), + 'quantity' => 2, + 'date' => new UTCDateTime(new DateTimeImmutable('2014-03-01T08:00:00Z')), + ], + [ + '_id' => 2, + 'item' => 'jkl', + 'price' => new Decimal128('20'), + 'quantity' => 1, + 'date' => new UTCDateTime(new DateTimeImmutable('2014-03-01T09:00:00Z')), + ], + [ + '_id' => 3, + 'item' => 'xyz', + 'price' => new Decimal128('5'), + 'quantity' => 10, + 'date' => new UTCDateTime(new DateTimeImmutable('2014-03-15T09:00:00Z')), + ], + [ + '_id' => 4, + 'item' => 'xyz', + 'price' => new Decimal128('5'), + 'quantity' => 20, + 'date' => new UTCDateTime(new DateTimeImmutable('2014-04-04T11:21:39.736Z')), + ], + [ + '_id' => 5, + 'item' => 'abc', + 'price' => new Decimal128('10'), + 'quantity' => 10, + 'date' => new UTCDateTime(new DateTimeImmutable('2014-04-04T21:23:13.331Z')), + ], + [ + '_id' => 6, + 'item' => 'def', + 'price' => new Decimal128('7.5'), + 'quantity' => 5, + 'date' => new UTCDateTime(new DateTimeImmutable('2015-06-04T05:08:13Z')), + ], + [ + '_id' => 7, + 'item' => 'def', + 'price' => new Decimal128('7.5'), + 'quantity' => 10, + 'date' => new UTCDateTime(new DateTimeImmutable('2015-09-10T08:43:00Z')), + ], + [ + '_id' => 8, + 'item' => 'abc', + 'price' => new Decimal128('10'), + 'quantity' => 5, + 'date' => new UTCDateTime(new DateTimeImmutable('2016-02-06T20:20:13Z')), + ], + ]); + + // start-builder-match-group + $pipeline = Sale::aggregate() + ->match( + date: [ + Query::gte(new UTCDateTime(new DateTimeImmutable('2014-01-01'))), + Query::lt(new UTCDateTime(new DateTimeImmutable('2015-01-01'))), + ], ) ->group( - _id: Expression::fieldPath('occupation'), - birth_year_avg: Accumulator::avg(Expression::numberFieldPath('birth_year')), + _id: Expression::dateToString(Expression::dateFieldPath('date'), '%Y-%m-%d'), + totalSaleAmount: Accumulator::sum( + Expression::multiply( + Expression::numberFieldPath('price'), + Expression::numberFieldPath('quantity'), + ), + ), + averageQuantity: Accumulator::avg( + Expression::numberFieldPath('quantity'), + ), + count: Accumulator::sum(1), ) - ->sort(_id: Sort::Asc) - ->project(profession: Expression::fieldPath('_id'), birth_year_avg: 1, _id: 0); - // end pipeline example + ->sort( + totalSaleAmount: Sort::Desc, + ); + // end-builder-match-group $result = $pipeline->get(); - $this->assertEquals(2, $result->count()); - $this->assertNotNull($result->first()['birth_year_avg']); + $this->assertEquals(3, $result->count()); + $this->assertNotNull($result->first()['totalSaleAmount']); + } + + public function testAggregationBuilderUnwind(): void + { + Sale::truncate(); + + Sale::insert([ + [ + '_id' => '1', + 'items' => [ + [ + 'name' => 'pens', + 'tags' => ['writing', 'office', 'school', 'stationary'], + 'price' => new Decimal128('12.00'), + 'quantity' => 5, + ], + [ + 'name' => 'envelopes', + 'tags' => ['stationary', 'office'], + 'price' => new Decimal128('19.95'), + 'quantity' => 8, + ], + ], + ], + [ + '_id' => '2', + 'items' => [ + [ + 'name' => 'laptop', + 'tags' => ['office', 'electronics'], + 'price' => new Decimal128('800.00'), + 'quantity' => 1, + ], + [ + 'name' => 'notepad', + 'tags' => ['stationary', 'school'], + 'price' => new Decimal128('14.95'), + 'quantity' => 3, + ], + ], + ], + ]); + + // start-builder-unwind + $pipeline = Sale::aggregate() + ->unwind(Expression::arrayFieldPath('items')) + ->unwind(Expression::arrayFieldPath('items.tags')) + ->group( + _id: Expression::fieldPath('items.tags'), + totalSalesAmount: Accumulator::sum( + Expression::multiply( + Expression::numberFieldPath('items.price'), + Expression::numberFieldPath('items.quantity'), + ), + ), + ); + // end-builder-unwind + + $result = $pipeline->get(); + + $this->assertEquals(5, $result->count()); + $this->assertNotNull($result->first()['totalSalesAmount']); + } + + public function testAggregationBuilderLookup(): void + { + Order::truncate(); + Inventory::truncate(); + + Order::insert([ + [ + '_id' => 1, + 'item' => 'almonds', + 'price' => 12, + 'quantity' => 2, + ], + [ + '_id' => 2, + 'item' => 'pecans', + 'price' => 20, + 'quantity' => 1, + ], + [ + '_id' => 3, + ], + ]); + + Inventory::insert([ + [ + '_id' => 1, + 'sku' => 'almonds', + 'description' => 'product 1', + 'instock' => 120, + ], + [ + '_id' => 2, + 'sku' => 'bread', + 'description' => 'product 2', + 'instock' => 80, + ], + [ + '_id' => 3, + 'sku' => 'cashews', + 'description' => 'product 3', + 'instock' => 60, + ], + [ + '_id' => 4, + 'sku' => 'pecans', + 'description' => 'product 4', + 'instock' => 70, + ], + [ + '_id' => 5, + 'sku' => null, + 'description' => 'Incomplete', + ], + [ + '_id' => 6, + ], + ]); + + // start-builder-lookup + $pipeline = Order::aggregate() + ->lookup( + from: 'inventory', + localField: 'item', + foreignField: 'sku', + as: 'inventory_docs', + ); + // end-builder-lookup + + $result = $pipeline->get(); + + $this->assertEquals(3, $result->count()); + $this->assertNotNull($result->first()['item']); } // phpcs:disable Squiz.Commenting.FunctionComment.WrongStyle diff --git a/docs/includes/fundamentals/aggregation/Inventory.php b/docs/includes/fundamentals/aggregation/Inventory.php new file mode 100644 index 000000000..e1cdc7be1 --- /dev/null +++ b/docs/includes/fundamentals/aggregation/Inventory.php @@ -0,0 +1,12 @@ +