MongoDB queries - $condition by element in array - mongodb

I have the following aggregation pipeline consisting of a single $redact step:
(which is supposed to return all products for which the recorded number of sales exceeds the stock)
$redact:
{
$cond: [
{ $gt: ["$sales", "$productInfo.0.stock"] },
"$$KEEP",
"$$PRUNE"
]
}
(the syntax above is specific to Compass, so no issues here)
Where the entities look something like:
{
_id: 123,
sales: 60,
price: 80,
productInfo: [
{
stock: 100
}
]
}
However, the query above does not seem to work. My presumption is that the issue is caused by the comparison with $productInfo.0.stock, as replacing it with another direct attribute of the entity (e.g. price) runs without any issues.
How should such a query ($cond by $gt where one of the values is from an array/list) be specified?

The productionInfo.0.stock syntax is the Mongo Query Language (MQL), which can be used in a $match or a find.
That particular construct is not availabl when using aggregation syntax, such as "$fieldName". Instead, you need to use $arrayElemAt, but that unfortunately doesn't support accessing fields in the reference element.
To get what you want, you will need to add a field in a prior stage that retrieves the desired element from the array, reference that object in the $redact, and then project out the temporary field, such as:
{$addFields: {firstProduct: {$arrayElemAt: [ "$productInfo", 0 ]}}},
{$redact:{
$cond: [
{ $gt: ["$sales", "$productInfo.0.stock"] },
"$$KEEP",
"$$PRUNE"
]
}},
{$project: {firstProduct: 0}}

Related

Push an object to a nested array within a document but use a value from a field in the root document

