Mongo addFields get the value from the nested array - mongodb

I want to sort the collection by the fieldValue based on the given fieldName.
For example:
sort the collection by fieldName = 'Author'
My problem:
I am unable to get the value from the collection, like I want to add a field for authorValue.
{
....
author: 'John'
}, {
author: 'Hengry'}
What I have tried:
.addFields({
author: {
$filter: {
input: "$sections.fieldData",
cond: {
$eq: ["$$this.fieldName", true],
},
},
},
The structure
[
{
"sections": [
{
name: "section1",
fields: [
{
fieldName: "Author",
fieldValue: "John"
},
{
fieldName: "Movie",
fieldValue: "Avenger"
}
]
}
]
},
{
"sections": [
{
name: "section1",
fields: [
{
fieldName: "Author",
fieldValue: "Hengry"
},
{
fieldName: "Movie",
fieldValue: "Test"
}
]
}
]
}
]

You can use $reduce to iterate your array and extract out the fieldValue for comparison.
db.collection.aggregate([
{
"$addFields": {
"sortField": {
"$reduce": {
"input": "$sections",
"initialValue": null,
"in": {
"$reduce": {
"input": "$$this.fields",
"initialValue": null,
"in": {
"$cond": {
"if": {
$eq: [
"$$this.fieldName",
"Author"
]
},
"then": "$$this.fieldValue",
"else": "$$value"
}
}
}
}
}
}
}
},
{
$sort: {
sortField: 1
}
},
{
"$project": {
sortField: false
}
}
])
Here is the Mongo playground for your reference.

Related

MongoDB - Lookup match with condition array of object with string

I have two collections "datasets" and "users".
I tried to lookup datasets.assignedTo = users.id that's working fine. Also, I want to match the field of datasets.firstBillable >= users.prices.beginDate date field are matched to get the current index price value. And also check users.prices.endDate is less than or equal to users.prices.beginDate.
For example:
cgPrices: 45
https://mongoplayground.net/p/YQps9EozlAL
Collections:
db={
users: [
{
id: 1,
name: "Aravinth",
prices: [
{
beginDate: "2022-08-24T07:29:01.639Z",
endDate: "2022-08-31T07:29:01.639Z",
price: 45
}
]
},
{
id: 2,
name: "Raja",
prices: [
{
beginDate: "2022-07-25T07:29:01.639Z",
endDate: "2022-07-30T07:29:01.639Z",
price: 55
}
]
}
],
datasets: [
{
color: "braun, rose gold",
firstBillable: "2022-08-24T07:29:01.639Z",
assignedTo: 1
},
{
color: "beige, silber",
firstBillable: "2022-07-25T07:29:01.639Z",
assignedTo: 2
}
]
}
My current implementation:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: "$firstBillable"
},
pipeline: [
{
"$match": {
$expr: {
"$and": [
{
"$eq": [
"$id",
"$$assigned_to"
]
},
{
"$gte": [
"$first_billable",
"$details.prices.beginDate"
]
},
{
"$lte": [
"$first_billable",
"$details.prices.endDate"
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: {
"$first": "$details.prices.price"
}
}
}
}
])
Output i needed:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"assignedTo": 1,
"cg": 45,
"color": "braun, rose gold",
"details": 0,
"firstBillable": "2022-08-24T07:29:01.639Z"
},
{
"_id": ObjectId("5a934e000102030405000001"),
"assignedTo": 2,
"cg": 55,
"color": "beige, silber",
"details": 0,
"firstBillable": "2022-07-25T07:29:01.639Z"
}
]
https://mongoplayground.net/p/YQps9EozlAL
Concerns:
You should compare the date as Date instead of string, hence you are required to convert the date strings to Date before comparing.
In users collection, prices is an array. You need to deconstruct the array to multiple documents first before compare the date fields in price.
The query should be:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: {
$toDate: "$firstBillable"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$id",
"$$assigned_to"
]
}
}
},
{
$unwind: "$prices"
},
{
"$match": {
$expr: {
"$and": [
{
"$gte": [
"$$first_billable",
{
$toDate: "$prices.beginDate"
}
]
},
{
"$lte": [
"$$first_billable",
{
$toDate: "$prices.endDate"
}
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: "$details.prices.price"
}
}
}
])
Demo # Mongo Playground

Self joining query in MongoDB

We are exploring the possibility of migrating from relational database to MongoDB and having difficulty dealing with a query, the data schema is like this:
data_store:
id,
userId,
study,
form,
formData: {
element1: value1,
element2: value2,
.....
}
formData is a json array of dynamic element:value pairs, different form has a different list of pre-defined elements.
The requirement is by given 2 forms in a study, we need to display all their elements' data in one row for the same user, i.e. we need to join by userId and display the formData in a flat structure.
Also one user may have multiple data entry for the same form, so if a user has 3 entries for form A and 4 entries for form B, we expect that there are 12 rows for them in the result.
Sample data:
id: "id1",
user: "user1",
study: "study",
form: "f1",
formData: [
{ "e11": "value1" },
{ "e12": "value2" },
{ "e13": "value3" }
]
,
id: "id2",
user: "user1",
study: "study",
form: "f1",
formData: [
{ "e11": "value4" },
{ "e12": "value5" },
{ "e13": "value6" }
]
,
id: "id3",
user: "user1",
study: "study",
form: "f2",
formData: [
{ "e21": "value7" },
{ "e22": "value8" }
]
,
id: "id4",
user: "user1",
study: "study1",
form: "f2",
formData: [
{ "e21": "value9" },
{ "e22": "value10" }
]
,
id: "id2",
user: "user2",
study: "study1",
form: "f2",
formData: [
{ "e21": "value11" },
{ "e22": "value12" }
The expected result for the sample data above is:
Row#
study
user
f1.e11
f1.e12
f1.e13
f2.e21
f2.e22
1
study
user1
value1
value2
value3
value7
value8
2
study
user1
value1
value2
value3
value9
value10
3
study
user1
value4
value5
value6
value7
value8
4
study
user1
value4
value5
value6
value9
value10
5
study
user2
value11
value12
A similar query in relational database may be like this:
select t1.*, t2.*
from data_store t1, data_store t2
where t1.form = 'f1'
and t2.form = 'f2'
and t1.userId = t2.userId;
I am having difficulty of converting it into MongoDB query, any one who can shed some light will be greatly appreciated.
You can use the below Aggregation Query to get the desired result.
db.collection.aggregate([
{
$group: {
_id: {
"user": "$user",
"form": "$form",
},
"formData": {
"$push": {
"$concatArrays": [
"$formData",
[
{
"study": "$study"
}
]
]
},
},
}
},
{
$group: {
_id: {
"user": "$_id.user",
},
formData: {
"$push": "$formData"
}
}
},
{
$project: {
"formData": {
"$map": {
"input": {
"$arrayElemAt": [
"$formData",
0
]
},
"as": "f1",
"in": {
"$cond": {
"if": {
"$gt": [
{
"$size": "$formData"
},
1
]
},
"then": {
"$map": {
"input": {
"$arrayElemAt": [
"$formData",
1
]
},
"as": "f2",
"in": {
"$concatArrays": [
"$$f1",
"$$f2"
],
},
},
},
"else": [
"$$f1"
]
},
},
},
},
}
},
{
$unwind: {
path: "$formData",
}
},
{
$unwind: {
path: "$formData",
}
},
{
$replaceRoot: {
newRoot: {
"$mergeObjects": [
{
"user": "$_id.user"
},
{
"$reduce": {
"input": "$formData",
"initialValue": {},
"in": {
"$mergeObjects": [
"$$this",
"$$value"
],
}
},
},
],
}
}
}
])
Mongo Playground Working Sample
Let me know if you want an explanation of each stage.
Limitations:
This will work only if there are two forms of studies f1 and f2.
I am working on making this dynamic and will update this answer once done.
EDIT:
Make use of this updated query which will work dynamically.
db.collection.aggregate([
{
"$match": {
"form": {
"$in": [
"f1",
"f2"
]
},
},
},
{
"$group": {
"_id": "$user",
"formData": {
"$push": {
"$mergeObjects": [
{
"$reduce": {
"input": "$formData",
"initialValue": {},
"in": {
"$mergeObjects": [
"$$value",
{
"$arrayToObject": {
"$map": {
"input": {
"$objectToArray": "$$this"
},
"as": "formValue",
"in": {
"k": {
"$concat": [
"$form",
"-",
"$$formValue.k"
]
},
"v": "$$formValue.v"
}
},
},
},
]
},
}
},
{
"study": "$study",
"user": "$user",
"form": "$form"
},
],
},
},
},
},
{
"$project": {
"formData": {
"$cond": {
"if": {
"$gt": [
{
"$size": "$formData"
},
1
]
},
"then": {
$reduce: {
input: {
$range: [
0,
{
$size: "$formData"
}
]
},
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$let: {
vars: {
i: "$$this"
},
in: {
$map: {
input: {
$range: [
{
$add: [
1,
"$$i"
]
},
{
$size: "$formData"
}
]
},
in: [
{
"$let": {
"vars": {
"arrayElem1": {
$arrayElemAt: [
"$formData",
"$$i"
]
},
"arrayElem2": {
$arrayElemAt: [
"$formData",
"$$this"
]
},
},
"in": {
"$cond": {
"if": {
"$and": [
{
"$ne": [
"$$arrayElem1.form",
"$$arrayElem2.form"
]
},
// {
// "$eq": [
// "$$arrayElem1.user",
// "$$arrayElem2.user"
// ]
// },
],
},
"then": {
"$mergeObjects": [
"$$arrayElem2",
"$$arrayElem1",
],
},
"else": "$$REMOVE",
},
},
}
}
]
}
}
}
}
]
}
}
},
"else": [
"$formData"
]
},
},
},
},
{
"$project": {
"formData": {
"$reduce": {
"input": "$formData",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
{
"$cond": {
"if": {
"$ne": [
"$$this",
[
null
]
]
},
"then": "$$this",
"else": []
}
}
]
}
}
}
}
},
{
"$unwind": "$formData"
},
{
"$replaceRoot": {
"newRoot": "$formData"
}
},
])
Mongo Playground Working Sample

