group base on array element mongoDB - arrays

Explanation: how we can do the group based on array elements, I want to do a group based on 0 element of both arrays.
{
"st": [
[
"2011-01-04T12:18:41Z",
0
],
[
"2011-01-04T15:00:00Z",
0
]
],
"en": [
[
"2011-01-04T14:59:50Z",
1
],
[
"2011-01-04T15:05:00Z",
4
]
]
}
The expected output document looks like. for example
[
{
"st": "2011-01-04T12:18:41Z",
"en": "2011-01-04T14:59:50Z",
"st_val": 0,
"en_val": 1,
"total_index_count": 2
},
{
"st": "2011-01-04T15:00:00Z",
"en": "2011-01-04T15:05:00Z",
"st_val": 0,
"en_val": 4,
"total_index_count": 2
}
]

You can do this in several different ways, here is an approach utilizing the $map and $arrayElemAt operators.
db.collection.aggregate([
{
"$project": {
elements: {
$map: {
input: {
$range: [
0,
{
$size: "$st"
}
]
},
in: {
st_val: {
"$arrayElemAt": [
{
"$arrayElemAt": [
"$st",
"$$this"
]
},
0
]
},
st: {
"$arrayElemAt": [
{
"$arrayElemAt": [
"$st",
"$$this"
]
},
1
]
},
en_val: {
"$arrayElemAt": [
{
"$arrayElemAt": [
"$en",
"$$this"
]
},
0
]
},
en: {
"$arrayElemAt": [
{
"$arrayElemAt": [
"$en",
"$$this"
]
},
1
]
},
}
}
}
}
},
{
$unwind: "$elements"
},
{
$replaceRoot: {
newRoot: "$elements"
}
}
])
Mongo Playground

Related

How to add the index number from the array in MongoDB

Explanation: How I can add the index of the array. please check the expected output, I have that array 10000+ elemets
[
{
"data": [
[
1000
],
[
20
],
[
300
],
[
40
]
]
}
]
The expected output will be with the index number.
{
"data": [{
"value": 1000,
"Index": 1
},
{
"value": 20,
"Index": 2
},
{
"value": 300,
"Index": 3
},
{
"value": 40,
"Index": 4
}
]
}
Would be this one:
db.collection.aggregate([
{
$set: {
data: {
$map: {
input: { $range: [0, { $size: "$data" }] },
as: "idx",
in: {
index: { $add: ["$$idx", 1] },
value: { $first: { $arrayElemAt: ["$data", "$$idx"] } }
}
}
}
}
}
])
Mongo Playground

remove 0 values if 0 is followed by >=1 from arrays in MongoDB

