Cursors and the Aggregation Framework
Now that MongoDB 2.6 has been released, the PHP driver for MongoDB has also received amny updates to support the new features. In this series of articles, I will illustrate some of those updates.
In this article, I will introduce command cursors and demonstrate how they can be applied to aggregations. I previously wrote about the Aggregation Framework last year, but since then it has received a lot of updates and improvements. One of those improvements relates to how the Aggregation Framework (A/F) returns results. Before MongoDB 2.6, the A/F could only return one document, with all the results stored under the results key:
<?php
$m = new MongoClient;
$c = $m->demo->cities;
$pipeline = [
[ '$group' => [
'_id' => '$country_code',
'timezones' => [ '$addToSet' => '$timezone' ]
] ],
[ '$sort' => [ '_id' => 1 ] ],
];
$r = $c->aggregate( $pipeline );
var_dump( $r['result'] );
?>
This code would output something like:
array(242) {
[0] =>
array(2) {
'_id' => string(2) "AD"
'timezones' => array(1) { [0] => string(14) "Europe/Andorra" }
}
[1] =>
array(2) {
'_id' => string(2) "AE"
'timezones' => array(1) { [0] => string(10) "Asia/Dubai" }
}
[2] =>
array(2) {
'_id' => string(2) "AF"
'timezones' => array(1) { [0] => string(10) "Asia/Kabul" }
}
…
MongoCollection::aggregate() is implemented under the hood as a database command. The method in the PHP driver merely wraps this, but you can also call A/F through the MongoDB::command() method:
<?php
$m = new MongoClient;
$d = $m->demo;
$pipeline = [
[ '$group' => [
'_id' => '$country_code',
'timezones' => [ '$addToSet' => '$timezone' ]
] ],
[ '$sort' => [ '_id' => 1 ] ],
];
$r = $d->command( [
'aggregate' => 'cities',
'pipeline' => $pipeline,
] );
var_dump( $r['result'] );
?>
Because a database command only returns one document, the result is limited to a maximum of 16MB. This is not a problem for my example, but it can can certainly be a limiting factor for other A/F queries.
MongoDB 2.6 adds support for returning a cursor for an aggregation command. With the raw command interface, you simply add the extra cursor element:
$r = $d->command( [
'aggregate' => 'cities',
'pipeline' => $pipeline,
'cursor' => [ 'batchSize' => 1 ],
] );
var_dump( $r );
Instead of a document with all results inline, you get a cursor definition back:
array(2) {
'cursor' =>
array(3) {
'id' => class MongoInt64#5 (1) {
public $value => string(12) "392201189815"
}
'ns' => string(11) "demo.cities"
'firstBatch' => array(1) {
[0] =>
array(2) {
'_id' => string(2) "AD"
'timezones' => array(1) { [0] => string(14) "Europe/Andorra" }
}
}
}
'ok' => double(1)
}
The cursor definition contains the cursor ID (in id), the namespace (ns), and whether the command succeeded (in ok). The definition also a portion of the results. The number of items in firstBatch is configured by the value given to batchSize in the command.
To create a cursor that you can iterate over in PHP, you need to convert this cursor definition to a MongoCommandCursor object. You can do that with the MongoCommandCursor::createFromDocument() factory method. This factory method takes three arguments: the MongoClient object ($m in my example), the connection hash, and the cursor definition that was returned. The hash is required so that we can fetch new results from the same connection that executed the original command.
To obtain the connection hash, we need to include a by-ref variable as the third argument to MongoCollection::command():
<?php
$m = new MongoClient;
$d = $m->demo;
$pipeline = [
[ '$group' => [
'_id' => '$country_code',
'timezones' => [ '$addToSet' => '$timezone' ]
] ],
[ '$sort' => [ '_id' => 1 ] ],
];
$r = $d->command(
[
'aggregate' => 'cities',
'pipeline' => $pipeline,
'cursor' => [ 'batchSize' => 1 ],
],
null,
$hash
);
var_dump( $hash );
The hash looks like localhost:27017;-;.;26415. Together with the result, you can now construct a MongoCommandCursor:
$cursor = MongoCommandCursor::createFromDocument( $m, $hash, $r );
And iterate over it:
foreach ( $cursor as $result )
{
echo $result['_id'], ': ', join( ', ', $result['timezones'] ), "\n";
}
?>
As this is all a bit cumbersome, we have also added a helper method for this: MongoCollection::aggregateCursor. This internally does the whole MongoCommandCursor creation dance, and simplifies the previous example to:
<?php
$m = new MongoClient;
$c = $m->demo->cities;
$pipeline = [
[ '$group' => [
'_id' => '$country_code',
'timezones' => [ '$addToSet' => '$timezone' ]
] ],
[ '$sort' => [ '_id' => 1 ] ],
];
$r = $c->aggregateCursor( $pipeline );
foreach ( $r as $result )
{
echo $result['_id'], ': ', join( ', ', $result['timezones'] ), "\n";
}
?>
This helper also automatically sets the initial batch size to 101. You can change the batchSize for subsequent batches by using the MongoCommandCursor::batchSize() method, and for the initial batch by specifying an option to MongoCollection::aggregateCursor:
$options = [ 'cursor' => [ 'batchSize' => 5 ] ]; $r = $d->cities->aggregateCursor( $pipeline, $options ); $r->batchSize( 25 );
In general, you probably should not change the default batch sizes.
The Aggregation Framework has some other new features in MongoDB 2.6 as well. Please refer to the release notes for more information. I might write another post on some of those features later, too.
Life Line
Updated an alcohol shop
Updated 2 benches
Created a bench; Updated a bench
I hiked 19.0km in 4h35m50s
I hiked 19.0km in 4h35m50s
I walked 6.8km in 1h15m36s
Updated an estate_agent office
I walked 4.1km in 55m33s
I walked 1.1km in 10m05s
My First Lapwing!
I went to the London Wetland Centre yesterday, for a day out in nature.
While hiding in a hide, this chap and a friend showed up starting to forage for grubs.
#BirdPhotography #BirdsOfMastodon #Photography #Birds #London #Nature
Created a waste_basket; Updated a cafe and a restaurant; Confirmed an estate_agent office
I walked 6.6km in 1h8m53s
@bennuttall Are you at the Crucible this year again?
I walked 9.7km in 5h29m12s
Updated a gate
Staring Contest with a Squirrel
On my walk on the weekend, I sat down on a tree branch of a tree that had fallen over some time ago. Just listening to the birds.
Then after hearing rustling in the foliage above me, I looked up, and saw this chap staring at me.
I walked 3.0km in 41m38s
I walked 1.1km in 12m20s
Bluebell Carpet
I had a lovely walk on Hampstead Heath yesterday, finding all the nooks and crannies away from the busy paths.
This field of bluebells under the colourful tree was a stand-out quiet spot.
I walked 2.3km in 21m51s
Fix paths
Created a memorial
Created a bench
@Edent Seems like my Android stopped sending coordinates to @openbenches as well, which is surprising as I haven't updated anything as far as I'm aware. Could it be a problem with the reader in your side after the latest changes and the addition of the warning?
I walked 5.8km in 2h15m45s




Shortlink
This article has a short URL available: https://drck.me/aggrcur-ax5