$lookup result sorted using a key from another array - arrays

I have the following data structures:
playlist collection:
{
_id:123,
artistId:959789,
title:'playlist1',
tracks:[{trackId:123456,createdDate:03.02.2017},
{trackId:213556,createdDate:04.02.2017},
{trackId:956125,createdDate:05.02.2017}]
},
{
_id:456,
artistId:456456,
title:'playlist2',
tracks:[{trackId:956336,createdDate:03.02.2017},
{trackId:213556,createdDate:09.02.2017},
{trackId:785556,createdDate:011.02.2017}]
},
{
_id:456,
artistId:456456,
title:'playlist3',
tracks:[{trackId:636985,createdDate:01.02.2017},
{trackId:456585,createdDate:06.02.2017},
{trackId:785556,createdDate:09.02.2017}]
}
The trackId in the tracks array of a playlist is the _id of a track in track collection
tracks collection:
{_id:956336,title:'abc'},
{_id:785556,title:'cdf'},
{_id:456585,title:'ghi'},
{_id:213556,title:'xyz'},
{_id:636985,title:'lmn'}
What i did was an aggregate $lookup using the trackId in the tracks array and i got the result. But the playlistTracks was sorted in some other order not in the order of the tracks array order.
{
$match: {artistId: 456}
},
{
$lookup: {
from: 'tracks',
localField: 'tracks.trackId',
foreignField: '_id',
as: 'playlistTracks'
}
},
Now what I need is to get the list of playlists by a particular artist having the following structure :
The playlistTracks should be sorted in the order on the createdDate in the tracks array.
{
_id:456,
title:'playlist2',
tracks:[{trackId:636985,createdDate:01.02.2017},
{trackId:456585,createdDate:06.02.2017},
{trackId:785556,createdDate:09.02.2017}]
playlistTracks:[{_id:956336,title:'abc'},
{_id:213556,title:'xyz'},
{_id:785556,title:'cdf'}]
},
{
_id:456,
title:'playlist2',
tracks:[{trackId:636985,createdDate:01.02.2017},
{trackId:456585,createdDate:06.02.2017},
{trackId:785556,createdDate:09.02.2017}]
playlistTracks:[{_id:636985,title:'lmn'},
{_id:456585,title:'ghi'},
{_id:785556,title:'cdf'}]
}

Follow below steps
1 unwind the tracks array in playlist collection
2 $lookup match with tracks collection
3 add createddate of tracks array to lookup result as a new key
4 sort based on new key
5 group the results for your requirements

So these are the documents I added, to reproduce your use case:
Playlist collection
{
"_id" : NumberInt(123),
"artistId" : NumberInt(959789),
"title" : "playlist1",
"tracks" : [
{
"trackId" : NumberInt(123456),
"createdDate" : "03.02.2017"
},
{
"trackId" : NumberInt(213556),
"createdDate" : "04.02.2017"
},
{
"trackId" : NumberInt(956125),
"createdDate" : "05.02.2017"
}
]
}
{
"_id" : NumberInt(456),
"artistId" : NumberInt(456456),
"title" : "playlist2",
"tracks" : [
{
"trackId" : NumberInt(956336),
"createdDate" : "03.02.2017"
},
{
"trackId" : NumberInt(213556),
"createdDate" : "09.02.2017"
},
{
"trackId" : NumberInt(785556),
"createdDate" : "11.02.2017"
}
]
}
{
"_id" : NumberInt(457),
"artistId" : NumberInt(456456),
"title" : "playlist3",
"tracks" : [
{
"trackId" : NumberInt(636985),
"createdDate" : "01.02.2017"
},
{
"trackId" : NumberInt(456585),
"createdDate" : "06.02.2017"
},
{
"trackId" : NumberInt(785556),
"createdDate" : "09.02.2017"
}
]
}
I changed the last duplicate _id on the playlist collection with _id: 457. I don't know how you could have two documents with same _id. _id field has to be unique. And I'm not sure I understand correct your desired result, because in your $match query your write the following: $match: {artistId: 456} but in your data there is no artiseId with 456.
and this date
{trackId:785556,createdDate:011.02.2017}
from document id_ 456 I changed to
{trackId:785556,createdDate:"11.02.2017"}
cause the date looked weird. It also looks like your date fields are strings, cause it certainly doesn't look like a date field. Either way the $sort works for both usecases.
The tracks collection I left as in your example.
So this seems to be what you need?
db.playlist.aggregate([
{
$match: {_id: {$in: [456]}}
},
{ $unwind: "$tracks"},
{$sort: {"tracks.createdDate": 1}},
{
$lookup: {
from: 'tracks',
localField: 'tracks.trackId',
foreignField: '_id',
as: 'playlistTracks'
}
},
{
$group:{
_id: "$_id",
artistId: {$first: "$artistId"},
title: {$first: "$title"},
tracks: { $push: { item: "$tracks.trackId", quantity: "$tracks.createdDate" } },
playlistTracks: { $push: "$playlistTracks" }
}
}
])
This puts both arrays into same order. You can specify here {$sort: {"tracks.createdDate": 1}} if you want ascending or descending -1 order
So before looking up the fields you can unwind and sort you playlist array.
Hope this works

