Mongodb how to get OR of two aggregated queries? - mongodb

I'm using following aggregated query to get results (let say query1):
db.fb.aggregate(
[
{
$addFields : { noOfLikes : { $sum : { $map : { input : "$facebookEvents", as : "f", in : {$cond : [ { $eq : ["$$f.type" , "like"] }, 1, 0 ]} }}}}
},
{
$match : {"noOfLikes" : {$gte : 2}}
}
]
)
and another query to get some other results (let's say query2):
db.fb.aggregate(
[
{ $match : { author : "dave" } },
{ $match : { test : "test1" } }
]
)
Is it possible to get query1 OR query2 using a single aggregate query? In other words, I want to get results that match either query1 or query2 using a single query. Appreciate any help
mongo version: 3.4.4

Within a single aggregate query, you would need to run your pipeline as follows:
db.fb.aggregate([
{
"$addFields": {
"noOfLikes": {
"$sum" : {
"$map": {
"input": "$facebookEvents",
"as": "f",
"in": {
"$cond": [{ "$eq": ["$$f.type" , "like"] }, 1, 0 ]
}
}
}
}
}
},
{
"$match" : {
"$or": [
{ "noOfLikes" : { "$gte" : 2 } },
{ "author": "dave", "test": "test1" }
]
}
}
])
or using $redact as:
db.fb.aggregate([
{
"$redact": {
"$cond": [
{
"$or": [
{ "$gte": [
{
"$sum" : {
"$map": {
"input": "$facebookEvents",
"as": "f",
"in": {
"$cond": [
{ "$eq": ["$$f.type" , "like"] },
1,
0
]
}
}
}
}, 2 ]
},
{
"$and": [
{ "$eq": ["$author", "dave"] },
{ "$eq": ["$test", "test1"] }
]
}
]
},
"$$KEEP",
"$$PRUNE"
]
}
}
])

You can use $facet
db.fb.aggregate(
{
$facet : {
byLikes : [
{ $addFields : { noOfLikes : { $sum : { $map : { input : "$facebookEvents", as : "f", in : {$cond : [ { $eq : ["$$f.type" , "like"] }, 1, 0 ]} }}}} },
{ $match : {"noOfLikes" : {$gte : 2}}}
],
byAuthor : [
{ $match : { author : "dave" } },
{ $match : { test : "test1" } }
]
}
}
)

Related

Concat int and string array fields which are in different arrays

{
"no" : "2020921008981",
"date" : ISODate("2020-04-01T05:19:02.263+0000"),
"sale" : {
"soldItems" : [
{
"itemId" : "5b55ac7f0550de00210a3b24",
"qty" : NumberInt(1),
},
{
"itemId" : "5b55ac7f0550de00210a3b25",
"qty" : NumberInt(2),
}
],
"items" : [
{
"_id" : ObjectId("5b55ac7f0550de00210a3b24"),
unit :"KG"
},
{
"_id" : ObjectId("5b55ac7f0550de00210a3b25"),
unit :"ML"
}
]
}
}
Desired output :
{
"no" : "2020921008981",
"sale" : {}
"qtyList" : "1 KG \n 2 ML"
}
In order to build itemQtyList output field, two fields from different arrays (string and int) should be used. Couldn't find any reference for doing that. Any idea would be appreciated.
You can use below aggregation
db.collection.aggregate([
{ "$project": {
"itemQtyList": {
"$reduce": {
"input": { "$range": [0, { "$size": "$sale.soldItems" }] },
"initialValue": "",
"in": {
"$concat": [
"$$value",
{ "$cond": [{ "$eq": ["$$this", 0] }, "", " \n "] },
{ "$toString": {
"$arrayElemAt": [
"$sale.soldItems.qty",
"$$this"
]
}},
" ",
{ "$arrayElemAt": ["$sale.items.unit", "$$this"] }
]
}
}
}
}}
])
MongoPlayground

Filter array in subdocument array field

I am trying to fetch an element from an array in the MongoDB. I think the aggregation filter is the right one to apply. But I tried million times already, I still cannot find where is the problem. Could you give me hand?
MongoDB sample data:
{
"_id" : 12,
"items" : [
{
"columns" : [
{
"title" : "hhh",
"value" : 10
},
{
"title" : "hahaha",
"value" : 20
}
]
},
{
"columns" : [
{
"title" : "hiii",
"value" : 50
}
]
}
]
}
My solution:
db.myCollection.aggregate([
{
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: { $eq: [ "$$item.columns.title", "hahaha" ]}
}
}
}
}
]).pretty()
My result:
{
"_id" : 15,
"items" : [
{
"columns" : [ ]
},
{
"columns" : [ ]
}
]
}
Expected result:
{
"_id" : 15,
"items" : [
{
"columns" : [
{
"title" : "hahaha",
"value" : 20
}
]
},
{
"columns" : []
}
]
}
I have checked the Mongo reference:
https://docs.mongodb.com/manual/reference/operator/aggregation/filter/#example
MongoDB version:3.4.1
Testing environment: Mongo Shell
You need to use the $map array operator to $filter the sub array in your subdocument. Also you should do this in the $addFields aggregation pipeline stage to automatically include all others fields in the query result if you need them.
You can also replace the $addFields stage with $project as you were doing but in this case, you will need to explicitly include all other fields.
let value = "hahaha";
db.coll.aggregate([
{
"$addFields": {
"items": {
"$map": {
"input": "$items",
"as": "item",
"in": {
"columns": {
"$filter": {
"input": "$$item.columns",
"as": "elt",
"cond": { "$eq": [ "$$elt.title", value ] }
}
}
}
}
}
}
}
])