In data_start if 0 is followed by >=1 in the array then remove the 0 from the data_start , also remove the element from time_start array accordingly.
{
"data_start": [
1,
1,
0,
0,
1,
1,
0
],
"time_start": [
"2021-09-04T12:18:42Z",
"2021-09-04T14:59:50Z",
"2021-09-04T14:59:59Z",
"2021-09-04T15:00:00Z",
"2021-09-04T15:00:01Z",
"2021-09-04T15:05:00Z",
"2021-09-04T15:05:01Z"
]
}
Output Document will be :
{
"data_start": [
1,
1,
0,
1,
1,
0
],
"time_start": [
"2021-09-04T12:18:42Z",
"2021-09-04T14:59:50Z",
"2021-09-04T14:59:59Z",
"2021-09-04T15:00:01Z",
"2021-09-04T15:05:00Z",
"2021-09-04T15:05:01Z"
]
}
One approach is this one:
db.collection.aggregate([
{
$set: {
data: {
$let: {
vars: {
val: "$data_start",
prev: { $concatArrays: [[null], "$data_start"] } // shift values by one element
},
in: {
$map: {
input: { $range: [0, { $size: "$$val" }] },
as: "idx",
in: {
$cond: {
if: {
$and: [
{ $eq: [{ $arrayElemAt: ["$$val", "$$idx"] }, 0] }, // is 0
{ $eq: [{ $arrayElemAt: ["$$val", "$$idx"] }, { $arrayElemAt: ["$$prev", "$$idx"] }] } // is equal to previous
]
},
then: null,
else: {
data_start: { $arrayElemAt: ["$$val", "$$idx"] },
time_start: { $arrayElemAt: ["$time_start", "$$idx"] }
}
}
}
}
}
}
}
}
},
{
$set: {
data: {
$filter: {
input: "$data",
cond: "$$this"// -> removes null's from array
}
}
}
}
])
Maybe you have to fine-tune the condition and/or reverse the loop, i.e. input: { $range: [{ $size: "$$val" }, 0, -1] }
Mongo playground
This is a little ugly in Mongo but possible, the strategy for us would be to first iterate over the array and calculate the indexes of the items we need to remove, once we have those we will iterate over the two arrays again and update them and finally we remove the temporary calculated values, like so:
Note that to do this purely in Mongo you will need to be on v4.2+ as we need to use pipelined updates if you're using a lesser version then you will have execute this logic in code.
db.collection.updateOne(
{},
[
{
$set: {
tmpField: {
$reduce: {
input: {
$zip: {
"inputs": [
{
$reverseArray: "$data_start"
},
{
$range: [
{
$subtract: [
{
$size: "$data_start"
},
1
]
},
0,
-1
]
}
]
}
},
initialValue: {
prev: null,
indexesToRemove: []
},
in: {
prev: {
$arrayElemAt: [
"$$this",
0
]
},
indexesToRemove: {
$concatArrays: [
"$$value.indexesToRemove",
{
$cond: [
{
$and: [
{
$eq: [
"$$value.prev",
1
]
},
{
$eq: [
{
$arrayElemAt: [
"$$this",
0
]
},
0
]
}
]
},
[
{
$arrayElemAt: [
"$$this",
1
]
}
],
[]
]
}
]
}
}
}
}
}
},
{
$addFields: {
data_start: {
$reduce: {
input: {
$zip: {
"inputs": [
"$data_start",
{
$range: [
0,
{
$size: "$data_start"
}
]
}
]
}
},
initialValue: [],
in: {
$cond: [
{
$in: [
{
$arrayElemAt: [
"$$this",
1
]
},
"$tmpField.indexesToRemove"
]
},
"$$value",
{
$concatArrays: [
"$$value",
[
{
$arrayElemAt: [
"$$this",
0
]
}
]
]
}
]
}
}
},
time_start: {
$reduce: {
input: {
$zip: {
"inputs": [
"$time_start",
{
$range: [
0,
{
$size: "$time_start"
}
]
}
]
}
},
initialValue: [],
in: {
$cond: [
{
$in: [
{
$arrayElemAt: [
"$$this",
1
]
},
"$tmpField.indexesToRemove"
]
},
"$$value",
{
$concatArrays: [
"$$value",
[
{
$arrayElemAt: [
"$$this",
0
]
}
]
]
}
]
}
}
}
}
},
{
$unset: "tmpField"
}
])
Mongo Playground

Multiply Varying number of Values Inside array: Mongodb