Related

Find all matching elements in the array

Can someone please help me with this query ??
Query >>> Find all warehouses that keep item "Planner" and having in-stock quantity less than 20
This is the sample document in the items collection of the Inventory database :
{
"_id" : ObjectId("6067640da9a907175caaca34"),
"id" : 101,
"name" : "Planner",
"status" : "A",
"height" : 12,
"tags" : [
"mens",
"womens"
],
"warehouses" : [
{
"name" : "Phoenix",
"quantity" : 25
},
{
"name" : "Quickshift",
"quantity" : 15
},
{
"name" : "Poona",
"quantity" : 10
}
]
}
This is what I have tried doing :
db.items.find({"name":"Planner","warehouses.quantity":{"$lt":20}},{"warehouses":1,"_id":0}).pretty()
But it gives me the result as
{
"warehouses" : [
{
"name" : "Phoenix",
"quantity" : 25
},
{
"name" : "Quickshift",
"quantity" : 15
},
{
"name" : "Poona",
"quantity" : 10
}
]
}
Demo - https://mongoplayground.net/p/IpD5ypWSZyt
Use aggregation query
db.collection.aggregate([
{ $match: { "name": "Planner" } },
{ $unwind: "$warehouses" }, // break into individual documents
{ $match: { "warehouses.quantity": { $lt: 20 } } }, // query the data
{ $group: { _id: "_id", warehouses: { $push: "$warehouses" } } } // join them back
])
Demo - https://mongoplayground.net/p/pdTY0IkIqgF
Use $elemMatch only if you think there will be only 1 array element matching per document
The $elemMatch operator matches documents that contain an array field with at least one element that matches all the specified query criteria.
The $elemMatch operator limits the contents of an field from the query results to contain only the first element matching the $elemMatch condition.
db.collection.find({
"name":"Planner",
"warehouses": { "$elemMatch": { "quantity": { $gt: 20 } } }
},
{ "warehouses.$": 1})
https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection

Query Mongodb Sum of firsts element of an array of objects