MongoDB how to filter in nested array

I have below data. I want to find value=v2 (remove others value which not equals to v2) in the inner array which belongs to name=name2. How to write aggregation for this? The hard part for me is filtering the nestedArray which only belongs to name=name2.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
}
]
}
And the desired output is below. Please note the value=v1 remains under name=name1 while value=v1 under name=name2 is removed.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v2"
}
]
}
]
}
You can try,
$set to update array field, $map to iterate loop of array field, check condition if name is name2 then $filter to get matching value v2 documents from nestedArray field and $mergeObject merge objects with available objects
let name = "name2", value = "v2";
db.collection.aggregate([
{
$set: {
array: {
$map: {
input: "$array",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.name", name] }, //name add here
{
nestedArray: {
$filter: {
input: "$$this.nestedArray",
cond: { $eq: ["$$this.value", value] } //value add here
}
}
},
{}
]
}
]
}
}
}
}
}
])
Playground
You can use the following aggregation query:
db.collection.aggregate([
{
$project: {
"array": {
"$concatArrays": [
{
"$filter": {
"input": "$array",
"as": "array",
"cond": {
"$ne": [
"$$array.name",
"name2"
]
}
}
},
{
"$filter": {
"input": {
"$map": {
"input": "$array",
"as": "array",
"in": {
"name": "$$array.name",
"nestedArray": {
"$filter": {
"input": "$$array.nestedArray",
"as": "nestedArray",
"cond": {
"$eq": [
"$$nestedArray.value",
"v2"
]
}
}
}
}
}
},
"as": "array",
"cond": {
"$eq": [
"$$array.name",
"name2"
]
}
}
}
]
}
}
}
])
MongoDB Playground

