How to use $set and dot notation to update embedded array elements using corresponding old element? - mongodb

I have following documents in a MongoDb:
from pymongo import MongoClient
client = MongoClient(host='my_host', port=27017)
database = client.forecast
collection = database.regions
collection.delete_many({})
regions = [
{
'id': 'DE',
'sites': [
{
'name': 'paper_factory',
'energy_consumption': 1000
},
{
'name': 'chair_factory',
'energy_consumption': 2000
},
]
},
{
'id': 'FR',
'sites': [
{
'name': 'pizza_factory',
'energy_consumption': 3000
},
{
'name': 'foo_factory',
'energy_consumption': 4000
},
]
}
]
collection.insert_many(regions)
Now I would like to copy the property sites.energy_consumption to a new field sites.new_field for each site:
set_stage = {
"$set": {
"sites.new_field": "$sites.energy_consumption"
}
}
pipeline = [set_stage]
collection.aggregate(pipeline)
However, instead of copying the individual value per site, all site values are collected and added as an array. Intead of 'new_field': [1000, 2000] I would like to get 'new_field': 1000 for the first site:
{
"_id": ObjectId("61600c11732a5d6b103ba6be"),
"id": "DE",
"sites": [
{
"name": "paper_factory",
"energy_consumption": 1000,
"new_field": [
1000,
2000
]
},
{
"name": "chair_factory",
"energy_consumption": 2000,
"new_field": [
1000,
2000
]
}
]
},
{
"_id": ObjectId("61600c11732a5d6b103ba6bf"),
"id": "FR",
"sites": [
{
"name": "pizza_factory",
"energy_consumption": 3000,
"new_field": [
3000,
4000
]
},
{
"name": "foo_factory",
"energy_consumption": 4000,
"new_field": [
3000,
4000
]
}
]
}
=> What expression can I use to only use the corresponding entry of the array?
Is there some sort of current-index operator:
$sites[<current_index>].energy_consumption
or an alternative dot operator (would remind me on difference between * multiplication and .* element wise matrix multiplication)?
$sites:energy_consumption
Or is this a bug?
Edit
I also tried to use the "$" positional operator, e.g. with
sites.$.new_field
or
$sites.$.energy_consumption
but then I get the error
FieldPath field names may not start with '$'
Related:
https://docs.mongodb.com/manual/reference/operator/aggregation/set/#std-label-set-add-field-to-embedded
In MongoDB how do you use $set to update a nested value/embedded document?

If the field is member of an array by selecting it you are selecting all of them.
{ar :[{"a" : 1}, {"a" : 2}]}
"$ar.a" = [1 ,2]
Also you cant mix update operators with aggregation, you cant use things like
$sites.$.energy_consumption, if you are doing aggregation you have to use aggregate operators, with only exception the $match stage where you can use query operators.
Query
alternative slightly different solution from yours using $setField
i guess it will be faster, but probably little difference
no need to use javascript it will be slower
this is >= MongoDB 5 solution, $setField is new operator
Test code here
aggregate(
[{"$set":
{"sites":
{"$map":
{"input":"$sites",
"in":
{"$setField":
{"field":"new_field",
"input":"$$this",
"value":"$$this.energy_consumption"}}}}}}]
)

use $addFields
db.collection.update({},
[
{
"$addFields": {
"sites": {
$map: {
input: "$sites",
as: "s",
in: {
name: "$$s.name",
energy_consumption: "$$s.energy_consumption",
new_field: {
$map: {
input: "$sites",
as: "value",
in: "$$value.energy_consumption"
}
}
}
}
}
}
}
])
mongoplayground

I found following ugly workarounds that set the complete sites instead of only specifying a new field with dot notation:
a) based on javascript function
set_stage = {
"$set": {
"sites": {
"$function": {
"body": "function(sites) {return sites.map(site => {site.new_field = site.energy_consumption_in_mwh; return site})}",
"args": ["$sites"],
"lang": "js"
}
}
}
}
b) based on map and mergeObjects
set_stage = {
"$set": {
"sites": {
"$map": {
"input": "$sites",
"in": {
"$mergeObjects": ["$$this", {
"new_field": "$$this.energy_consumption_in_mwh"
}]
}
}
}
}
}
If there is some kind of $$this context for the dot operator expression, allowing a more elegant solution, please let me know.

Related

How can I sum the lengths of arrays that are properties of an object inside of another array?