I would like to write a query for summing each field payment of the first object inside an array, for each element of my database.
The schema is the following:
var schema = new Schema({
plate : String,
category : String,
brand : String,
model : String,
sign : String,
tax : [{
date : { type: Date, default: Date.now },
payment : { type: Number, default: 0 },
}],
});
I wrote the following function for my query:
function(callback){
Machine.aggregate(
[
{$unwind: "$tax"},
{$group : {
_id : null ,
tot : { $sum: "$tax.payment"}
}}
]
,callback);
}
But in this way I retrieve the sum of all the payments inside the array tax. My goal is to take only the first, so I tried with $tax.0.payment and using arrayElemAt : [$tax,0] but all my trials gave a tot = 0.
The idea here is pick out the first element of each of payment array field via $arrayElemAt with projection and then group-sum the field $group $sum.
Query:
db.collection.aggregate([
{
$project: {
firstPayment: {
$arrayElemAt: [
"$tax",
0
]
}
}
},
{
$group: {
_id: null,
PaymentSum: {
$sum: "$firstPayment.payment"
}
}
}
]);
Demo O/P:
[
{
"PaymentSum": 11,
"_id": null
}
]
Machine.aggregate({$unwind:
{path: "$tax"}
},
{$group:{
_id: "$_id",
payment: {$first: "$tax.payment"}
}},
{$group: {
_id: null,
total: {$sum: "$payment"}
}}
)
Explanation:
First I used $unwind on tax, then in the first $group stage I grouped them according to _id,
that way I will get the first payment information from unwinded tax array.
Then I used $sum to add them in the second $group stage.
I tested with this data:
Machine collection docs:
{
"_id" : ObjectId("5dbf09a4d7912bcbc61ee9e4"),
"tax" : [
{
"payment" : 10
},
{
"payment" : 20
}
]
},
{
"_id" : ObjectId("5dbf09aad7912bcbc61ee9e5"),
"tax" : [
{
"payment" : 30
},
{
"payment" : 40
}
]
},
{
"_id" : ObjectId("5dbf09afd7912bcbc61ee9e6"),
"tax" : [
{
"payment" : 50
},
{
"payment" : 60
}
]
}
The result I got is:
{ "_id" : null, "tot" : 90 }
I hope this fulfills your requirements.
Add $arrayElemAt in your aggregate like this..
Machine.aggregate(
[
{$unwind: "$tax"},
{$group : {
_id : null ,
tot : { $sum: { $arrayElemAt : [ "$tax.payment", 0 ]}
}}
]
,callback);

Sort by deep document field in MongoDb

I have a collection called Visitor which has an array of chats and each array has a document called user.
I need to find some documents on this collection and sort them by if they have some specific user in their chats first.
The path for the user id is:
chats.user._id
where:
chats // array
user // document
_id // ObjectId
The below script does sort the documents correctly, however, it expands the chats array and multiplies the document for each chat in the array.
I only need the sorting, so can I sort and not use the unwind pipeline or make it somehow not multiply the documents?
db.getCollection('Visitor').aggregate([
{$unwind: "$chats"},
{ $match: {'event._id':ObjectId('5c942a3591deb389bfd92579'), 'chats.enabled': {$exists: true}}},
{
"$project": {
"_id": 1,
"chats.user._id": 1,
"weight": {
"$cond": [
{ "$eq": [ "$chats.user._id", ObjectId("5c942a3591deb389bfd92579") ] },
10,
0
]
}
}
},
{ "$sort": { "weight": -1 } },
])
EDIT: I don't need to sort the inner array, but sort the find command by checking if a specific user is in the chats array.
Some sample of Visitor collection:
[
{
"_id" : ObjectId("5c9a3a1bd86e0ba64106e90e"),
"event" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"chats" : [
{
"enabled" : false,
"user" : {
"_id" : ObjectId("5c81232f09a923b559763418")
},
"_id" : ObjectId("5c9a3a1bd86e0ba64106e915")
}
]
},
{
"_id" : ObjectId("5c9a3a35d86e0ba64106e950"),
"event" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"chats" : [
{
"enabled" : true,
"user" : {
"_id" : ObjectId("5c81232f09a923b559763418")
},
"_id" : ObjectId("5c9a3a35d86e0ba64106e957")
},
{
"enabled" : true,
"user" : {
"_id" : ObjectId("5c942a3591deb389bfd92579")
},
"_id" : ObjectId("5c9a3a34d86e0ba64106e91d")
}
]
}
]
In the above sample, I need to make the second document to be sorted first because it has the user with the _id ObjectId("5c942a3591deb389bfd92579").
The problem here is that using $unwind you modify initial structure of your documents (you will get one document per chats. I would suggest using $map to get an array of weights based on specified userId and then you can use $max to get final weight
db.col.aggregate([
{ $match: {'event._id':ObjectId('5c942a3591deb389bfd92579'), 'chats.enabled': {$exists: true}}},
{
"$project": {
"_id": 1,
"chats.user._id": 1,
"weight": {
$max: { $map: { input: "$chats", in: { $cond: [ { $eq: [ "$$this.user._id", ObjectId("5c942a3591deb389bfd92579") ] }, 10, 0 ] } } }
}
}
},
{ "$sort": { "weight": -1 } },
])

Using the results of a MongoDB db.collection.aggregate to exclude results from a db.collection.find

I am programming a MongoDB function in AngularJS and Node and I have a single collection that has a field called documentId that is not unique. I want to find all docs in that collection where NONE of the status fields of a particular document contains the word open and only include the documents where ALL of the status' are either "closed" or "canceled".
Ex:
_id: <val> , companyId : "CO-000001", documentId : "001", status:"closed", value:"123.45"
_id: <val> , companyId : "CO-000001", documentId : "002", status:"closed", value:"323.67"
_id: <val> , companyId : "CO-000001", documentId : "001", status:"open", value:"434.56"
_id: <val> , companyId : "CO-000001", documentId : "002", status:"canceled", value:"523.16"
I want my results to ONLY include documentId 002 and not documentId 001 because one of the documents with documentId 001 contains a status of open. But since all documents with documentId 002 have a status of closed or canceled, I want to include it in my result.
I can get the aggregate of what documents I want to exclude from a db.collection.find() using the following...
db.tickets.aggregate(
[
{
"$match" : {
"companyId" : "CO-000001",
"$and" : [
{
"status" : {
"$in" : [
"open","on hold"
]
}
}
]
}
},
{
"$group" : {
"_id" : null,
"uniqueValues" : {
"$addToSet" : "$documentId"
}
}
}
],
{
"allowDiskUse" : false
}
);
but now I want to use the array results from the aggregate in a query when doing my...
db.tickets.find("companyId":"CO-000001", $and:[{documentId:{$nin:[<results from aggregation>]}}])
Is there a way to use the results from the aggregation inside another query? or is there a way to do all of this within an aggregation?
Change the projections according to your need. observe how I used group and match stage later to get the required data. Hope this solves your problem.
db.tickets.aggregate(
[
{
"$match" : {
"companyId" : "CO-000001"
}
},
{
"$group" : {
"_id" : "$documentId",
"status" :{
"$addToSet" : "$status"
}
}
},
{
$match:{ status:{$nin:["open","on hold"]} }
},{
$lookup:{
from:"tickets",
as: "final",
localField: "_id",
foreignField: "documentId",
}
},
{
$project :{
_id:0,
final:1
}
}
],
{
"allowDiskUse" : false
}
).pretty();

MongoDB get count of particular key in an array

In mongoDB, how can we get the count of particular key in an array
{
"_id" : ObjectId("52d9212608a224e99676d378"),
"business" : [
{
"name" : "abc",
"rating" : 4.5
},
{
"name" : "pqr"
},
{
"name" : "xyz",
"rating" : 3.6
}
]
}
in the above example, business is an array (with "name" and/or "rating" keys)
How can i get the count of business array with only "rating" key existing ?
Expected output is : 2
Looks like you have to use Aggregation Framework. In particular you need to $unwind your array, then match only elements with rating field included, then $group documents back to original format.
Try something like this:
db.test.aggregate([
{ $match: { /* your query criteria document */ } },
{ $unwind: "$business" },
{ $match: {
"business.rating": { $exists: 1 }
}
},
{ $group: {
_id: "$_id",
business: { $push: "$business" },
business_count: { $sum: 1 }
}
}
])
Result will look like the following:
{
_id: ObjectId("52d9212608a224e99676d378"),
business: [
{ name: "abc", rating: 4.5 },
{ name: "xyz", rating: 3.6 }
],
business_count: 2
}
UPD Looks like OP doesn't want to group results by wrapping document _id field. Unfortunately $group expression must specify _id value, otherwise it fails with exception. But, this value can actually be constant (e.g. plain null or 'foobar') so there will be only one resulting group with collection-wise aggregation.

Resources