Is it possible to update a document using its own properties? - mongodb

I've tried and to no avail, heres the example:
db.examples.insert({
_id: 1,
topLevelValue: 10,
values: [
{value: 10},
{value: 14},
{value: 5}
]
})
db.examples.updateOne({_id: 1}, {
$set: {
'values.$.calculatedValue': {
$divide: ['values.$.value', '$topLevelValue']
}
}
})
The expected outcome is that all the values have a new field calculatedValue which is the result of its value divided by the topLevelValue
I am not saying this SHOULD work, I am saying that if what I want to do is possible via mongodb updates.

Related

Extract last value from an array of objects using monogdb query language?

I'm new to Mongo. By new I mean couple of hours new.
Basically I have this document structure:
{
_id: ObjectId("614513461af3bf569fdc420e"),
item: 'postcard',
status: 'A',
size: { h: 10, w: 15.25, uom: 'cm' },
instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ]
}
I would like if possible to extract particular field (i.e. its value) from instock's last element. In this case I just need to extract 35 i.e. qty field.
I have managed to do this:
db.offer.find( { _id: ObjectId("614513461af3bf569fdc420e") }, { instock: 1, _id: 0} )
Which results in :
{ instock: [ { warehouse: 'B', qty: 15 }, { warehouse: 'C', qty: 35 } ] }
I don't know how to reach to last object in array and than its qty field and everything needs to be as single query.
Aggregate solution
(requires MongoDB 5, else query would be a little bigger)
Query
filter for the _id with the $match stage
get last element of $instock, and then field qty
project to keep only the above part
*we do it like we would do it in a programming language, get last element, and get a field value.
Test code here
db.collection.aggregate([
{"$match": {"_id": ObjectId("614513461af3bf569fdc420e")}},
{
"$project": {
"_id": 0,
"qty": {"$getField": {"field": "qty","input": {"$last": "$instock"}}}
}
}
])

Why mongodb finds this document ($ne: null for array)

Data:
db.inventory.insertMany([
{ _id: 1, item: null },
{ _id: 2 },
{ _id: 3, item: 3 },
{ _id: 4 items: [1, 2, 3] },
{ _id: 5, items: [] }
])
Query 1:
db.inventory.find({ 'item': {$ne: null} })
Result 1:
{ _id: 3, item: 3 }
Query 2:
db.inventory.find({ 'items.0': {$ne: null} })
Result 2:
{ _id: 3, items: [1, 2, 3] },
{ _id: 4, items: [] }
Why mongoDB finds this document: { _id: 4, items: [] }?
Is this a bug?
Use $exists: true instead of $ne: null.
$ne docs explain:
$ne selects the documents where the value of the field is not equal to the specified value. This includes documents that do not contain the field.
null itself a datatype in mongodb. this is why at the 0 index of items
it is searching for a null value and not getting it. if you add null like
items: [null] and run the query again you will not see _id: 4
This is not a bug. this is how Mongo works.
In your first result, the key item in the object itself does not exist. To satisfy 'item': {$ne: null} the key itself has to be present in the object. This is how javascript works as well. Check the JS Code.
It also works with arrays. try: {'items.0': null} and {'items.0': {$ne: null}}, and you should see the difference.

MongoDB aggregation but not including certain items

I'm very new to MongoDB's aggregation framework, so I do not know properly how to do this.
I have a data model that is structured like this:
{
name: String,
store: {
item1: Number,
item2: Number,
item3: Number,
item4: Number,
},
createdAt: Date
}
I want to return the average price of every item'i'. I'm trying with this query:
db.commerces.aggregate([
{
$group: {
_id: "",
item1Avg: { $avg: "$store.item1"},
item2Avg: { $avg: "$store.item2"},
item3Avg: { $avg: "$store.item3"},
item4Avg: { $avg: "$store.item4"}
}
}
]);
The problem is that when an item has no price set, it's stored in the database as a "-1".
I don't want these values to pollute the average result. Is there any way to limit the agreggation to only take into account when price is > 0.
$match operator before $group is not a solution because I want to return all the average prices.
Thank you!
EDIT: Here you have of an example of the input & desired output:
[{
name: 'name',
store: {
item1: 10,
item2: -1,
item3: 12,
item4: 3,
}
},
{
name: 'name2',
store: {
item1: 10,
item2: -1,
item3: -1,
item4: 2,
}
},...]
An the desired output:
{
item1Avg: 10,
item2Avg: 0,
item3Avg: 12,
item4Avg: 2.5
}
You need to $unwind the store, then $match values to meet your condition, then $group ones that passed the test. Unfortunately there is no way to $unwind an object, so you need to $project it to array first:
db.commerces.aggregate([
{$project: {store:[
{item:{$literal:"item1"}, val:"$store.item1"},
{item:{$literal:"item2"}, val:"$store.item2"},
{item:{$literal:"item3"}, val:"$store.item3"},
{item:{$literal:"item4"}, val:"$store.item4"}
]}},
{$unwind:"$store"},
{$match: {"store.val":{$gt:0}}},
{$group: {_id:"$store.item", avg:{$avg:"$store.val"}}}
])
EDIT:
As #blakes-seven pointed, it may not work on versions < 3.2. An alternative approach with $map may work:
db.commerces.aggregate([
{$project: {
store: {
$map:{
input:[
{item:{$literal:"item1"}, val:"$store.item1"},
{item:{$literal:"item2"}, val:"$store.item2"},
{item:{$literal:"item3"}, val:"$store.item3"},
{item:{$literal:"item4"}, val:"$store.item4"}
],
as: "i",
in: "$$i"
}
}
}},
{$unwind:"$store"},
{$match: {"store.val":{$gt:0}}},
{$group: {_id:"$store.item", avg:{$avg:"$store.val"}}}
])