Nested filters: $filter array, then $filter child array

Essentially I'm trying to filter OUT subdocuments and sub-subdocuments that have been "trashed". Here's a stripped-down version of my schema:
permitSchema = {
_id,
name,
...
feeClassifications: [
new Schema({
_id,
_trashed,
name,
fees: [
new Schema({
_id,
_trashed,
name,
amount
})
]
})
],
...
}
So I'm able to get the effect I want with feeClassifications. But I'm struggling to find a way to have the same effect for feeClassifications.fees as well.
So, this works as desired:
Permit.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(req.params.id) }},
{ $project: {
_id: 1,
_name: 1,
feeClassifications: {
$filter: {
input: '$feeClassifications',
as: 'item',
cond: { $not: {$gt: ['$$item._trashed', null] } }
}
}
}}
])
But I also want to filter the nested array fees. I've tried a few things including:
Permit.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(req.params.id) }},
{ $project: {
_id: 1,
_name: 1,
feeClassifications: {
$filter: {
input: '$feeClassifications',
as: 'item',
cond: { $not: {$gt: ['$$item._trashed', null] } }
},
fees: {
$filter: {
input: '$fees',
as: 'fee',
cond: { $not: {$gt: ['$$fee._trashed', null] } }
}
}
}
}}
])
Which seems to follow the mongodb docs the closest. But I get the error:
this object is already an operator expression, and can't be used as a document expression (at 'fees')
Update: -----------
As requested, here's a sample document:
{
"_id" : ObjectId("57803fcd982971e403e3e879"),
"_updated" : ISODate("2016-07-11T19:24:27.204Z"),
"_created" : ISODate("2016-07-09T00:05:33.274Z"),
"name" : "Single Event",
"feeClassifications" : [
{
"_updated" : ISODate("2016-07-11T19:05:52.418Z"),
"_created" : ISODate("2016-07-11T17:49:12.247Z"),
"name" : "Event Type 1",
"_id" : ObjectId("5783dc18e09be99840fad29f"),
"fees" : [
{
"_updated" : ISODate("2016-07-11T18:51:10.259Z"),
"_created" : ISODate("2016-07-11T18:41:16.110Z"),
"name" : "Basic Fee",
"amount" : 156.5,
"_id" : ObjectId("5783e84cc46a883349bb2339")
},
{
"_updated" : ISODate("2016-07-11T19:05:52.419Z"),
"_created" : ISODate("2016-07-11T19:05:47.340Z"),
"name" : "Secondary Fee",
"amount" : 50,
"_id" : ObjectId("5783ee0bad7bf8774f6f9b5f"),
"_trashed" : ISODate("2016-07-11T19:05:52.410Z")
}
]
},
{
"_updated" : ISODate("2016-07-11T18:22:21.567Z"),
"_created" : ISODate("2016-07-11T18:22:21.567Z"),
"name" : "Event Type 2",
"_id" : ObjectId("5783e3dd540078de45bbbfaf"),
"_trashed" : ISODate("2016-07-11T19:24:27.203Z")
}
]
}
And here's the desired output ("trashed" subdocuments are excluded from BOTH feeClassifications AND fees):
{
"_id" : ObjectId("57803fcd982971e403e3e879"),
"_updated" : ISODate("2016-07-11T19:24:27.204Z"),
"_created" : ISODate("2016-07-09T00:05:33.274Z"),
"name" : "Single Event",
"feeClassifications" : [
{
"_updated" : ISODate("2016-07-11T19:05:52.418Z"),
"_created" : ISODate("2016-07-11T17:49:12.247Z"),
"name" : "Event Type 1",
"_id" : ObjectId("5783dc18e09be99840fad29f"),
"fees" : [
{
"_updated" : ISODate("2016-07-11T18:51:10.259Z"),
"_created" : ISODate("2016-07-11T18:41:16.110Z"),
"name" : "Basic Fee",
"amount" : 156.5,
"_id" : ObjectId("5783e84cc46a883349bb2339")
}
]
}
]
}
Since we want to filter both the outer and inner array fields, we can use the $map variable operator which return an array with the "values" we want.
In the $map expression, we provide a logical $conditional $filter to remove the non matching documents from both the document and subdocument array field.
The conditions are $lt which return true when the field "_trashed" is absent in the sub-document and or in the sub-document array field.
Note that in the $cond expression we also return false for the <false case>. Of course we need to apply filter to the $map result to remove all false.
Permit.aggregate(
[
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{ "$project": {
"_updated": 1,
"_created": 1,
"name": 1,
"feeClassifications": {
"$filter": {
"input": {
"$map": {
"input": "$feeClassifications",
"as": "fclass",
"in": {
"$cond": [
{ "$lt": [ "$$fclass._trashed", 0 ] },
{
"_updated": "$$fclass._updated",
"_created": "$$fclass._created",
"name": "$$fclass.name",
"_id": "$$fclass._id",
"fees": {
"$filter": {
"input": "$$fclass.fees",
"as": "fees",
"cond": { "$lt": [ "$$fees._trashed", 0 ] }
}
}
},
false
]
}
}
},
"as": "cls",
"cond": "$$cls"
}
}
}}
]
)
In the upcoming MongoDB release (as of this writing and since MongoDB 3.3.5), You can replace the $cond expression in the the $map expression with a $switch expression:
Permit.aggregate(
[
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{ "$project": {
"_updated": 1,
"_created": 1,
"name": 1,
"feeClassifications": {
"$filter": {
"input": {
"$map": {
"input": "$feeClassifications",
"as": "fclass",
"in": {
"$switch": {
"branches": [
{
"case": { "$lt": [ "$$fclass._trashed", 0 ] },
"then": {
"_updated": "$$fclass._updated",
"_created": "$$fclass._created",
"name": "$$fclass.name",
"_id": "$$fclass._id",
"fees": {
"$filter": {
"input": "$$fclass.fees",
"as": "fees",
"cond": { "$lt": [ "$$fees._trashed", 0 ] }
}
}
}
}
],
"default": false
}
}
}
},
"as": "cls",
"cond": "$$cls"
}
}
}}
]
)
For more complicated bigdats, it would be unnecessarily difficult.
Just edit it in $filter input by adding a dotted annotation field.You can search the document to any depth of JSON by dotted annotation without further complicated $filter mapping.
"$filter":{
"input": "$feeClassifications._trashed",
"as": "trashed",
"cond": { "$lt": [ "$$trashed._trashed", 0 ] }
}

