Can I avoid using the same $match criteria twice when using $unwind? - mongodb

Take the following data as an example:
{
_id: 1,
item: "abc",
stock: [
{ size: "S", color: "red", quantity: 25 },
{ size: "S", color: "blue", quantity: 10 },
{ size: "M", color: "blue", quantity: 50 }
]
}
{
_id: 2,
item: "def",
stock: [
{ size: "S", color: "blue", quantity: 20 },
{ size: "M", color: "blue", quantity: 5 },
{ size: "M", color: "black", quantity: 10 },
{ size: "L", color: "red", quantity: 2 }
]
}
{
_id: 3,
item: "ijk",
stock: [
{ size: "M", color: "blue", quantity: 15 },
{ size: "L", color: "blue", quantity: 100 },
{ size: "L", color: "red", quantity: 25 }
]
}
Say I'm going to filter out the stocks that matches the criteria size = 'L'. I already have a multikey index on the stock.size field.
In the aggregation pipeline, if I use the following two operations:
[{$unwind: {path: "$stock"}},
{$match: {"stock.size": "L"}}]
I will get the desired results, but when the db gets very large, the $unwind step will have to scan the whole collection, without utilizing the existing index, which is very inefficient.
If I reverse the order of the $unwind and $match operations, the $match will utilize the index to apply an early filtering, but the final result will not be as desired: it will fetch the extra stocks that are not of size L, but have sibling L-sized stocks that belong to the same item.
Would I have to use the same $match operation twice, i.e. both before and after the $unwind, to make it both utilizing the index and return the correct results?

Yes you can use $match stage twice in the aggregation pipeline but here only the first $match stage will use the index second one will do the collscan.
[
{ "$match": { "stock.size": "L" }},
{ "$unwind": { "path": "$stock" }},
{ "$match": { "stock.size": "L" }}
]
If you want to avoid the $match twice then use $filter aggregation
[
{ "$match": { "stock.size": "L" } },
{ "$addFields": {
"stock": {
"$filter": {
"input": "$stock",
"as": "st",
"cond": { "$eq": ["$stock.size", "L"] }
}
}
}}
]

Related

Mongoose - Filter Subdocuments inside multiple documents and get as a plain array

I have an ecommerce app that has Products with multiple variants. So, variants are stored inside an array in each Product Object.
[
{
title: "Test"
description: "test description",
....
....
variants: [
{
color: "red",
size: "L"
....
....
price: 500
},
{
color: "red",
size: "M"
....
....
price: 500
},
{
color: "red",
size: "S"
....
....
price: 500
},
]
}
]
Is there a way to filter these products variants by its ID and return as a plain array of variants ignoring they're from different parents but with parent details ?
[
{
color: "red",
size: "L"
....
....
price: 500,
parent: {
title: "Test"
description: "test description"
}
},
{
color: "red",
size: "M"
....
....
price: 500,
parent: {
title: "Test"
description: "test description"
}
},
{
color: "red",
size: "S"
....
....
price: 500,
parent: {
title: "Test"
description: "test description"
}
},
{
color: "orange",
size: "S"
....
....
price: 500,
parent: {
title: "Test 2"
description: "test description 2"
}
},
]
Query1
add one field named parent with the fields that are in the root, except variants
remove those fields, keep the variants
map to add the parent field inside the array variants
unwind variants and replace the root
Playmongo
aggregate(
[{"$set": {"parent": {"title": "$title", "description": "$description"}}},
{"$unset": ["title", "description"]},
{"$set":
{"variants":
{"$map":
{"input": "$variants",
"in": {"$mergeObjects": ["$$this", {"parent": "$parent"}]}}}}},
{"$unwind": "$variants"},
{"$replaceRoot": {"newRoot": "$variants"}}])
Query2
same as above just with less stages, 1 set to do the 3 first stages
Playmongo
aggregate(
[{"$set":
{"title": "$$REMOVE",
"description": "$$REMOVE",
"variants":
{"$map":
{"input": "$variants",
"in":
{"$mergeObjects":
["$$this",
{"parent":
{"title": "$title", "description": "$description"}}]}}}}},
{"$unwind": "$variants"}, {"$replaceRoot": {"newRoot": "$variants"}}])

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

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.

MongoDB grouping based on intervals

I would like to group my data based on number intervals in measurements. Can I do this with the aggregation framework, or with some map-reduce function?
I would like to group by color and whether the size is larger or smaller than 5. I would also want to add e.g. "medium" for sizes between 3 and 5.
I can group by size and color, but then each different size will have its own object.
I know this can be done by checking each different object's size by db.collection.find(), and then adding them according to my specifications, but that would be very slow.
Example:
Objects:
{
color: "red",
size: 2
}
{
color: "red",
size: 4
}
{
color: "blue",
size: 2
}
{
color: "blue",
size: 1
}
{
color: "blue",
size: 7
}
Output:
{
_id: {
color: "red",
size: "small"
}
total size: 6
}
{
_id: {
color: "red",
size: "large"
}
total size: 0
}
{
_id: {
color: "blue",
size: small
}
total size: 3
}
{
_id: {
color: "blue",
size: "large"
}
total size: 7
}
This is easy using $cond:
db.collection.aggregate([
{ "$group": {
"_id": {
"color": "$color",
"size": {
"$cond": [
{ "$lt": [ "$size", 3 ] },
"small",
{ "$cond": [
{ "$lt": [ "$size", 6 ] },
"medium",
"large"
]}
]
}
},
"total_size": { "$sum": "$size" }
}}
])
So just conditionally select the value in the grouping key based on the current value in the document and count.