I have arranged my data so that the documents belonging to the same customer id are aggregated into a single collection. The data format is as follows.
{
"items": [
{
"stock_code": [
"22617",
"22768",
"20749"
],
"description": [
"DESIGN",
"FAMILY PHOTO FRAME",
"ASSORTED CASES"
],
"quantity": [
18,
12,
84
],
"unit_price": [
4.95,
9.95,
6.35
]
}
],
"_id": 581485,
"customer_id": 17389,
"country": "United Kingdom"
}
I need to multiply the values of array quantity with corresponding unit_price and get a total for multiple documents in a new field. I have tried using the $reduce function and $map function to get the output but both of them result in "error"
Multiply only supports numeric types, and not arrays
Could you please suggest how should i go about accomplishing this.
Codes tried:
"$addFields": {"order_total" :
{
"$sum": {
"$map": {
"input": "$items",
"as": "items",
"in": { "$multiply": [
{ "$ifNull": [ "$$items.quantity", 0 ] },
{ "$ifNull": [ "$$items.unit_price", 0 ] }
]}
}
}
}
}
Second:
"order_total" : {
"$reduce" : {
"input" : "$items",
"initialValue" : Decimal128("0.00"),
"in": {
"$sum" : [
"$$value",
{"$multiply" : [ "$$this.quantity", "$$this.unit_price" ] }
]}
}
}
The expected result needs to add a new field of "total" by multiplying the corresponding entries of unit_price with quantity. The error message is that of multiply only supports numeric types and not arrays.
I would decompose the problem into solvable chunks beginning with the smallest unit and start with the two arrays quantity and unit_price.
So given just a document with the structure
{
"quantity": [ 18, 12, 84 ],
"unit_price": [ 4.95, 9.95, 6.35 ]
}
We can add another field with the totals for each element in both arrays i.e.
{
"quantity": [ 18, 12, 84 ],
"unit_price": [ 4.95, 9.95, 6.35 ],
"total": [ 89.1, 119.4, 533.4 ]
}
This field can be computed using $range within $map as
{
"total": {
"$map": {
"input": { "$range": [ 0, { "$size": "$quantity" } ] },
"as": "idx",
"in": {
"$let": {
"vars": {
"qty": { "$arrayElemAt": [ "$quantity", "$$idx" ] },
"price": {
"$ifNull": [
{ "$arrayElemAt": [ "$unit_price", "$$idx" ] },
0
]
}
},
"in": { "$multiply": [ "$$qty", "$$price" ] }
}
}
}
}
}
This then becomes basis for calculating the order_total field with two pipeline stages for clarity (although can be composed into a single pipeline with $reduce for brevity)
var totalMapExpression = {
"$map": {
"input": { "$range": [ 0, { "$size": "$$item.quantity" } ] },
"as": "idx",
"in": {
"$let": {
"vars": {
"qty": { "$arrayElemAt": [ "$$item.quantity", "$$idx" ] },
"price": {
"$ifNull": [
{ "$arrayElemAt": [ "$$item.unit_price", "$$idx" ] },
0
]
}
},
"in": { "$multiply": [ "$$qty", "$$price" ] }
}
}
}
};
db.collection.aggregate([
{ "$addFields": {
"items": {
"$map": {
"input": "$items",
"as": "item",
"in": {
"quantity": "$$item.quantity",
"unit_price": "$$item.unit_price",
"stock_code": "$$item.stock_code",
"description": "$$item.description",
"total": { "$sum": totalMapExpression }
}
}
}
} },
{ "$addFields": {
"order_total": { "$sum": "$items.total" }
} }
])
Try the below query:
db.collection.aggregate(
[{ $unwind: { path: "$items",} },
{ $unwind: { path: "$items.quantity",} },
{ $unwind: { path: "$items.unit_price",} },
{ $addFields: { 'total': {$multiply: ["$items.quantity", "$items.unit_price"] }} }])

Mongo filter array of array of array

I'm trying to filter the list of an array of array arrays, this is an example of structure.
{
"array1": [
{
"array2": [
{
"array3": [
{
"sampleId": 1
},
{
"sampleId": 2
},
{
"sampleId": 5
}
]
},
{
"array3": [
{
"sampleId": 7
},
{
"sampleId": 8
}
]
}
]
},
{
"array2": [
{
"array3": [
{
"sampleId": 1
}
]
}
]
}
]
}
Let's say that i want to filter out all the subdocuments with sampleId > 2
this is an example of the expected result.
{
"array1": [
{
"array2": [
{
"array3": [
{
"sampleId": 1
},
{
"sampleId": 2
}
]
},
{
"array3": []
}
]
},
{
"array2": [
{
"array3": [
{
"sampleId": 1
}
]
}
]
}
]
}
I tried using aggregation/map/filter technique as explained in this post and others but the results are always giving array3 empty.
You can try below aggregation
Basically you need to loop over each array using $map aggregation and finally use $filter with the last one.
db.collection.aggregate([
{ "$project": {
"array1": {
"$map": {
"input": "$array1",
"as": "a1",
"in": {
"array2": {
"$map": {
"input": "$$a1.array2",
"as": "a2",
"in": {
"array3": {
"$filter": {
"input": "$$a2.array3",
"as": "a3",
"cond": { "$lte": ["$$a3.sampleId", 2] }
}
}
}
}
}
}
}
}
}}
])
Output
[
{
"array1": [
{
"array2": [
{
"array3": [
{
"sampleId": 1
},
{
"sampleId": 2
}
]
},
{
"array3": []
}
]
},
{
"array2": [
{
"array3": [
{
"sampleId": 1
}
]
}
]
}
]
}
]

Conditionally Project Matching Array Item

