How to get distinct combinations of two fields from a collection when one of the fields is in an array of subdocuments - mongodb

From a collection consisting of documents representing products similar to the following:
[
{
code: "0WE3A5CMY",
name: "lorem",
category: "voluptas",
variants: [
{
color: "PapayaWhip",
stock: 17,
barcode: 4937310396997
},
{
color: "RoyalBlue",
stock: 13,
barcode: 9787252504890
},
{
color: "DodgerBlue",
stock: 110,
barcode: 97194456959791
}
]
},
{
code: "0WE3A5CMX",
name: "ipsum",
category: "temporibus",
variants: [
{
color: "RoyalBlue",
stock: 113,
barcode: 23425202111840
},
{
color: "DodgerBlue",
stock: 10,
barcode: 2342520211841
}
]
},
{
code: "0WE3A5CMZ",
name: "dolor",
category: "temporibus",
variants: [
{
color: "MaroonRed",
stock: 17,
barcode: 3376911253701
},
{
color: "RoyalBlue",
stock: 12,
barcode: 3376911253702
},
{
color: "DodgerBlue",
stock: 4,
barcode: 3376911253703
}
]
}
]
I would like to retrieve distinct combinations of variants.color and category. So the result should be:
[
{
category: 'voluptas',
color: 'PapayaWhip',
},
{
category: 'voluptas',
color: 'RoyalBlue',
},
{
category: 'voluptas',
color: 'DodgerBlue',
},
{
category: 'temporibus',
color: 'RoyalBlue',
},
{
category: 'temporibus',
color: 'DodgerBlue',
}
]
Based on some cursory research I think I will have to use an aggregate but I've never worked with those and could use some help. I've tried the solution at How to efficiently perform "distinct" with multiple keys?
I've tried the method mentioned by jcarter in the comments but it doesn't solve my problem. If I do:
db.products.aggregate([
{
$group: {
_id: {
"category": "$category",
"color": "$variants.color"
}
}
}
])
I get the result:
[
{
"_id": {
"category": "temporibus",
"color": [
"MaroonRed",
"RoyalBlue",
"DodgerBlue"
]
}
},
{
"_id": {
"category": "temporibus",
"color": [
"RoyalBlue",
"DodgerBlue"
]
}
},
{
"_id": {
"category": "voluptas",
"color": [
"PapayaWhip",
"RoyalBlue",
"DodgerBlue"
]
}
}
]
Which isn't what I need.

Since variants is an array you need to unwind it & group on two fields to get unique docs based on category + 'variants.color' combo.
As group stage results something like :
[
{
"_id": {
"category": "voluptas",
"color": "DodgerBlue"
}
},
{
"_id": {
"category": "voluptas",
"color": "PapayaWhip"
}
}
]
then using $replaceRoot stage you can make _id object field as root for each document to get desired result.
Query :
db.collection.aggregate([
{
$unwind: "$variants"
},
{
$group: { _id: { "category": "$category", "color": "$variants.color" } }
},
{
$replaceRoot: { newRoot: "$_id" }
}
])
Test : mongoplayground

Related

How to find documents from multiple collection with similar field value

I have two collections:
Product
{ id: "1", model: "Light1", category: "Light"},
{ id: "2", model: "Light3", category: "Light"},
{ id: "3", model: "Lock1", category: "Lock"},
Item
{ id: "1", model: "Light1", category: "Light", color: "Blue"},
{ id: "2", model: "Light2", category: "Light", color: "Blue"},
{ id: "3", model: "Lock1", category: "Lock", color: "Blue"},
{ id: "4", model: "Light3", category: "Light", color: "Blue"}
{ id: "5", model: "Lock2", category: "Lock", color: "Blue"},
I want to find documents from the Item collection containing both model and category from the product collection.
From the example above, I want to get this so called new collection:
{ id: "1", model: "Light1", category: "Light", color: "Blue"},
{ id: "3", model: "Lock1", category: "Lock", color: "Blue"},
{ id: "4", model: "Light3", category: "Light", color: "Blue"}
You can try this aggregation query:
First $lookup from Item collection to join collections. This lookup uses a pipeline where you match the desired values: Local model is equal to foreign model and local category is equal to foreign category. This produces an array as output: if there is not any match the array will be empty.
So you can $match to not shown empty result array.
And use $project to output the values you want.
db.Item.aggregate([
{
"$lookup": {
"from": "Product",
"let": {
"model": "$model",
"category": "$category"
},
"pipeline": [
{
"$match": {
"$and": [
{
"$expr": {
"$eq": [
"$model",
"$$model"
]
}
},
{
"$expr": {
"$eq": [
"$category",
"$$category"
]
}
}
]
}
}
],
"as": "result"
}
},
{
"$match": {
"result": {
"$ne": []
}
}
},
{
"$project": {
"_id": 0,
"result": 0
}
}
])
Example here

Comparing 2 fields from $project in a mongoDB pipeline

In a previous post I created a mongodb query projecting the number of elements matching a condition in an array. Now I need to filter this number of elements depending on another field.
This is my db :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name: "beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand: "Bosch",
size: 195,
cooler: true,
color: "grey",
nbMax: 2
},
{
_id: 2,
items: [
{
itemId: 1,
name: "beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand: "Electrolux",
size: 200,
cooler: true,
color: "white",
nbMax: 2
},
]
}
This is my query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Electrolux"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
$size: {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
}
},
brand: 1,
cooler: 1,
color: 1,
nbMax: 1
}
}
])
The runnable example.
Which gives me this :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 1,
"nbMax": 2
},
{
"_id": 2,
"brand": "Electrolux",
"color": "white",
"cooler": true,
"itemsNumber": 2,
"nbMax": 2
}
]
What I expect is to keep only the results having a itemsNumber different from nbMax. In this instance, the second fridge with _id:2 would not match the condition and should not be in returned. How can I modify my query to get this :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 1,
"nbMax": 2
}
]
You can put a $match stage with expression condition at the end of your query,
$ne to check both fields should not same
{
$match: {
$expr: { $ne: ["$nbMax", "$itemsNumber"] }
}
}
Playground

