I need to join more than two fields in two collections using aggregate $lookup. is it possible to join? please let me know if it is possible. Here i have two collections:
For Example:
"people" collections fields "city,state,country" in "country" collection fields "city_id,state_id,country_id", I want to join this three fields in following collections.
"People"
{
"_id" : 1,
"email" : "admin#gmail.com",
"userId" : "AD",
"userName" : "admin",
"city" : 1,
"state" : 1,
"country" : 1
}
"country"
{
"country_id" : 1,
"userId" : "AD",
"phone" : "0000000000",
"stateinfo":[{
"state_id" : 1,
"state_name" : "State1"
},{
"state_id" : 2,
"state_name" : "State2"
}
],
"cityinfo":[{
"city_id" : 1,
"city_name" : "city1"
},{
"city_id" : 2,
"city_name" : "city2"
}
]
}
This is probably a lot more simple than you think, considering that of course all of the "three" fields are contained within the one "country" document. So it's just a matter of doing the $lookup by "country_id" and then using the retrived content to populate the other fields.
var pipeline = [
{ "$lookup": {
"from": "country",
"localField": "country",
"foreignField": "country_id",
"as": "country"
}},
{ "$project": {
"email": 1,
"userId": 1,
"userName": 1,
"country": {
"$arrayElemAt": [
{ "$filter": {
"input": {
"$map": {
"input": "$country",
"as": "country",
"in": {
"country_id": "$$country.country_id",
"userId": "$$country.userId",
"phone": "$$country.phone",
"stateInfo": {
"$arrayElemAt": [
{ "$filter": {
"input": "$$country.stateInfo",
"as": "state",
"cond": { "$eq": [ "$$state.state_id", "$state" ] }
}},
0
]
},
"cityinfo": {
"$arrayElemAt": [
{ "$filter": {
"input": "$$country.cityinfo",
"as": "city",
"cond": { "$eq": [ "$$city.city_id", "$city" ] }
}},
0
]
}
}
}
},
"as": "country",
"cond": { "$eq": [ "$$country.userId", "$userId" ] }
}},
0
]
}
}}
]
db.people.aggregate(pipeline)
That should give you a result like:
{
"_id" : 1,
"email" : "admin#gmail.com",
"userId" : "AD",
"userName" : "admin",
"country" : {
"country_id" : 1,
"userId" : "AD",
"phone" : "0000000000",
"stateinfo": {
"state_id" : 1,
"state_name" : "State1"
},
"cityinfo": {
"city_id" : 1,
"city_name" : "city1"
}
}
So once the array is matched in by $lookup it all comes down to using $filter to do the matcing and $arrayElemAt to get the first match from each filtered array.
Since the outer array has "inner" arrays, you want to use $map for the "outer" source and apply $filter to each of it's "inner" arrays.
You can get more fancy with $let to get that "reduced" array content down to the returned sub-document and then just directly reference the resulting properties for an even "flatter" response, but the general concept of "matching" the array elements remains the same as above.
For a PHP structure translation:
$pipeline = array(
array(
'$lookup' => array(
'from' => 'country',
'localField' => 'country'
'foreignField' => 'country_id',
'as' => 'country'
)
)
array(
'$project' => array(
'email' => 1,
'userId' => 1,
'userName' => 1,
'country' => array(
'$arrayElemAt' => array(
array(
'$filter' => array(
'input' => array(
'$map' => array(
'input' => '$country',
'as' => 'country',
'in' => {
'country_id' => '$$country.country_id',
'userId' => '$$country.userId',
'phone' => '$$country.phone',
'stateInfo' => array(
'$arrayElemAt' => array(
array(
'$filter' => array(
'input' => '$$country.stateInfo',
'as' => 'state',
'cond' => array( '$eq' => array( '$$state.state_id', '$state' ) )
)
),
0
)
),
'cityinfo' => array(
'$arrayElemAt' => array(
array(
'$filter' => array(
'input' => '$$country.cityinfo',
'as' => 'city',
'cond' => array( '$eq' => array( '$$city.city_id', '$city' ) )
)
),
0
)
)
}
)
),
'as' => 'country',
'cond' => array( '$eq' => array( '$$country.userId', '$userId' ) )
)
),
0
)
)
)
)
);
$people->aggregate($pipeline);
You can usually check your PHP matches a JSON structure when you are working from a JSON example by dumping the pipeline structure:
echo json_encode($pipeline, JSON_PRETTY_PRINT)
And that way you cannot go wrong.
As another final note here, the process after the $lookup is done is quite "complex" even if very efficient. So I would advise that unless there is some need to take this aggregation pipeline further and actually "aggregate" something, then you are probably better off doing that "filtering" in client code rather than doing it on the server.
The client code to do the same thing is far less "obtuse" than what you need to tell the aggregation pipeline to do. So unless this "really" saves you a lot of bandwidth usage by reducing down the matched array, or indeed you if can just "lookup" by doing another query instead, then stick with doing it in code and/or do the seperate query.
Related
I have a Laravel Collection with a lot of duplicated items like that:
[
id: 'NAME1',
prop1: 'yes',
prop2: null,
prop3: 'bla',
prop4: null
],
[
id: 'NAME1',
prop1: null,
prop2: 'yes'
prop3: null,
prop4: 'bla'
]
And i want to merge the elements with the same 'id' property, and get a collection like that, preserving both properties:
[
id: 'NAME1',
prop1: 'yes',
prop2: 'yes',
prop3: 'bla',
prop4: 'bla'
]
When i use $collection->unique('id') i only get a collection like that, losing the prop2 and prop4 of
the second element:
[
id: 'NAME1',
prop1: 'yes',
prop2: null,
prop3: 'bla',
prop4: null
]
How can i solve it? I doesn't find any method of Laravel Collections which could merge elements of a Collection when one of the elements had a null key.
Here's a macro that will do what you want:
use Illuminate\Support\Collection;
Collection::macro('mergeByKey', function ($key) {
return $this->groupBy($key)->map(function($group) {
$filteredGroup = collect($group)->map(function($item) {
return collect($item)->reject(function($value, $key) {
return $value === null;
});
});
return array_merge(...$filteredGroup->toArray());
})->values();
});
Then you can use it on a collection like this:
$collection = collect([
[
'id' => 'NAME1',
'prop1' => 'yes',
'prop2' => null,
'prop3' => 'bla',
'prop4' => null
],
[
'id' => 'NAME1',
'prop1' => null,
'prop2' => 'yes',
'prop3' => null,
'prop4' => 'bla'
],
[
'id' => 'NAME2',
'prop1' => null,
'prop2' => 'fdsa',
'prop3' => null,
'prop4' => 'asdf'
],
[
'id' => 'NAME2',
'prop1' => 'fdsa',
'prop2' => null,
'prop3' => 'asdf',
'prop4' => null
],
]);
$result = $collection->mergeByKey('id');
Result:
Collection {#268 ▼
#items: array:2 [▼
0 => array:5 [▼
"id" => "NAME1"
"prop1" => "yes"
"prop3" => "bla"
"prop2" => "yes"
"prop4" => "bla"
]
1 => array:5 [▼
"id" => "NAME2"
"prop2" => "fdsa"
"prop4" => "asdf"
"prop1" => "fdsa"
"prop3" => "asdf"
]
]
}
So you want to merge all non-null properties for each id (you have only one ID in your list, but I assume there can be many)
1) group by id and get the list of [id => [all property lists for id]]
2) for each id:
2a) remove empty properties from each list
2b) merge all lists into one
It can be done this way with laravel collections:
$data = '[
{
"id": "NAME1",
"prop1": "yes",
"prop2": null,
"prop3": "bla",
"prop4": null
},
{
"id": "NAME1",
"prop1": null,
"prop2": "yes",
"prop3": null,
"prop4": "bla"
},
{
"id": "NAME2",
"prop1": "no",
"prop2": "dah",
"prop4": "bla"
}
]
';
$coll = collect(json_decode($data, JSON_OBJECT_AS_ARRAY))
->groupBy('id')
->map(function ($propGroup) {
//for each group of 'objects' of property lists
return $propGroup
->map(function ($props) {
//remove empty properties
return collect($props)->filter(function ($prop) {
return !empty($prop);
});
})
->reduce(function ($carry, $item) {
return $carry->merge($item);
}, collect());
});
I am developing a hotel booking availability check using laravel and mongodb. I am confused to write query to find available dates. I tried a method but it is like between and. I am not sure query is correct or not.
I have included laravel raw query and mongo query in this question. Both are same.
Why query is not correct?
Because, Guest checking on 2017-09-02 and checkout on 2017-09-05. Currently I am fetching data like 2017-09-02 >= checkin_from && 2017-09-05 <= checkin_from. If 2017-09-02 is the checkin_from date then, this is correct. But if checkin_from is 2017-08-25 and reserve_to is 2017-09-06. Here dates 2017-09-02 to 2017-09-05 includes. In this case how will we check?
Is this possible with query?
Or
Array 1: Fetch all the bookings and store in an array.
Array 2: Then prepare date (checkin and checkout date from frontend) using DatePeriod and DateInterval and store in an array. Then check matching array(1 & 2).
Which method do I follow?
Query Currently I am using
$bookings = Booking::raw(function ($collection) use ($dateBegin, $dateEnd) {
return $collection->aggregate([
[
'$match' => [
'cabinname' => 'test',
'checkin_from' => ['$gte' => $dateBegin, '$lte' => $dateEnd],
'is_delete' => 0,
],
],
[
'$project' => [
'beds' => 1,
'dormitory' => 1,
'sleeps' => 1,
'status' => 1,
'has status' => [
'$in' => ['status', ['1', '4', '5', '7']]
]
],
]
]);
});
Mongo Query
db.booking.aggregate([
{
$match: {
"cabinname" : 'Test',
"checkin_from" : {$gte :[ new Date ('2017-09-01') ], $lte : [ new Date ('2017-09-03') ] },
"is_delete" : 0,
}
},
{
$project: {
"beds" : 1,
"cabinname":1,
"checkin_from":1,
"reserve_to":1,
"has status" : {
$in: [ "status", ['1', '4', '5', '7'] ]
}
}
}
])
Data from database
{ "_id" : ObjectId("5888fbd5d2ae672245fb5f79"), "cabinname" : "Test", "checkin_from" : ISODate("2017-08-29T22:00:00Z"), "reserve_to" : ISODate("2017-09-03T22:00:00Z"), "beds" : "8" }
{ "_id" : ObjectId("58a4812bd2ae67066eeaea41"), "cabinname" : "Test", "checkin_from" : ISODate("2017-09-01T22:00:00Z"), "reserve_to" : ISODate("2017-09-05T22:00:00Z"), "beds" : "18" }
{ "_id" : ObjectId("58bac8a5d2ae67951845edaf"), "cabinname" : "Test", "checkin_from" : ISODate("2017-09-01T22:00:00Z"), "reserve_to" : ISODate("2017-09-02T22:00:00Z"), "beds" : "0" }
{ "_id" : ObjectId("58d03541d2ae671c668b4568"), "cabinname" : "Test", "checkin_from" : ISODate("2017-09-02T22:00:00Z"), "reserve_to" : ISODate("2017-09-04T22:00:00Z"), "beds" : "14" }
My query is not working because of wrong conditions. I have updated the conditions now I am getting exact results.
{checkin_from:{$lte: new Date ('2017-09-02')}, reserve_to:{$gt: new Date ('2017-09-02')}}
$bookings = Booking::raw(function ($collection) use ($dateBegin, $dateEnd) {
return $collection->aggregate([
[
'$match' => [
'cabinname' => 'test',
'checkin_from' => ['$lte' => $dateBegin],
'reserve_to' => ['$gt' => $dateBegin],
'is_delete' => 0,
],
],
[
'$project' => [
'beds' => 1,
'dormitory' => 1,
'sleeps' => 1,
'status' => 1,
'has status' => [
'$in' => ['status', ['1', '4', '5', '7']]
]
],
]
]);
});
So here is my attempt it did not work, I am not sure what I need to do, and the docs explaining this are just confessing.
so I am hoping someone on here knows how to do this.
Basic concept: I have stored the users latest profile picture ID in the users main profile collection
members collection
{
"_id" : ObjectId("aaaat656a464"),
"email" : "russell#ipet.xyz",
"personal" : {
"name" : {
"firstname" : "Russell",
"lastname" : "Harrower"
},
"profile_id" : ObjectId("333a0e2b7acebe9b869b1b0a")
},
"kst" : ObjectId("111111111111g")
}
Now I want to get that profile_id from members_media collection which _id = profile_id
This is the members_media collection
{
"_id" : ObjectId("333a0e2b7acebe9b869b1b0a"),
"data" : {
"url" : "https://scontent.xx.fbcdn.net/v/t1.0-1/11659354_1625058617782022_7822649569017662262_n.jpg?oh=6e2b74b33c1c4ea4cdc0094a1ae35e14&oe=59DF809A",
"type" : "facebook/image",
"date" : ISODate("2017-06-21T06:16:18.580Z"),
"profile_pic" : true
},
"uid" : ObjectId("111111111111g")
}
So here is my attempt
$check = $db->aggregate([
['$unwind'=> "$members_media"],
['$unwind' => ['path'=>"$members_media", 'includeArrayIndex'=>"arrayIndex"]],
['$lookup'=>
[
'from'=>"members_media",
'localField'=>"personal.profile_id",
'foreignField'=>"_id",
'as'=>"profilepic"
]
]
]);
However when I run the following it does not get the members collection
$db = static::db()->members;
/*$check = $db->findOne(['kst' => New MongoDB\BSON\ObjectId($_SESSION["ipet_user"]), 'personal'=>['$exists'=>true]],
['personal.profile_id' => 1]
);*/
$check = $db->aggregate([
['$lookup' =>
[
'from'=>"members_media",
'localField'=>"members.personal.profile_id",
'foreignField'=>"_id",
'as'=>"data"
]
],
['$match' =>
['members.kst' => New MongoDB\BSON\ObjectId($_SESSION["user"]), 'members.personal'=>['$exists'=>true]]
],
['$unwind' => ['path' => '$members_media.data', 'includeArrayIndex' => 'arrayIndex']],
['$project' =>
['members.profile_path'=>'$data.url']
]
]);
my result is
object(MongoDB\Driver\Cursor)#19 (9) { ["database"]=> string(4) "ipet" ["collection"]=> NULL ["query"]=> NULL ["command"]=> object(MongoDB\Driver\Command)#18 (1)
We can do like this
$resultData = $db->collectionName->aggregate([
['$lookup' => [
'from' => 'members_media',
'localField' => 'personal.profile_id',
'foreignField' => '_id',
'as' => 'profileDetail',
]]]);
$myData = array();
foreach($resultData as $result) {
//var_dump($result);
array_push($myData, $result);
}
print_r($myData);
This is my current output
Collection {#794 ▼
#items: array:8 [▼
"IN" => Collection {#795 ▶}
"NZ" => Collection {#787 ▶}
]}
I want the items to be hold the no of count for each codes like
"IN" => 4,
"NZ" => 3,
I know that I can directly write in a query like this
$query->groupBy('country_code')->orderBy('country_code', 'ASC');
return $query->get([
DB::raw('country_code as country_code'),
DB::raw('COUNT(*) as "count"')
]);
But I want the output from a already queried collection to reduce multiple queries which is a collection.
Right now I am only able to group by on the collection like this
$collection->groupBy('country_code');
$b = $a->groupBy('country_code');
You've done most of the job by proper grouping the data based on the country_code. Now it's just to iterate through the collection with a foreach, key, value and use the collections's count() method to count the number of elements stored under a given country_code
foreach ($b as $countryCode => $items) {
echo $items->count()."\n";
}
Reproduce:
php artisan ti
Psy Shell v0.7.2 (PHP 7.0.8-0ubuntu0.16.04.3 — cli) by Justin Hileman
>>> $cities = collect([['country_code' => 'pl', 'name' => 'Warszawa'], ['country_code' => 'pl', 'name' => 'Wrocław'], ['country_code' => 'de', 'name' => 'Berlin']]);
=> Illuminate\Support\Collection {#846
all: [
[
"country_code" => "pl",
"name" => "Warszawa",
],
[
"country_code" => "pl",
"name" => "Wrocław",
],
[
"country_code" => "de",
"name" => "Berlin",
],
],
}
>>> $grouped = $cities->groupBy('country_code');
=> Illuminate\Support\Collection {#836
all: [
"pl" => Illuminate\Support\Collection {#838
all: [
[
"country_code" => "pl",
"name" => "Warszawa",
],
[
"country_code" => "pl",
"name" => "Wrocław",
],
],
},
"de" => Illuminate\Support\Collection {#837
all: [
[
"country_code" => "de",
"name" => "Berlin",
],
],
},
],
}
>>> foreach ($grouped as $cCode => $cities) {
... echo $cCode . ' has '.$cities->count()."\n";
... }
pl has 2
de has 1
I am using codeigniter with mongodb library https://github.com/intekhabrizvi/Codeigniter-mongo-library
below is my collection "users". I want to count all the badges which has badge_slug = 100_club for userid 57b83ae9faa76bac338b4579 from users collection.
{
"_id" : ObjectId("57b83ae9faa76bac338b4579"),
"displayname" : "test",
"email" : "test#gmail.com",
"badges" : [
{
"awarded_at" : ISODate("2015-04-21T05:52:06Z"),
"object_id" : "",
"badge_slug" : "100_club"
},
{
"awarded_at" : ISODate("2015-04-21T06:12:14Z"),
"object_id" : "",
"badge_slug" : "100_club"
},
{
"awarded_at" : ISODate("2015-04-21T07:09:55Z"),
"object_id" : "",
"badge_slug" : "reader"
}
]
}
what I have done is
$ops = array(
array(
"$project" => array(
"count" => array(
"$size" => array(
"$filter" => array(
"input" => "$badges",
"as" => "badge",
"cond" => array("$eq" => => array("$$badge.badge_slug", "100_club") )
)
)
)
)
),
array(
"$group" => array(
"_id" => null,
"total" => array( "$sum" => "$count" )
)
)
);
$this->mongo_db->aggregate("users", $ops);
So the result should be 2 for given document.
But its returning count for all users. Where should I need to give userid condition?
You can use $match to filter documents before they're passed to next stage.