How can I select document with array items containing in values array?

I have collection in mongodb (3.0):
{
_id: 1,
m: [{_id:11, _t: 'type1'},
{_id:12, _t: 'type2'},
{_id:13, _t: 'type3'}]
},
{
_id: 2,
m: [{_id:21, _t: 'type1'},
{_id:22, _t: 'type21'},
{_id:23, _t: 'type3'}]
}
I want to find documents with m attributes where m._t containing ['type1', 'type2'].
Like this:
{
_id: 1,
m: [{_id:11, _t: 'type1'},
{_id:12, _t: 'type2'}]
},
{
_id: 2,
m: [{_id:21, _t: 'type1'}]
}
I tried to use $ and $elemMatch, but couldn't get required result.
How to do it, using find()?
Help me, please! Thanks!
Because the $elemMatch operator limits the contents of the m array field from the query results to contain only the first element matching the $elemMatch condition, the following will only return the an array with the first matching elements
{
"_id" : 11,
"_t" : "type1"
}
and
{
"_id" : 21,
"_t" : "type1"
}
Query using $elemMatch projection:
db.collection.find(
{
"m._t": {
"$in": ["type1", "type2"]
}
},
{
"m": {
"$elemMatch": {
"_t": {
"$in": ["type1", "type2"]
}
}
}
}
)
Result:
/* 0 */
{
"_id" : 1,
"m" : [
{
"_id" : 11,
"_t" : "type1"
}
]
}
/* 1 */
{
"_id" : 2,
"m" : [
{
"_id" : 21,
"_t" : "type1"
}
]
}
One approach you can take is the aggregation framework, where your pipeline would consist of a $match operator, similar to the find query above to filter the initial stream of documents. The next pipeline step would be the crucial $unwind operator that "splits" the array elements to be further streamlined with another $match operator and then the final $group pipeline to restore the original data structure by using the accumulator operator $push.
The following illustrates this path:
db.collection.aggregate([
{
"$match": {
"m._t": {
"$in": ["type1", "type2"]
}
}
},
{
"$unwind": "$m"
},
{
"$match": {
"m._t": {
"$in": ["type1", "type2"]
}
}
},
{
"$group": {
"_id": "$_id",
"m": {
"$push": "$m"
}
}
}
])
Sample Output:
/* 0 */
{
"result" : [
{
"_id" : 2,
"m" : [
{
"_id" : 21,
"_t" : "type1"
}
]
},
{
"_id" : 1,
"m" : [
{
"_id" : 11,
"_t" : "type1"
},
{
"_id" : 12,
"_t" : "type2"
}
]
}
],
"ok" : 1
}
To get your "filtered" result, the $redact with the aggregation pipeline is the fastest way:
db.junk.aggregate([
{ "$match": { "m._t": { "$in": ["type1", "type2"] } } },
{ "$redact": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ { "$ifNull": ["$_t", "type1"] }, "type1" ] },
{ "$eq": [ { "$ifNull": ["$_t", "type2"] }, "type2" ] }
],
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}}
])
The $redact operator sets up a logical filter for the document that can also traverse into the array levels. Note that this is matching on _t at all levels of the document, so make sure there are no other elements sharing this name.
The query uses $in for selection just as the logical filter uses $or. Anything that does not match, gets "pruned".
{
"_id" : 1,
"m" : [
{
"_id" : 11,
"_t" : "type1"
},
{
"_id" : 12,
"_t" : "type2"
}
]
}
{
"_id" : 2,
"m" : [ { "_id" : 21, "_t" : "type1" } ]
}
Short and sweet and simple.
A bit more cumbersome, but a reasonably safer is to use this construct with $map and $setDifference to filter results:
db.junk.aggregate([
{ "$match": { "m._t": { "$in": ["type1", "type2"] } } },
{ "$project": {
"m": {
"$setDifference": [
{ "$map": {
"input": "$m",
"as": "el",
"in": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ "$$el._t", "type1" ] },
{ "$eq": [ "$$el._t", "type2" ] }
]
},
"then": "$$el",
"else": false
}
}
}},
[false]
]
}
}}
])
The $map evaluates the conditions against each element and the $setDifference removes any of those condtions that returned false rather than the array content. Very similar to the $cond in redact above, but it is just working specifically with the one array and not the whole document.
In future MongoDB releases ( currently available in development releases ) there will be the $filter operator, which is very simple to follow:
db.junk.aggregate([
{ "$match": { "m._t": { "$in": ["type1", "type2"] } } },
{ "$project": {
"m": {
"$filter": {
"input": "$m",
"as": "el",
"cond": {
"$or": [
{ "$eq": [ "$$el._t", "type1" ] },
{ "$eq": [ "$$el._t", "type2" ] }
]
}
}
}
}}
])
And that will simply remove any array element that does not match the specified conditions.
If you want to filter array content on the server, the aggregation framework is the way to do it.