Display the conditionnal size of an array with the others fields of a mongodb document

I have a collection of fridges and I would like to have some fields from each fridge matching a condition plus the 'conditionnal size' of the items in this fridge.
This is an example of my DB :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand:"Bosch",
size:195,
cooler:true,
color:"grey"
},
{
_id: 2,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand:"Electrolux",
size:200,
cooler:true,
color:"white"
},
]
}
I want to get fridges with these mutuals conditions ('and' condition):
brand is $in ["Bosch","Samsung"]
color is $in ["grey","white"]
In addition :
The number of items with a name $in ["beer","lettuce"]
And finally :
Removing some fields like the size and items of the result.
In our example, the excepted output would be :
{
_id:1
itemsNumber:1,
brand:"Bosch",
cooler:true,
color:"grey"
}
Explanations :
We removed the field items and size, itemsNumber counts the number of beers and lettuce from items array. And we only keep the first fridge its brand is Bosch and it's grey.
This what I have so far :
db.fridges.aggregate([
{
"$match": {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
"$project": {
"itemsNumber": {
$size: "$items" // This is not good
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Which returns me :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 2
}
]
Counting the items matching with either beer or lettuce is my main problem.
This is an executable example.
Thanks in advance !
I found out how to make it work. Thank you #joe for suggesting to use filter this was indeed the solution.
Here is the complete query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Runnable example.

How to replace/insert a subdocument in nested array in MongoDB in just one command

I have a collection persons with an array of answers:
[{ _id: ObjectId("5abd0550dcc44451cf3272ef"), "name": "Mike",
"answers": [ { "questionId": 118, "aaa": "xyz", "bbb": "xyz" } ] },
{ _id: ObjectId("5abd0550dcc44451cf3272ab"), "name": "John",
"answers": [ { "questionId": 101, "aaa": "xyz", "ccc": "xyz" } ] },
{ _id: ObjectId("5abd0550dcc44451cf327212"), "name": "Els",
"answers": [ { "questionId": 101, "aaa": "qrt", "ccc": "qrt" } ] },
{ _id: ObjectId("5abd0550dcc44451cf32724d"), "name": "Josefien",
"answers": [
{ "questionId": 109, "sss": "xyz", "ttt": "xyz" },
{ "questionId": 110, "nnn": "xyz", "mmm": "xyz" },
{ "questionId": 111, "kkk": "xyz", "lll": "xyz" },
]}]
Now I have a new answer from one person. When the questionId already exists I want to replace the whole subdocument answer. If not I want to add a new subdocument answer.
I tried this, but doesn't work, giving an error if it's a new subdocument:
db.persons.update(
{ _id: ObjectId("5abd0550dcc44451cf3272ef"), "answers.questionId": answer.questionId },
{ $set: { "answers.$": answer } },
{ upsert: true }
)
This is working fine:
db.persons.update(
{ _id: ObjectId("5abd0550dcc44451cf3272ef") },
{ $pull: { answers: { questionId: answer.questionId } } }
);
db.persons.update(
{ _id: ObjectId("5abd0550dcc44451cf3272ef") },
{ $addToSet : { answers: answer } }
)
But is there a way to do this in just one command?
Imagine that the persons collection has 10 millions of documents in real life. Also every person gives many new answers. In that case one transaction above two would be highly preferred.
This should work:
db.persons.update(
{ _id: ObjectId("5abd0550dcc44451cf3272ef"), "answers.questionId": answer.questionId },
{ $set: { 'answers.$': answer }}
);

MongoDB Query - query on values of any key in a sub-object: $match combined with $elemMatch

How can I filter on all userIDs that have color blue and size 50 in the same element of the list? Only user 1347 should be output.
{
"userId": "12347",
"settings": [
{ name: "SettingA", color: "blue", size: 10 },
{ name: "SettingB", color: "blue", size: 20 },
{ name: "SettingC", color: "green", size: 50 }
],
}
{
"userId": "1347",
"settings": [
{ name: "SettingA", color: "blue", size: 10 },
{ name: "SettingB", color: "blue", size: 50 },
{ name: "SettingC", color: "green", size: 20 }
]
}
If this can be done with $elemMatch, how can I include it in the following query, assuming the following two elements needs to be in the same list: { "rounds.round_values.decision" : "Fold"},
{ "rounds.round_values.gameStage" : "PreFlop"}
I tried this query but it doesn't yield any results. I've read that because elemMatch deosnt' work in projections. But how can I tell $filter to only return objects that have the $elemmMatch conditions met?
db.games.aggregate([
{ $match: { $and: [
{ Template: "PPStrategy4016" },
{ FinalOutcome: "Lost" }]
}},
{ $elemMatch: {
{ "rounds.round_values.decision" : "Fold"},
{ "rounds.round_values.gameStage" : "PreFlop"}
} },
{
$group: {
_id: null,
total: {
$sum: "$FinalFundsChange"
}
}
} ] )
Following the given documents, the query is something such as follows:
db.games.aggregate(
{$unwind : "$settings"},
{$match: {"settings.color" : "blue", "settings.size" : 50}} ,
{$group: {_id: null, total: {$sum: "$settings.size"}}} )
If you have difficulties in transforming it into your own domain, pleas supply some example documents from your domain.