I've been scratching my head with this problem. I've attempted to search for a solution but I didn't find anything relating to my specific use case.
Would anyone be able to help me out?
Say I have a collection of "discount" documents, and importantly they have an "amount" field to say how much the discount is worth. Whenever a discount is redeemed I currently want to track what the worth was at the time of the redemption.
To do this I've been attempting to use the following code:
await datastore.collection('discounts').updateOne(
{
$expr: { $gt: [ '$maxUses', '$uses' ] },
...criteria
},
{
$set: {
uses: 1
},
$push: {
redemptions: {
name: concatNames(user),
email: user.email,
amount: '$amount', // <-- use amount from root document
when: new Date()
}
}
}
)
Unfortunately $amount does not pull the value from the root document, instead it just becomes "$amount" as a string. I've also attempted to convert this update to use a pipeline but $push is not a valid pipeline stage.
Here's a quick Mongo playground link.
Thanks in advance.
In order to refer to another fields value, you'll need to use the aggregation pipeline form of update. However, '$push' is an update operator, not an aggregation operator.
$concatArrays gets most of the way there like
{$set: {redepmtions: {$concatArrays: [ "$redemptions", [{amount: "$amount"}]}}
That will throw an error if $redemptions doesn't already exist, so use $cond to subsitute an empty array in that case:
.updateOne(
{ ...criteria },
[{$set: {
redemptions: {$concatArrays: [
{$cond: [{$eq: ["array", {$type: "$redemptions"}]}, "$redemptions", []]},
[{amount: "$amount"}]
]}
}}]
)
Playground

How can I find all the documents that starts with a certain number

{
"_id":ObjectId("92837261829"),
"num":12
}
{
"_id":ObjectId("d4372643229"),
"num":25
}
What would be the mongodb syntax to get the document where "num" starts with a 1.
Note that value in "num" is a number not a string. So I guess db.collectionName.find({"num":{$regex:/1.*/}}) or something like this might not work.
the output of the syntax should be
{
"_id":ObjectId("92837261829"),
"num":12
}
You can actually check the $substr of the first character to see if it matches:
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": { "$eq": [ { "$substr": [ "$num", 0, 1 ] }, "1" ] },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
The logical condition is sent to $redact, which uses the "ternary" to take the action to either $$KEEP or $$PRUNE the document from the results.
From MongoDB 3.4 that should be replaced with either $substrCP or $substrBytes, as the former operator is deprecated and now aliases $substrBytes.
Alternately you can do the same thing with $where, albeit a little slower due to JavaScript evaluation:
db.collection.find(function() {
return this.num.toString().substr(0,1) === "1";
})
Note that both methods are doing a calculation and therefore must scan the full collection in order to get a result. So in production environments this is not a practical operation to do.
In production it would be better advised to store such a thing as a separate property, then you can query it directly and utilize and index to speed search results.

Conditionally include a field (_id or other) in mongodb project aggregation?

I've got a mongodb aggregation pipeline with a $project stage and I'd like to include certain fields only if conditions are met. Specifically, I'd like to exclude the _id in one condition and include a second field 'second_id' in the other condition.
I know that it's not possible (yet) to exclude fields from a mongodb $project, but is it possible to conditionally include them?
Is there a way to conditionally exclude the _id field? It accepts a 0 or 1, but what if I want to determine that 0 or 1 based on an if statement. How would this be done?
Pseudocode for _id:
$project: { _id: { $ifNull: [ "$user_id", 0 ] } }
The main use of this would be to use the doc.user_id as the result _id, or allow mongodb to create a new autoincrement _id if that user_id is null.
There isn't a way currently to do this within the $project stage, but you can use the $redact stage to remove the field conditionally (i.e. you set the value to 0 like you are doing in your example.
db.collection.aggregate(
... matching and stuff ...
{$project: { _id: { $ifNull: [ "$user_id", 0 ] } }},
{$redact: {
{$cond: {
if: { $eq: [ "$user_id", 0 ] },
then: '$$PRUNE',
else: '$$DESCEND'
}}
}
I have a trick for this one:
"_id": {"$cond": [{"$eq": ["$user_id", null]}, "$nonExistinField", "$user_id"]}
Maybe this was not doable when the question was asked, but now it is.
There is a way to conditionally include/exclude a particular field in $project stage.
This can be achieved using $cond to evaluate your condition, and then return either '$true' or '$false'
{
myConditionalField:{$cond:{
if:{
$eq:['$some_field','some_value']
},
then:'$false',
else:'$true'
}}
}
Here, the field myConditionalField will be projected only if the value of some_field from previous stage matched some_value.
NOTE: Its assumed here that myConditionalField already exists from a previous stage. If it doesn't, then the else should be $some_field or whatever field you want to be projected.

Find doc by relative array element positions

I want to search a field in array and want to know what is the index of that field in array is it possible to find it?
example:
doc1: {id:123, hobby:['chess','cards','dance']}
doc2: {id:123, hobby:['cards','dance','chess']}
I want to search only the document which has
chess and cards but only the one which has chess before card. So in case doc1.
You can do this with the $where operator, coupling it with the $all operator to keep it reasonably performant:
db.test.find({
// Find all the docs where hobby contains both 'cards' and 'chess'
hobby: {$all: ['cards', 'chess']},
// Of those, run this $where query that evaluates the indexes of the matched elements
$where: "this.hobby.indexOf('cards') > this.hobby.indexOf('chess')"
})
Add an index on hobby for best performance.
For the best performance you would actually use .aggregate() with a $match stage for a "query" to narrow down possible matches to documents that contain $all the elements in the array, and $redact to filter from the remaining results:
db.test.aggregate([
{ "$match": { "hobby": { "$all": [ "chess", "cards" ] } } },
{ "$redact": {
"$cond": {
"if": {
"$eq": [
{ "$filter": {
"input": "$hobby",
"as": "hobby",
"cond": {
"$or": [
{ "$eq": [ "$$hobby", "chess" ] },
{ "$eq": [ "$$hobby", "cards" ] }
]
}
}},
["chess","cards"]
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
The reason that works is because $filter can return only the elements that match the conditions given here, and returns them "in the same order" in which they are stored in the original document.
Once "filtered" down, the array should then be an exact match for the content ["chess","cards"] if indeed those elements in the original array were in that order.
Note that while it may "look" simpler, you cannot use "set operators" to test this since "sets" are not actually considered to be ordered.
So you need something that maintains the order, and $filter is really the only thing that can do this with array content in an efficient way. So the solution is indeed restricted to requiring a MongoDB 3.2 or greater release in order to actually work.
For older releases, see the answer from JohnnyHK using $where to apply an .indexOf() test on the array elements as the additional filter to using $all.
But if you have MongoDB 3.2 with the available operators, then the native coded operations available to the aggregation framework run much faster than the JavaScript evaluation of$where, and should be used in preference.

Can you do a MongoDB aggregate projection operator $in

Can you do a MongoDB aggregate projection operator {$cond: { '$field': {$in: ['val1', 'val2']}}}? Or I have to break it into $or?
Currently I have a:
$project: {
countNames: [
// if
{$or: [
{$eq: ['$name', 'Frist variant']},
{$eq: ['$name', 'Second variant']}
]
// then
1,
// else
0
]
}
I just wonder if there is a way to avoid those repeating $eq's, something like:
sum: {
$cond: [
'$name': {$in: ['Frist', 'Second']},
1,
0
]
}
Okay, I know that the version I wrote doesn't work (tried it), but is there an operator that can do it? Repeating $eq's gets old quickly when there are more of them, I sometimes resort to multi-stage pipe, or mapReduce instead. This would help up with some of my code.
For what it's worth, I'm currently on MongoDB 2.6, but I plan to switch production to Mongo 3 within weeks, so v3 solution will also be acceptable.