Check if an element appears in an array during the projection stage of a mongo aggregation pipeline

I've got a collection of mongo documents like -
{
"_id" : "c959e4d6-961d-4043-ade6-2f93aa055e11",
"events" : [
"clickOut"
"showHoverAd",
"closeHoverAd"
]
}
{
"_id" : "d0dcb2be-f8bc-45cd-8337-d89a16063b08",
"events" : [
"zoom",
"pan"
]
}
{
"_id" : "9179b26e-e45c-48ab-93f6-e73b8ebe559b",
"events" : [
"clickOut"
]
}
{
"_id" : "db0b82ad-7a33-4ce8-9117-f6ecf041d0d9",
"events" : [
"adjustStars",
"adjustPrice",
"closeHoverAd",
"showHoverAd"
]
}
I'm trying to use a projection stage in an aggregation pipeline to identify if a particular string appears in the events field.
db.events.aggreate([
{$project: {
session: '$_id',
clickedOut: {
$cond: [{$elemMatch: {'$events':'clickOut'}},true,false]
}
}}
])
I'm getting an error - exception: invalid operator '$elemMatch'.
I want my output documents to look like -
{
"session" : "c959e4d6-961d-4043-ade6-2f93aa055e11",
"clickedOut" : false
}
{
"session" : "d0dcb2be-f8bc-45cd-8337-d89a16063b08",
"clickedOut" : true
}
But I can't seem to find a way of doing it. I've tried using $in and $all or simply
$cond: {'$events':'clickOut'}
but I'm not getting anywhere.
Use the following aggregation:
db.events.aggregate([
{
"$unwind": "$events"
},
{
"$project": {
"_id": 0,
"session": "$_id",
"clickedOut": {
"$cond": [ { "$eq": [ "$events", "clickOut" ] }, 1, 0 ]
}
}
},
{
"$group": {
"_id": "$session",
"count": {
"$sum": "$clickedOut"
}
}
},
{
"$project": {
"_id": 0,
"session": "$_id",
"clickedOut": {
"$cond": [ { "$eq": [ "$count", 1 ] }, true, false ]
}
}
},
]);
Output:
/* 1 */
{
"result" : [
{
"session" : "db0b82ad-7a33-4ce8-9117-f6ecf041d0d9",
"clickedOut" : false
},
{
"session" : "9179b26e-e45c-48ab-93f6-e73b8ebe559b",
"clickedOut" : true
},
{
"session" : "d0dcb2be-f8bc-45cd-8337-d89a16063b08",
"clickedOut" : false
},
{
"session" : "c959e4d6-961d-4043-ade6-2f93aa055e11",
"clickedOut" : true
}
],
"ok" : 1
}