MongoDB Aggregation: Combine two arrays

I have the following type of documents stored in a collection.
{
"_id" : "318036:2014010100",
"data": [
{"flow": [6, 10, 12], "occupancy": [0.0356, 0.06, 0.0856], time: 0},
{"flow": [2, 1, 4], "occupancy": [0.01, 0.0056, 0.0422], time: 30},
...
]
}
I want to compute an aggregated value from the first, second, ..., nth value in the flow and occupancy arrays. The order within the array should be preserved. Assuming I want compute the sum. The result should look like the following:
{
"_id" : "318036:2014010100",
"data": [
{"flow": [6, 10, 12], "occupancy": [0.0356, 0.06, 0.0856], sum: [6.0356, 10.006, 12.00856], time: 0},
{"flow": [2, 1, 4], "occupancy": [0.01, 0.0056, 0.0422], sum: [2.01, 1.0056, 4.0422], time: 30},
...
]
}
I tried to solve this by using the aggregation framework but my current approach does not preserve the ordering and produces to much sums.
db.sens.aggregate([
{$match: {"_id":/^318036:/}},
{$limit: 1},
{$unwind: "$data"},
{$unwind: "$data.flow"},
{$unwind: "$data.occupancy"},
{
$group: {
_id: {id: "$_id", time: "$data.time", o: "$data.occupancy", f: "$data.flow", s: {$add: ["$data.occupancy", "$data.flow"]}}
}
},
{
$group: {
_id: {id: "$_id.id", time: "$_id.time"}, occ: { $addToSet: "$_id.o"}, flow: {$addToSet: "$_id.f"}, speed: {$addToSet: "$_id.s"}
}
}
])
I am not sure if it is possible to solve this problem with the aggregation framework, so a solution using MapReduce would also be fine. How can I produce the desired result?
An alternative solution with neither aggregation framework nor map/reduce:
db.sens.find().forEach(function (doc) {
doc.data.forEach(function(dataElement) {
var sumArray = [];
for (var j = 0; j < dataElement.flow.length; j++) {
sumArray[j] = dataElement.flow[j] + dataElement.occupancy[j];
}
dataElement.sum = sumArray;
collection.save(doc);
});
});

MongoDB: match non-empty doc in array

I have a collection structured thusly:
{
_id: 1,
score: [
{
foo: 'a',
bar: 0,
user: {user1: 0, user2: 7}
}
]
}
I need to find all documents that have at least one 'score' (element in score array) that has a certain value of 'bar' and a non-empty 'user' sub-document.
This is what I came up with (and it seemed like it should work):
db.col.find({score: {"$elemMatch": {bar:0, user: {"$not":{}} }}})
But, I get this error:
error: { "$err" : "$not cannot be empty", "code" : 13030 }
Any other way to do this?
Figured it out: { 'score.user': { "$gt": {} } } will match non-empty docs.
I'm not sure I quite understand your schema, but perhaps the most straight forward way would be to not have an "empty" value for score.user ?
Instead purposely not have that field in your document if it has no content?
Then your query could be something like ...
> db.test.find({ "score" : { "$elemMatch" : { bar : 0, "user" : {"$exists": true }}}})
i.e. looking for a value in score.bar that you want (0 in this case) checking for the mear existence ($exists, see docs) of score.user (and if it has a value, then it'll exist?)
editied: oops I missed the $elemMatch you had ...
You probably want to add an auxiliary array that keeps track of the users in the user document:
{
_id: 1,
score: [
{
foo: 'a',
bar: 0,
users: ["user1", "user2"],
user: {user1: 0, user2: 7}
}
]
}
Then you can add new users atomically:
> db.test.update({_id: 1, score: { $elemMatch: {bar: 0}}},
... {$set: {'score.$.user.user3': 10}, $addToSet: {'score.$.users': "user3"}})
Remove users:
> db.test.update({_id: 1, score: { $elemMatch: {bar: 0}}},
... {$unset: {'score.$.user.user3': 1}, $pop: {'score.$.users': "user3"}})
Query scores:
> db.test.find({_id: 1, score: {$elemMatch: {bar: 0, users: {$not: {$size: 0}}}}})
If you know you'll only be adding non-existent users and removing existent users from the user document, you can simplify users to a counter instead of an array, but the above is more resilient.
Look at the $size operator for checking array sizes.
$group: {
_id: '$_id',
tasks: {
$addToSet: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.id', ''],
},
'',
],
},
then: '$$REMOVE',
else: {
id: '$tasks.id',
description: '$tasks.description',
assignee: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.assignee._id', ''],
},
'',
],
},
then: undefined,
else: {
id: '$tasks.assignee._id',
name: '$tasks.assignee.name',
},
},
},
},
},
},
},