I'm trying to sum up the number of years a player played which is set up as an array that is a property of an object which is in another array. I'm trying to do this in MongoDB but not sure what I need to do, I feel like I may be over complicating this. Here is my document structure. I'm trying to use aggregates to accomplish this.
{
"_id": "/players/h/hunteto01.shtml",
"url": "/players/h/hunteto01.shtml",
"name": "Torii Hunter",
"image": "https://www.baseball-reference.com/req/202108020/images/headshots/7/79f9873b_br.jpg",
"teams": [
{
"name": "MIN",
"years": [
1997,1998,1999,2000,
2001,2002,2003,2004,
2005,2006,2007,2015
]
},
{
"name": "LAA",
"years": [
2008,2009,2010,
2011,2012
]
},
{
"name": "DET",
"years": [
2013,2014
]
}
],
"searchName": "torii hunter"
}
In this example I would I want to see output of something like careerLength: 19
$set - Set the careerLength field.
1.1. $reduce - Iterate elements in the teams array and transform the result to a numeric value.
1.1.1. $sum - Sum the accumulated value ($$value) with the size of the years array for the current iterated element via $size.
db.collection.aggregate([
{
$set: {
careerLength: {
$reduce: {
input: "$teams",
initialValue: 0,
in: {
$sum: [
"$$value",
{
$size: "$$this.years"
}
]
}
}
}
}
}
])
Demo # Mongo Playground

mongodb query to filter the array of objects using $gte and $lte operator

My doucments:
[{
"_id":"621c6e805961def3332bcf97",
"title":"monk plus",
"brand":"venture electronics",
"category":"earphones",
"variant":[
{
"price":1100,
"impedance":"16ohm"
},
{
"price":1600,
"impedance":"64ohm"
}],
"salesCount":185,
"buysCount":182,
"viewsCount":250
},
{
"_id":"621c6dab5961def3332bcf92",
"title":"nokia1",
"brand":"nokia",
"category":"mobile phones",
"variant":[
{
"price":10000,
"RAM":"4GB",
"ROM":"32GB"
},
{
"price":15000,
"RAM":"6GB",
"ROM":"64GB"
},
{
"price":20000,
"RAM":"8GB",
"ROM":"128GB"
}],
"salesCount":34,
"buysCount":21,
"viewsCount":80
}]
expected output
[{
_id:621c6e805961def3332bcf97
title:"monk plus"
brand:"venture electronics"
category:"earphones"
salesCount:185
viewsCount:250
variant:[
{
price:1100
impedance:"16ohm"
}]
}]
I have tried this aggregation method
[{
$match: {
'variant.price': {
$gte: 0,$lte: 1100
}
}},
{
$project: {
title: 1,
brand: 1,
category: 1,
salesCount: 1,
viewsCount: 1,
variant: {
$filter: {
input: '$variant',
as: 'variant',
cond: {
$and: [
{
$gte: ['$$variant.price',0]
},
{
$lte: ['$$variant.price',1100]
}
]
}
}
}
}}]
This method returns the expected output, now my question is there any other better approach that return the expected output.Moreover thank you in advance, and as I am new to nosql database so I am curious to learn from the community.Take a note on expected output all properties of particular document must return only the variant array of object I want to filter based on the price.
There's nothing wrong with your aggregation pipeline, and there are other ways to do it. If you just want to return matching documents, with only the first matching array element, here's another way to do it. (The .$ syntax only returns the first match unfortunately.)
db.collection.find({
// matching conditions
"variant.price": {
"$gte": 0,
"$lte": 1100
}
},
{
title: 1,
brand: 1,
category: 1,
salesCount: 1,
viewsCount: 1,
// only return first array element that matched
"variant.$": 1
})
Try it on mongoplayground.net.
Or, if you want to use an aggregation pipeline and return all matching documents in entirety except for the filtered array, you could just "overwrite" the array with the elements you want using "$set" (or its alias "$addFields"). Doing this means you won't need to "$project" anything.
db.collection.aggregate([
{
"$match": {
"variant.price": {
"$gte": 0,
"$lte": 1100
}
}
},
{
"$set": {
"variant": {
"$filter": {
"input": "$variant",
"as": "variant",
"cond": {
"$and": [
{ "$gte": [ "$$variant.price", 0 ] },
{ "$lte": [ "$$variant.price", 1100 ] }
]
}
}
}
}
}
])
Try it on mongoplayground.net.
your solution is good, just make sure to apply your $match and pagination before applying this step for faster queries