Aggregate mongodb change objects to array

I am working with a mongodb aggregation and I would need to replace the part of my object array to an array concatenation. I understand that the part that I have to modify would be push but it is added as objects and I am not very clear how to concatenate that part
This is my current aggregation:
Datagreenhouse.aggregate([
{ "$match": { "id_sensor_station_absolute": { "$in": arr }, "recvTime": { $gte: fechainicio, $lte: fechafin } } }, //, "id_station": { "$in": id_station }
{
"$lookup": {
"from": "station_types",
"localField": "id_station", // local field in measurements collection
"foreignField": "id_station", //foreign field from sensors collection
"as": "sensor"
}
},
{ "$unwind": "$sensor" },
{
"$addFields": {
"sensor.attrValue": "$attrValue", // Add attrValue to the sensors
"sensor.recvTime": "$recvTime", // Add attrName to the sensors
"sensor.marca": "$sensor.marca",
"sensor.sensor_type": {
$filter: {
input: '$sensor.sensor_type',
as: 'shape',
cond: { $eq: ['$$shape.name', '$attrName'] },
}
}
}
},
{
"$group": {
"_id": {
"name_comun": "$sensor.sensor_type.name_comun",
"name_comun2": "$sensor.marca"
}, // Group by time
"data": {
"$push": {
"name": "$sensor.recvTime",
"value": "$sensor.attrValue",
},
},
}
},
{
"$project": {
"name": {
$reduce: {
input: [
["$_id.name_comun2"]
],
initialValue: "$_id.name_comun",
in: { $concatArrays: ["$$value", "$$this"] }
}
},
"_id": 0,
"data": 1
}
}
]
The current output of this aggregation is:
[
{
"data":[
{
"name":"2020-06-04T14:30:50.00Z",
"value":69.4
},
{
"name":"2020-06-04T14:13:31.00Z",
"value":68.9
}
],
"name":[
"Hum. Relativa",
"Hortisys"
]
},
{
"data":[
{
"name":"2020-06-04T14:30:50.00Z",
"value":25.5
},
{
"name":"2020-06-04T14:13:31.00Z",
"value":26.6
}
],
"name":[
"Temp. Ambiente",
"Hortisys"
]
}
]
I need to change the format of data for data: [[], [], []]
Example:
[
{
"data":[
["2020-06-04T14:30:50.00Z",69.4],
["2020-06-04T14:13:31.00Z",68.9],
"name":[
"Hum. Relativa",
"Hortisys"
]
},
{
"data":[
["2020-06-04T14:30:50.00Z",69.4],
["2020-06-04T14:13:31.00Z",68.9],
"name":[
"Temp. Ambiente",
"Hortisys"
]
}
]
The $push operator won't accept an array directly, but you can give it another operator that returns an array, like
data:{ $push:{ $concatArrays:[[ "$sensor.recvTime", "$sensor.attrValue" ]]}},

$group inner array values without $unwind

I want to group objects in the array by same value for specified field and produce a count.
I have the following mongodb document (non-relevant fields are not present).
{
arrayField: [
{ fieldA: value1, ...otherFields },
{ fieldA: value2, ...otherFields },
{ fieldA: value2, ...otherFields }
],
...otherFields
}
The following is what I want.
{
arrayField: [
{ fieldA: value1, ...otherFields },
{ fieldA: value2, ...otherFields },
{ fieldA: value2, ...otherFields }
],
newArrayField: [
{ fieldA: value1, count: 1 },
{ fieldA: value2, count: 2 },
],
...otherFields
}
Here I grouped embedded documents by fieldA.
I know how to do it with unwind and 2 group stages the following way. (irrelevant stages are ommited)
Concrete example
// document structure
{
_id: ObjectId(...),
type: "test",
results: [
{ choice: "a" },
{ choice: "b" },
{ choice: "a" }
]
}
db.test.aggregate([
{ $match: {} },
{
$unwind: {
path: "$results",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: {
_id: "$_id",
type: "$type",
choice: "$results.choice",
},
count: { $sum: 1 }
}
},
{
$group: {
_id: {
_id: "$_id._id",
type: "$_id.type",
result: "$results.choice",
},
groupedResults: { $push: { count: "$count", choice: "$_id.choice" } }
}
}
])
You can use below aggregation
db.test.aggregate([
{ "$addFields": {
"newArrayField": {
"$map": {
"input": { "$setUnion": ["$arrayField.fieldA"] },
"as": "m",
"in": {
"fieldA": "$$m",
"count": {
"$size": {
"$filter": {
"input": "$arrayField",
"as": "d",
"cond": { "$eq": ["$$d.fieldA", "$$m"] }
}
}
}
}
}
}
}}
])
The below adds a new array field, which is generated by:
Using $setUnion to get unique set of array items, with inner $map to
extract only the choice field
Using $map on the unique set of items,
with inner $reduce on the original array, to sum all items where
choice matches
Pipeline:
db.test.aggregate([{
$addFields: {
newArrayField: {
$map: {
input: {
$setUnion: [{
$map: {
input: "$results",
in: { choice: "$$this.choice" }
}
}
]
},
as: "i",
in: {
choice: '$$i.choice',
count: {
$reduce: {
input: "$results",
initialValue: 0,
in: {
$sum: ["$$value", { $cond: [ { $eq: [ "$$this.choice", "$$i.choice" ] }, 1, 0 ] }]
}
}
}
}
}
}
}
}])
The $reduce will iterate over the results array n times, where n is the number of unique values of choice, so the performance will depend on that.