I have a collection named questions with documents like this:
{
"formats": [
{
"language_id": 1,
"text": "question text1"
},
{
"language_id": 2,
"text": "question text 2"
}
],
"qid": "HQSRFA3T"
}
I want to write a query such that if a specific language_id is not present, then language_id with 1 should be returned by default.
I've tried two queries so far:
db.questions.aggregate([
{
$match: {
'qid': 'HQSRFA3T'
}
},
{
$project: {
formats: {
$ifNull: [
{ $filter: { input: '$formats', as: 'format', cond: {$eq: ['$$format.language_id', 3]}} },
{ $filter: { input: '$formats', as: 'format', cond: {$eq: ['$$format.language_id', 1]}} }
]
},
_id: 0
}
}
])
This query results in something like this:
{ "formats" : [ ] }
Then there's another query which is something like this:
db.questions.aggregate([ { $match: {'qid': 'HQSRFA3T'}}, { $project: {
formats: {
$filter: {
input: '$formats',
as: 'format',
cond: {
$or: [
{ $eq: ['$$format.language_id', 1] },
{ $eq: ['$$format.language_id', 3] }
]
}
}
},
_id: 0
}}])
This query returns two elements in case both language_id's are present in the array.
There are a "few" ways:
Ideally you have $indexOfArray from MongoDB 3.4 then you can use that in combination with $in:
db.questions.aggregate([
{ "$match": { "qid": "HQSRFA3T" } },
{ "$project": {
"formats": {
"$cond": {
"if": { "$in": [ 3, "$formats.language_id"] },
"then": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats.language_id", 3 ] }
]
},
"else": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats.language_id", 1 ] }
]
}
}
}}
}
])
And if all you really want is the matching "text", then a slight alteration:
db.questions.aggregate([
{ "$match": { "qid": "HQSRFA3T" } },
{ "$project": {
"text": {
"$cond": {
"if": { "$in": [ 3, "$formats.language_id"] },
"then": {
"$arrayElemAt": [
"$formats.text",
{ "$indexOfArray": [ "$formats.language_id", 3 ] }
]
},
"else": {
"$arrayElemAt": [
"$formats.text",
{ "$indexOfArray": [ "$formats.language_id", 1 ] }
]
}
}
}}
}
])
That works because if the $indexOfArray returns -1 indicating "not found" then the the $cond will branch accordingly:
Alternately, use $filter with $size:
db.questions.aggregate([
{ "$match": { "qid": "HQSRFA3T" } },
{ "$project": {
"formats": {
"$cond": {
"if": { "$gt": [
{ "$size": {
"$filter": {
"input": "$formats",
"cond": { "$eq": [ "$$this.language_id", 3 ] }
}
}},
0
]},
"then": {
"$filter": {
"input": "$formats",
"cond": { "$eq": [ "$$this.language_id", 3 ] }
}
},
"else": {
"$filter": {
"input": "$formats",
"cond": { "$eq": [ "$$this.language_id", 1 ] }
}
}
}
}
}}
])
You can even vary on the last form with $arrayElemAt to just return the "single" matching array element at position 0 if you at least have MongoDB 3.2.
db.questions.aggregate([
{ "$match": { "qid": "HQSRFA3T" } },
{ "$project": {
"formats": {
"$cond": {
"if": { "$gt": [
{ "$size": {
"$filter": {
"input": "$formats",
"cond": { "$eq": [ "$$this.language_id", 3 ] }
}
}},
0
]},
"then": {
"$arrayElemAt": [
{ "$filter": {
"input": "$formats",
"cond": { "$eq": [ "$$this.language_id", 3 ] }
}},
0
]
},
"else": {
"$arrayElemAt": [
{ "$filter": {
"input": "$formats",
"cond": { "$eq": [ "$$this.language_id", 1 ] }
}},
0
]
}
}
}
}}
])
Other alternatives on the $cond for the if condition are using $in, to match the comparison on the array elements:
"if": { "$in": [ 3, "$formats.language_id" ] }
But since that requires MongoDB 3.4, then you may as well use the $indexOfArray operator instead.
There is very little point in trying to "force" multiple matches into $filter and then ultimately looking to discard one of them, but you "can" do it with $let:
db.questions.aggregate([
{ "$match": { "qid": "HQSRFA3T" } },
{ "$project": {
"formats": {
"$let": {
"vars": {
"formats": {
"$filter": {
"input": "$formats",
"cond": {
"$or": [
{ "$eq": [ "$$this.language_id", 1 ] },
{ "$eq": [ "$$this.language_id", 3 ] }
]
}
}
}
},
"in": {
"$cond": {
"if": {
"$gt": [
{ "$size": {
"$filter": {
"input": "$$formats",
"cond": { "$eq": [ "$$this.language_id", 3 ] }
}
}},
0
]
},
"then": {
"$filter": {
"input": "$$formats",
"cond": { "$eq": [ "$$this.language_id", 3 ] }
}
},
"else": {
"$filter": {
"input": "$$formats",
"cond": { "$eq": [ "$$this.language_id", 1 ] }
}
}
}
}
}
}
}}
])
So it's there, but it's just extra work with little gain since at best the $or condition matches the "default" case and you still needed to "filter away" for only the "preferred" match anyway.

Resources