MongoDb - Update all properties in an object using MongoShell

I have a collection with many documents containing shipping prices:
{
"_id": {
"$oid": "5f7439c3bc3395dd31ca4f19"
},
"adapterKey": "transport1",
"pricegrid": {
"10000": 23.66,
"20000": 23.75,
"30000": 23.83,
"31000": 43.5,
"40000": 44.16,
"50000": 49.63,
"60000": 50.25,
"70000": 52,
"80000": 56.62,
"90000": 59,
"100000": 62.5,
"119000": 68.85,
"149000": 80,
"159000": 87,
"179000": 94,
"199000": 100.13,
"249000": 118.5,
"299000": 138.62,
"999000": 208.63
},
"zones": [
"25"
],
"franco": null,
"tax": 20,
"doc_created": {
"$date": "2020-09-30T07:54:43.966Z"
},
"idConfig": "0000745",
"doc_modified": {
"$date": "2020-09-30T07:54:43.966Z"
}
}
In pricegrid, all the properties can be different from one grid to another.
I'd like to update all the prices in the field "pricegrid" (price * 1.03 + 1).
I tried this :
db.shipping_settings.updateMany(
{ 'adapterKey': 'transport1' },
{
$mul: { 'pricegrid.$': 1.03 },
$inc: { 'pricegrid.$': 1}
}
)
Resulting in this error :
MongoServerError: Updating the path 'pricegrid.$' would create a conflict at 'grille.$'
So I tried with only $mul (planning on doing $inc in another query) :
db.livraison_config.updateMany(
{ 'adapterKey': 'transport1' },
{
$mul: { 'pricegrid.$': 1.03 }
}
)
But in that case, I get this error :
MongoServerError: The positional operator did not find the match needed from the query.
Could you please direct me on the correct way to write the request ?
You can use an aggregation pipeline in an update. $objectToArray pricegrid to convert it into an array of k-v tuple first. Then, do a $map to perform the computation. Finally, $arrayToObject to convert it back.
db.collection.update({
"adapterKey": "transport1"
},
[
{
$set: {
pricegrid: {
"$objectToArray": "$pricegrid"
}
}
},
{
"$set": {
"pricegrid": {
"$map": {
"input": "$pricegrid",
"as": "p",
"in": {
"k": "$$p.k",
"v": {
"$add": [
{
"$multiply": [
"$$p.v",
1.03
]
},
1
]
}
}
}
}
}
},
{
$set: {
pricegrid: {
"$arrayToObject": "$pricegrid"
}
}
}
])
Here is the Mongo playground for your reference.
You can do it with Aggregation framework:
$objectToArray - to transform pricegrid object to array so you can iterate of its items
$map to iterate over array generated in previous step
$sum and multiply to perform mathematical operations
$arrayToObject to transform updated array back to object
db.collection.update({
"adapterKey": "transport1"
},
[
{
"$set": {
"pricegrid": {
"$arrayToObject": {
"$map": {
"input": {
"$objectToArray": "$pricegrid"
},
"in": {
k: "$$this.k",
v: {
"$sum": [
1,
{
"$multiply": [
"$$this.v",
1.02
]
}
]
}
}
}
}
}
}
}
],
{
"multi": true
})
Working example
I might be wrong, but it looks like there's currently no support for this feature - there's actually an open jira-issue that addresses this topic. Doesn't look like this is going to be implemented though.

mongoose $elemMatch returns all results instead only the first

