MongoDB $lookup on array of objects - mongodb

Categories
{
"_id" : ObjectId("61740086893f048528d166b9"),
"name": "Category1",
"tracks" : [
"61c65353565a2d9a1cd3020d",
"61c74518962dc3efb96c3438",
"61c74775703176a6f72df444"
]
}
Tracks
{
"_id" : ObjectId("61c65353565a2d9a1cd3020d"),
"name" : "Track1",
"categoryId" : ObjectId("61740086893f048528d166b9"),
"creatorId" : ObjectId("61c6478304e98ed63e8ee7d3"),
"thumbnailId" : ObjectId("61c65353565a2d9a1cd3020c"),
"plays" : [],
"media" : {
"type" : "wav",
"url" : ""
},
"status" : "approved",
"downloads" : [],
"uploadedDate" : 1640387411
}
Assuming that I have 5 categories and each category has many tracks ID, I wanna get N last tracks for each category so I used this code below
categories.aggregate([
{
$project: {
tracks: { $slice: ["$tracks", -2] },
},
},
]
And the response is
[
{
"_id": "61740086893f048528d166b9",
"tracks": [
"61c74518962dc3efb96c3438",
"61c74775703176a6f72df444"
]
},
{
"_id": "61740094893f048528d166c1",
"tracks": []
},
{
"_id": "617400a0893f048528d166cb",
"tracks": []
}
]
So far it's good, but the question is how can I replace each category's tracks from an array of IDs to an array of objects?
I tried $loopup but I probably didn't implement the localField correctly.
Expected result
[
{
"_id": "61740086893f048528d166b9",
"tracks": [
{
"_id": ObjectId("61c74518962dc3efb96c3438")
...
},
{
"_id": ObjectId("61c74775703176a6f72df444")
...
}
]
},
{
"_id": "61740094893f048528d166c1",
"tracks": []
},
{
"_id": "617400a0893f048528d166cb",
"tracks": []
}
]
***** UPDATE *****
I'm trying to replace the creatorId by createdBy which is an object of the users from the users collection
Users
{
"_id": ObjectId("61c6478304e98ed63e8ee7cb"),
"email": "USER888#gmail.com",
"username": "USER999",
"tracks": [
ObjectId("61c65353565a2d9a1cd3020d"),
],
}
The expected result should be
[
{
"_id": "61740086893f048528d166b9",
"tracks": [
{
"_id": ObjectId("61c74518962dc3efb96c3438"),
"createdBy": {
"_id": "userId"
...
},
...
},
{
"_id": ObjectId("61c74775703176a6f72df444"),
"createdBy": {
"_id": "userId"
...
}
...
}
]
},
{
"_id": "61740094893f048528d166c1",
"tracks": []
},
{
"_id": "617400a0893f048528d166cb",
"tracks": []
}
]
In addition to the solution below by ray, I added the code here https://mongoplayground.net/p/8AjmnL-vhtz
The createdBy is at the top level but not under every track

$lookup is the correct way for you to find the corresponding object in Tracks collection. Why your code does not work is that you are storing strings in tracks array in Categories collection; while the _id of Tracks collection is ObjectId. There will be no $lookup result as the datatypes do not match. What you can do is converting the strings to ObjectId by using $toObjectId in a $map, and then do the $lookup
db.categories.aggregate([
{
$project: {
tracks: {
$slice: [
"$tracks",
-2
]
}
}
},
{
$project: {
tracks: {
"$map": {
"input": "$tracks",
"as": "t",
"in": {
"$toObjectId": "$$t"
}
}
}
}
},
{
"$lookup": {
"from": "tracks",
let: {
t: "$tracks"
},
pipeline: [
{
$match: {
$expr: {
"$in": [
"$_id",
"$$t"
]
}
}
}
],
"as": "tracks"
}
}
])
Here is the Mongo playground for your reference.

Related

mongodb update document from first element of array

Consider a collection client with the following documents:
[
{
"id": 1,
"Name": "Susie",
"ownership" : {
"ownershipContextCode" : "C1"
},
"clientIds": [
{
"clientClusterCode": "clientClusterCode_1",
"clientId": "11"
}
]
},
{
"id": 2,
"Name": "John",
"ownership" : {
"ownershipContextCode" : "C2"
},
"clientIds": [
{
"clientClusterCode": "clientClusterCode_2",
"clientId": "22"
}
]
}
]
I am attempting to set a field (ownershipClientCode) as the first element of the clientIds array.
The result should be like that:
[
{
"id": 1,
"Name": "Susie",
"ownership" : {
"ownershipContextCode" : "C1",
"ownershipClientCode" : "clientClusterCode_1"
},
"clientIds": [
{
"clientClusterCode": "clientClusterCode_1",
"clientId": "11"
}
],
},
{
"id": 2,
"Name": "John",
"ownership" : {
"ownershipContextCode" : "C2",
"ownershipClientCode" : "clientClusterCode_2"
},
"clientIds": [
{
"clientClusterCode": "clientClusterCode_2",
"clientId": "22"
}
],
}
]
I'm using this query but I can't get sub object from the first element in the array
db.collection.aggregate([
{
$addFields: {
"Last Semester": {
"$arrayElemAt": [
"$clientIds",
0
]
}
}
}
])
This query add the all object but I want only the field (clientClusterCode).
Some thing like that
db.collection.aggregate([
{
$addFields: {
"Last Semester": {
"$arrayElemAt": [
"$clientIds",
0
].clientClusterCode
}
}
}
])
I'm using mongodb 4.0.0
You're very close: https://mongoplayground.net/p/HY1Pj0P4z12
db.collection.aggregate([
{
$addFields: {
"ownership.ownershipClientCode": {
"$arrayElemAt": [
"$clientIds.clientClusterCode",
0
]
}
}
}
])
You can use the dot notation within the $arrayElemAt as well as when you defining the field name.
To directly set the field, do something like this (use aggregation in the update): https://mongoplayground.net/p/js-usEJSH_A
db.collection.update({},
[
{
$set: {
"ownership.ownershipClientCode": {
"$arrayElemAt": [
"$clientIds.clientClusterCode",
0
]
}
}
}
],
{
multi: true
})
Note: The second method to update needs to be an array, so that it functions as an pipeline.

How to retrieve just the array values only of a nested field of MongoDB document? [duplicate]

This question already has answers here:
How to return just the nested documents of an array from all documents
(2 answers)
Closed 3 years ago.
I'm trying to deep query and retrieve specific fields from MongoDB, but unfortunately couldn't able to figure out the correct solution.
Document data:
[ {
"_id": 39127198,
"name": "Mike",
"details": {
"age": 25,
"vehicles":[
{"brand":"Chevrolet","model":"Silverado","plate":"AB11"},
{"brand":"Jeep","model":"Cherokee","plate":"CG678"}
]
}
}, {
"_id": 39127198,
"name": "Taylor",
"details": {
"age": 25,
"vehicles": [
{"brand":"GMC","model":"Sierra","plate":"748397"}
]
}
} ]
My requirement: Return "vehicles" array alone for a specific player. Let's say for user "Mike" in this case.
Here is what I tried;
collection.find( {"name":"Mike"} )
.project( {"details.vehicles" : 1, "_id": 0, "name": 0} )
.toArray(function(err, result) { ... } )
collection.aggregate([
{ $match: { "name":"Mike" } },
{ $project: {"details.vehicles" : 1, "_id": 0, "name": 0} }
]).toArray(function(err, result) { ... } )
Here is what I get for the above code:
[
{
"details": {
"vehicles": [
{"brand":"Chevrolet","model":"Silverado","plate":"AB11"},
{"brand":"Jeep","model":"Cherokee","plate":"CG678"}
]
}
}
]
Expected:
[
{"brand":"Chevrolet","model":"Silverado","plate":"AB11"},
{"brand":"Jeep","model":"Cherokee","plate":"CG678"}
]
I am using MongoClient. MongoDB shell version v4.2.1
You can use $unwind and $replaceRoot stages to achieve this :
db.collection.aggregate([
{
$match: {
"name": "Mike"
}
},
{
$unwind: "$details.vehicles"
},
{
$replaceRoot: {
newRoot: "$details.vehicles"
}
}
])
Will output exactly what you need.
Hope it helps
The query:
db.vehi.aggregate( [
{ $match: { "name":"Mike" } },
{ $project: { "vehicles": "$details.vehicles", "_id": 0 } }
] ).next().vehicles
The exact output:
[
{
"brand" : "Chevrolet",
"model" : "Silverado",
"plate" : "AB11"
},
{
"brand" : "Jeep",
"model" : "Cherokee",
"plate" : "CG678"
}
]
- OR -
This also gets the same result:
db.vehi.find(
{ "name" : "Mike" },
{ "details.vehicles" : 1, _id : 0 }
).next().details.vehicles

$elemMatch against two Array elements if one fails

A bit odd but this is what I am looking for.
I have an array as follow:
Document 1:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "1234"
}
Document 2:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "0000"
}
I would like to use a single query, and send a filter on ZipCode = 1111 && ZipCode4 = 4321, if this fails, the query should look for ZipCode = 1111 && ZipCode4: 0000
Is there a way to do this in a single query ? or do I need to make 2 calls to my database ?
For matching both data set (11111/4321) and (11111/0000), you can use $or and $and with $elemMatch like the following :
db.test.find({
$or: [{
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "4321" }
}
}]
}, {
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "0000" }
}
}]
}]
})
As you want conditional staging, this is not possible but we can get closer to it like this :
db.test.aggregate([{
$match: {
$or: [{
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "4321" }]
}, {
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "0000" }]
}]
}
}, {
$project: {
Items: 1,
match: {
"$map": {
"input": "$Items",
"as": "val",
"in": {
"$cond": [
{ $and: [{ "$eq": ["$$val.ZipCode", "11111"] }, { "$eq": ["$$val.ZipCode4", "4321"] }] },
true,
false
]
}
}
}
}
}, {
$unwind: "$match"
}, {
$group: {
_id: "$match",
data: {
$push: {
_id: "$_id",
Items: "$Items"
}
}
}
}])
The first $match is for selecting only the items we need
The $project will build a new field that check if this items is from the 1st set of data (11111/4321) or the 2nd set of data (11111/0000).
The $unwind is used to remove the array generated by $map.
The $group group by set of data
So in the end you will have an output like the following :
{ "_id" : true, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394972"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] }, { "_id" : ObjectId("58af69ac594b51730a394974"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] } ] }
{ "_id" : false, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394971"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "0000" } ] } ] }
Your application logic can check if there is _id:true in this output array, just take the corresponding data field for _id:true. If there is _id:false in this object take the corresponding data field for _id:false.
In the last $group, you can also use $addToSet to builds 2 field data1 & data2 for both type of data set but this will be painful to use as it will add null object to the array for each one of the opposite type :
"$addToSet": {
"$cond": [
{ "$eq": ["$_id", true] },
"$data",
null
]
}
Here is a gist

MongoDB select distinct and count

I have a product collection which looks like that:
products = [
{
"ref": "1",
"facets": [
{
"type":"category",
"val":"kitchen"
},
{
"type":"category",
"val":"bedroom"
},
{
"type":"material",
"val":"wood"
}
]
},
{
"ref": "2",
"facets": [
{
"type":"category",
"val":"kitchen"
},
{
"type":"category",
"val":"livingroom"
},
{
"type":"material",
"val":"plastic"
}
]
}
]
I would like to select and count the distinct categories and the number of products that have the category (Note that a product can have more than one category). Something like that:
[
{
"category": "kitchen",
"numberOfProducts": 2
},
{
"category": "bedroom",
"numberOfProducts": 1
},
{
"category": "livingroom",
"numberOfProducts": 1
}
]
And it would be better if I could get the same result for each different facet type, something like that:
[
{
"facetType": "category",
"distinctValues":
[
{
"val": "kitchen",
"numberOfProducts": 2
},
{
"val": "livingroom",
"numberOfProducts": 1
},
{
"val": "bedroom",
"numberOfProducts": 1
}
]
},
{
"facetType": "material",
"distinctValues":
[
{
"val": "wood",
"numberOfProducts": 1
},
{
"val": "plastic",
"numberOfProducts": 1
}
]
}
]
I am doing tests with distinct, aggregate and mapReduce. But can't achieve the results needed. Can anybody tell me the good way?
UPDATE:
With aggregate, this give me the different facet categories that a product have, but not the values nor the count of different values:
db.products.aggregate([
{$match:{'content.facets.type':'category'}},
{$group:{ _id: '$content.facets.type'} }
]).pretty();
The following aggregation pipeline will give you the desired result. In the first pipeline step, you need to do an $unwind operation on the facets array so that it's deconstructed to output a document for each element. After the $unwind stage is the first of the $group operations which groups the documents from the previous stream by category and type and calculates the number of products in each group using $sum. The next $group operation in the next pipeline stage then creates the array that holds the aggregated values by using $addToSet operator. The final pipeline stage is the $project operation which then transforms the document in the stream by modifying existing fields:
var pipeline = [
{ "$unwind": "$facets" },
{
"$group": {
"_id": {
"facetType": "$facets.type",
"value": "$facets.val"
},
"count": { "$sum": 1 }
}
},
{
"$group": {
"_id": "$_id.facetType",
"distinctValues": {
"$addToSet": {
"val": "$_id.value",
"numberOfProducts": "$count"
}
}
}
},
{
"$project": {
"_id": 0,
"facetType": "$_id",
"distinctValues": 1
}
}
];
db.product.aggregate(pipeline);
Output
/* 0 */
{
"result" : [
{
"distinctValues" : [
{
"val" : "kitchen",
"numberOfProducts" : 2
},
{
"val" : "bedroom",
"numberOfProducts" : 1
},
{
"val" : "livingroom",
"numberOfProducts" : 1
}
],
"facetType" : "category"
},
{
"distinctValues" : [
{
"val" : "wood",
"numberOfProducts" : 1
},
{
"val" : "plastic",
"numberOfProducts" : 1
}
],
"facetType" : "material"
}
],
"ok" : 1
}

MongoDB query with conditional group by statement

I need to export customer records from database of mongoDB. Exported customer records should not have duplicated values. "firstName+lastName+code" is the key to DE-duped the record and If there are two records present in database with same key then I need to give preference to source field with value other than email.
customer (id,firstName,lastName,code,source) collection is this.
If there are record 3 records with same unique key and 3 different sources then i need to choose only one record between 2 sources(TV,internet){or if there are n number of sources i need the one record only}not with the 'email'(as email will be choosen when only one record is present with the unique key and source is email)
query using:
db.customer.aggregate([
{
"$match": {
"active": true,
"dealerCode": { "$in": ["111391"] },
"source": { "$in": ["email", "TV", "internet"] }
}
},
{
$group: {
"_id": {
"firstName": "$personalInfo.firstName",
"lastName": "$personalInfo.lastName",
"code": "$vehicle.code"
},
"source": {
$addToSet: { "source": "$source" }
}
}
},
{
$redact:
{
$cond: [
{ $eq: [{ $ifNull: ["$source", "other"] }, "email"] },
"$$PRUNE",
"$$DESCEND"
]
}
},
{
$project:
{
"source":
{
$map:
{
"input": {
$cond: [
{ $eq: [{ $size: "$source" }, 0] },
[{ "source": "email" }],
"$source"
]
},
"as": "inp",
"in": "$$inp.source"
}
},
"record": { "_id": 1 }
}
}
])
sample output:
{ "_id" : { "firstName" : "sGI6YaJ36WRfI4xuJQzI7A==", "lastName" : "99eQ7i+uTOqO8X+IPW+NOA==", "code" : "1GTHK23688F113955" }, "source" : ["internet"] }
{ "_id" : { "firstName" : "WYDROTF/9vs9O7XhdIKd5Q==", "lastName" : "BM18Uq/ltcbdx0UJOXh7Sw==", "code" : "1G4GE5GV5AF180133" }, "source" : ["internet"] }
{ "_id" : { "firstName" : "id+U2gYNHQaNQRWXpe34MA==", "lastName" : "AIs1G33QnH9RB0nupJEvjw==", "code" : "1G4GE5EV0AF177966" }, "source" : ["internet"] }
{ "_id" : { "firstName" : "qhreJVuUA5l8lnBPVhMAdw==", "lastName" : "petb0Qx3YPfebSioY0wL9w==", "code" : "1G1AL55F277253143" }, "source" : ["TV"] }
{ "_id" : { "firstName" : "qhreJVuUA5l8lnBPVhMAdw==", "lastName" : "6LB/NmhbfqTagbOnHFGoog==", "code" : "1GCVKREC0EZ168134" }, "source" : ["TV", "internet"] }
This is a problem with this query please suggest :(
Your code doesn't work, because $cond is not an accumulator operator. Only these accumulator operators, can be used in a $group stage.
Assuming your records contain not more than two possible values of source as you mention in your question, you could add a conditional $project stage and modify the $group stage as,
Code:
db.customer.aggregate([
{
$group: {
"_id": {
"id": "$id",
"firstName": "$firstName",
"lastName": "$lastName",
"code": "$code"
},
"sourceA": { $first: "$source" },
"sourceB": { $last: "$source" }
}
},
{
$project: {
"source": {
$cond: [
{ $eq: ["$sourceA", "email"] },
"$sourceB",
"$sourceA"
]
}
}
}
])
In case there can be more that two possible values for source, then you could do the following:
Group by the id, firstName, lastName and code. Accumulate
the unique values of source, using the $addToSet operator.
Use $redact to keep only the values other than email.
Project the required fields, if the source array is empty(all the elements have been removed), add a
value email to it.
Unwind the source field to list it as a field and not an array.
(optional)
Code:
db.customer.aggregate([
{
$group: {
"_id": {
"id": "$id",
"firstName": "$firstName",
"lastName": "$lastName",
"code": "$code"
},
"sourceArr": { $addToSet: { "source": "$source" } }
}
},
{
$redact: {
$cond: [
{ $eq: [{ $ifNull: ["$source", "other"] }, "email"] },
"$$PRUNE",
"$$DESCEND"
]
}
},
{
$project: {
"source": {
$map: {
"input":
{
$cond: [
{ $eq: [{ $size: "$sourceArr" }, 0] },
[{ "source": "item" }],
"$sourceArr"]
},
"as": "inp",
"in": "$$inp.source"
}
}
}
}
])