As I understand $elemMatch should find and return only the first elem` it encounters
but instead it returns all the array, this is the DB data
{
_id: "123123123",
"user_id": "0",
"list": [
{
"employee_code": 1111,
"list": []
},
{
"employee_code": 2222,
"list": []
}
]
}
I want to return only the first elem'
this is my query:
Reports.find(
{user_id:"123123123"},
{"list":{$elemMatch:{"employee_code": 1111}}
})
I expect it to return only the first obj' list:[{employee_code:1111,...}]
but instead it returns all the array
better ex:
you should use $filter in aggregration
db.collection.aggregrate([
[
{
'$project': {
'list': {
'$filter': {
'input': '$list',
'as': 'list1',
'cond': {
'$eq': [
'$$list1.employee_code', 1111
]
}
}
},
'user_id': 1
}
}
]
])
$elemMatch will return document that contain an array field with at least one element that matches all the specified query criteria. You should use positional operator $ instead:
db.collection.find({
"_id": "123123123",
"list.employee_code": 1111
},
{
"list.$": 1
});
Here is the working example: https://mongoplayground.net/p/gQvKniEVgfT

Turn _ids into Keys of New Object

I have a huge bunch of documents as such:
{
_id: '1abc',
colors: [
{ value: 'red', count: 2 },
{ value: 'blue', count: 3}
]
},
{
_id: '2abc',
colors: [
{ value: 'red', count: 7 },
{ value: 'blue', count: 34},
{ value: 'yellow', count: 12}
]
}
Is it possible to make use of aggregate() to get the following?
{
_id: 'null',
colors: {
"1abc": [
{ value: 'red', count: 2 },
{ value: 'blue', count: 3}
],
"2abc": [
{ value: 'red', count: 7 },
{ value: 'blue', count: 34},
{ value: 'yellow', count: 12}
]
}
}
Basically, is it possible to turn all of the original documents' _ids into keys of a new object in the singular new aggregated document?
So far, when trying to use$group, I had not been able to use a variable value, e.g. $_id, on the left hand side of an assignment. Am I missing something or is it simply impossible?
I can do this easily using Javascript but it is unbearably slow. Hence why I am looking to see if it is possible using mongo native aggregate(), which will probably be faster.
If impossible... I would appreciate any kind suggestions that could point towards a sufficient alternative (change structure, etc.?). Thank you!
Like a said in comments, whilst there are things you can do with the aggregation framework or even mapReduce to make the "server" reshape the response, it's kind of silly to do so.
Lets consider the cases:
Aggregate
db.collection.aggregate([
{ "$match": { "_id": { "$in": ["1abc","2abc"] } } },
{ "$group": {
"_id": null,
"result": { "$push": "$$ROOT" }
}},
{ "$project": {
"colors": {
"1abc": {
"$arrayElemAt": [
{ "$map": {
"input": {
"$filter": {
"input": "$result",
"as": "r",
"cond": { "$eq": [ "$$r._id", "1abc" ] },
}
},
"as": "r",
"in": "$$r.colors"
}},
0
]
},
"2abc": {
"$arrayElemAt": [
{ "$map": {
"input": {
"$filter": {
"input": "$result",
"as": "r",
"cond": { "$eq": [ "$$r._id", "2abc" ] },
}
},
"as": "r",
"in": "$$r.colors"
}},
0
]
}
}
}}
])
So the aggregation framework purely does not dynamically generate "keys" of a document. If you want to process this way, then you need to know all of the "values" that you are going to use to make the keys in the result.
After putting everything into one document with $group, you can then work with the result array to extact data for your "keys". The basic operators here are:
$filter to get the matched element of the array for the "value" that you want.
$map to return just the specific property from the filtered array
$arrayElemAt to just grab the single elment that was filtered out of the resulting mapped array
So it really isn't practical in a lot of cases, and the coding of the statement is fairly involved.
MapReduce
db.collection.mapReduce(
function() {
var obj = { "colors": {} };
obj.colors[this._id] = this.colors;
emit(null,obj);
},
function(key,values) {
var obj = { "colors": {} };
values.forEach(function(value) {
Object.keys(value.colors).forEach(function(key) {
obj.colors[key] = value.colors[key];
});
})
return obj;
},
{ "out": { "inline": 1 } }
)
Since it is actually written in a "language" then you have the ability to loop structures and "build things" in a more dynamic way.
However, close inspection should tell you that the "reducer" function here is not doing anything more than being the processor of "all the results" which have been "stuffed into it" but each emitted document.
That means that "iterating the values" fed to the reducer is really no different to "iterating the cursor", and that leads to the next conclusion.
Cursor Iteration
var result = { "colors": {} };
db.collection.find().forEach(function(doc) {
result.colors[doc._id] = doc.colors;
})
printjson(result)
The simplicity of this should really speak volumes. It is afterall doing exactly what you are trying to "shoehorn" into a server operation and nothing more, and just simply "rolls up it sleeves" and gets on with the task at hand.
The key point here is none of the process requires any "aggregation" in a real sense, that cannot be equally achieved by simply iterating the cursor and building up the response document.
This is really why you always need to look at what you are doing and choose the right method. "Server side" aggregation has a primary task of "reducing" a result so you would not need to iterate a cursor. But nothing here "reduces" anything. It's just all of the data, transformed into a different format.
Therefore the simple approach for this type of "transform" is to just iterate the cursor and build up your transformed version of "all the results" anyway.