multiple conditions in $match in mongodb - mongodb

I have a collection AccountSupport. I has array of supports property. I want to filter the record on parent property and a property of an array
db = {
"AccountSupport": [
{
"_id" : ObjectId("5e9c6170b38c373530c5b00a"),
"accountName" : "domestic",
"supports" : [
{
"subject" : "Traverse",
"desc" : "Travers support consolidation",
},
{
"subject" : "Non Traverse",
"desc" : "Non Travers support consolidation",
},
{
"subject" : "Domestic Traverse",
"desc" : "Domestic Travers support consolidation",
}
],
}
I want to filter on accountName and supports.subject.
Below is my query
db.AccountSupport.aggregate([
{
"$match": {
"$and": [
{
"supports.subject": "Traverse"
},
{
"accountName": "domestic"
}
]
}
},
{
"$unwind": "$supports"
},
{
"$project": {
"SupportName": "$supports.subject",
"desc": "$supports.desc"
}
}
])
The above query returns me all the supports of a particular accountName whereas i only want the single object of the matched subject. Is above the simplest approach to do it?
MongoPlayGround
MongoPlayGround for my query

Took a little bit of experimenting but try this:
db.AccountSupport.aggregate([
{
$match: {
"accountName": "domestic",
"supports.subject": "Traverse"
}
},
{
$project: {
accountName: "$accountName",
subject: {
$filter: {
input: "$supports",
as: "supports",
cond: {
$eq: [
"$$supports.subject",
"Traverse"
]
}
}
},
_id: 0
}
}
])
Which returns:
[
{
"accountName": "domestic",
"subject": [
{
"desc": "Travers support consolidation",
"subject": "Traverse"
}
]
}
]

One option is to just add another $match step to your aggregation query. Since the $unwind breaks your doc into 3 docs, you can then match the individual doc you are interested in returning. The $filter solution by james looks worth investigating and could be the simpler approach for you.
Query:
db.AccountSupport.aggregate([
{
"$match": {
"$and": [
{
"supports.subject": "Traverse"
},
{
"accountName": "domestic"
}
]
}
},
{
"$unwind": "$supports"
},
{
"$match": {
"supports.subject": "Traverse"
}
},
{
"$project": {
"SupportName": "$supports.subject",
"desc": "$supports.desc"
}
}
])
Result:
[
{
"SupportName": "Traverse",
"_id": ObjectId("5e9c6170b38c373530c5b00a"),
"desc": "Travers support consolidation"
}
]
MongoPlayground

Below is the query to get my needed result, not sure if this is the best practice though, as the other answers posted works fine as well for the mentioned requirement
db.AccountSupport.find({'accountName': 'domestic' },
{
'supports':
{
'$elemMatch': { 'subject': 'Traverse'}
}
})

Related

Pivot mongo data in aggregation pipeline after $addToSet: one set becomes keys, one set values

I have a pipeline so far that looks like this:
db.getCollection("Members").aggregate(
[
{
"$match" : {
"member.MemberInfo.BusinessUnitCode" : "20"
}
},
{
"$group" : {
"_id" : "$_id",
"items" : {
"$addToSet" : {
"Type" : "$member.MemberIdentifications.MemberIdentification.IdentificationType",
"Value" : "$member.MemberIdentifications.MemberIdentification.Identifier"
}
}
}
}}
And I have results that are:
{
"_id" : ObjectId("53ad60c1600f5b241c693cbd"),
"items" : [
{
"Type" : [
"Medicaid ID",
"Medicare ID",
"Amisys ID",
"Social Security Number",
"Market Prominence ID",
"MBI",
"MPI"
],
"Value" : [
"221075***",
"450807099M",
"C0004125301",
"45*******",
"N00020269104",
"3K13EA8EY99",
"17296217"
]
}
]}
What I want is this:
{
"_id" : ObjectId("53ad60c1600f5b241c693cbd"),
{"Medicaid ID": "221075501",
"Medicare ID": "450807099M",
"Amisys ID": "C0004125301",
"Social Security Number": "45*******",
"Market Prominence ID": "N00020269104",
"MBI": "3K13EA8EY99",
"MPI": "17296217"
}
}
In table view this would look like (obviously with many more records):
_id Medicaid ID Medicare ID Amisys ID Social Security Number Market
53ad60c1600f5b241c693cbd 221075*** 450807099M C0004125301 45******* N00020269104
I'm not sure what next steps to take. I've looked at a similar question about pivoting data in mongo to make rows columns here, but Its a different case and it doesnt really apply to mine. What Im basically doing is pivoting from the two sets I created so one set become the kets and one set becomes the values of one new document
You may solve it as you would do with for loop. For your aggregation, add 3 steps.
Explanation
$unwind: "$items" will create single document for each item.
$range: [0,{$size: "$items.Type"},1] returns [0,1,2,3,4,5,6]
We create array with this structure [{ k:"Type[i]", v:"Value[i]" }]
If we use $arrayToObject + $replaceRoot we get desired result
db.Members.aggregate([
{
$unwind: "$items"
},
{
$project: {
_id: 1,
data: {
$map: {
input: {
$range: [
0,
{
$size: "$items.Type"
},
1
]
},
in: {
k: {
$arrayElemAt: [
"$items.Type",
"$$this"
]
},
v: {
$arrayElemAt: [
"$items.Value",
"$$this"
]
}
}
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: {
$concatArrays: [
[
{
k: "_id",
v: "$_id"
}
],
"$data"
]
}
}
}
}
])
MongoPlayground
NOTE: You can get desired result with $unwind explained here
EDIT:
Try to change:
db.Members.aggregate([
{
"$match": {
"member.MemberInfo.BusinessUnitCode": "20"
}
},
{
"$group": {
"_id": "$_id",
"items": {
"$push": {
"k": "$member.MemberIdentifications.MemberIdentification.IdentificationType",
"v": "$member.MemberIdentifications.MemberIdentification.Identifier"
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: {
$concatArrays: [
[
{
k: "_id",
v: "$_id"
}
],
"$items"
]
}
}
}
}
])

MongoDB/Mongoose how to return a nested subdocument by _id

MongoDB newbie here.
I have a 'client' document that looks like this:
{
name: "myClient",
products: [{
name: "myProduct1",
environments: [{
name: "myEnvironment1",
changeLogs: [
{ "some": "fields21" },
{ "some": "fields22" }
]
},
{
name: "myEnvironment2",
changeLogs: [
{ "some": "fields11" },
{ "some": "fields12" }
]
}
]
},
{
name: "myProduct2",
environments: [{
name: "myEnv1",
changeLogs: [
{ "some": "fields1" },
{ "some": "fields2" }
]
},
{
name: "myEnv1",
changeLogs: [
{ "some": "fields3" },
{ "some": "fields4" }
]
}
]
}]
}
So a client has many products, which has many environments, which has many changeLogs. I am looking to return a list of changeLogs for a given environment, with only the environment._id to go on.
I can find the correct client document using just this _id:
db.clients.find({'products.environments._id': ObjectId("5a1bf4b410842200043d56ff")})
But this returns the entire client document. What I want is to return just the changeLogs array from the environment with _id: ObjectId("5a1bf4b410842200043d56ff")
Assuming I have the _id of the first environment of the first product, my desired output is the following:
[
{ "some": "fields21" },
{ "some": "fields22" }
]
What query would you recommend I use to achieve this?
Many thanks in advance for any help. The docs thus far have only been confusing, but I'm sure I'll get there in the end!
The idea here is to $unwind the products array so that its environments can be fed as input to $filter after a $match on the _id.
(lets assume the enviroment _id is 1)
db.collection.aggregate([
{
$unwind: "$products"
},
{
$match: {
"products.environments._id": 1
}
},
{
$project: {
"logsArray": {
$filter: {
input: "$products.environments",
as: "env",
cond: {
$eq: [
"$$env._id",
1
]
}
}
}
}
},
{
$unwind: "$logsArray"
}
])
O/P Should be like:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"logsArray": {
"changeLogs": [
{
"some": "fields21"
},
{
"some": "fields22"
}
],
"id": 1,
"name": "myEnvironment1"
}
}
]
Note: notice the last stage $unwind of logsArray which I think is just pretty-fying the ouput. Otherwise without it also the resultant is acceptable (if you agree, can remove that).
This is just another way of doing the aggregation query. This gets the desired result.
Note I am using the "name" field of the "environments" from the sample document you had provided. The "name" can be substituted with "id" as needed.
var ENV = "myEnvironment1";
db.env.aggregate( [
{ $match: {
{ $unwind: "$products" },
{ $unwind: "$products.environments" },
{ $match: { "products.environments.name": ENV} },
{ $project: { _id: 0, changeLogs: "$products.environments.changeLogs" } },
] )
The result:
{ "changeLogs" : [ { "some" : "fields21" }, { "some" : "fields22" } ] }
If the variable ENV's value is changed, then the result will be accordingly; e.g.,: ENV = "myEnv1";
{ "changeLogs" : [ { "some" : "fields1" }, { "some" : "fields2" } ] }
{ "changeLogs" : [ { "some" : "fields3" }, { "some" : "fields4" } ] }
db.clients.aggregate([
{
$unwind: "$products"
},
{
$unwind: "$products.environments"
},
{
$match: { "products.environments._id": ObjectId("5a1bf4b410842200043fffff") }
},
{
$project: { _id: 0, changeLogs: "$products.environments.changeLogs" }
}
]).pretty()
Results in:
{
"changeLogs": [
{ "some": "fields21" },
{ "some": "fields22" }
]
}
For those finding that code confusing I found it very useful to just add one aggregate method at a time, look at the results, and then add then next method to the pipeline. Once the pipeline was complete I also experimented with removing intermediary steps to see if I could get the same results with less piping.

Matching ObjectId to String for $graphLookup

I'm trying to run a $graphLookup like demonstrated in print bellow:
The objective is to, given a specific record (commented $match there), retrieve it's full "path" throught immediateAncestors property. As you can see, it's not happening.
I introduced $convert here to deal with _id from collection as string, believing it could be possible to "match" with _id from immediateAncestors records list (which is a string).
So, I did run another test with different data (no ObjectIds involved):
db.nodos.insert({"id":5,"name":"cinco","children":[{"id":4}]})
db.nodos.insert({"id":4,"name":"quatro","ancestors":[{"id":5}],"children":[{"id":3}]})
db.nodos.insert({"id":6,"name":"seis","children":[{"id":3}]})
db.nodos.insert({"id":1,"name":"um","children":[{"id":2}]})
db.nodos.insert({"id":2,"name":"dois","ancestors":[{"id":1}],"children":[{"id":3}]})
db.nodos.insert({"id":3,"name":"três","ancestors":[{"id":2},{"id":4},{"id":6}]})
db.nodos.insert({"id":7,"name":"sete","children":[{"id":5}]})
And the query:
db.nodos.aggregate( [
{ $match: { "id": 3 } },
{ $graphLookup: {
from: "nodos",
startWith: "$ancestors.id",
connectFromField: "ancestors.id",
connectToField: "id",
as: "ANCESTORS_FROM_BEGINNING"
}
},
{ $project: {
"name": 1,
"id": 1,
"ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING.id"
}
}
] )
...which outputs what I was expecting (the five records directly and indirectly connected to the one with id 3):
{
"_id" : ObjectId("5afe270fb4719112b613f1b4"),
"id" : 3.0,
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [
1.0,
4.0,
6.0,
5.0,
2.0
]
}
The question is: there is a way to achieve the objetive I mentioned in the beginning?
I'm running Mongo 3.7.9 (from official Docker)
Thanks in advance!
You are currently using a development version of MongoDB which has some features enabled expected to be released with MongoDB 4.0 as an official release. Note that some features may be subject to change before the final release, so production code should be aware of this before you commit to it.
Why $convert fails here
Probably the best way to explain this is to look at your altered sample but replacing with ObjectId values for _id and "strings" for those under the the arrays:
{
"_id" : ObjectId("5afe5763419503c46544e272"),
"name" : "cinco",
"children" : [ { "_id" : "5afe5763419503c46544e273" } ]
},
{
"_id" : ObjectId("5afe5763419503c46544e273"),
"name" : "quatro",
"ancestors" : [ { "_id" : "5afe5763419503c46544e272" } ],
"children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{
"_id" : ObjectId("5afe5763419503c46544e274"),
"name" : "seis",
"children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{
"_id" : ObjectId("5afe5763419503c46544e275"),
"name" : "um",
"children" : [ { "_id" : "5afe5763419503c46544e276" } ]
}
{
"_id" : ObjectId("5afe5763419503c46544e276"),
"name" : "dois",
"ancestors" : [ { "_id" : "5afe5763419503c46544e275" } ],
"children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{
"_id" : ObjectId("5afe5763419503c46544e277"),
"name" : "três",
"ancestors" : [
{ "_id" : "5afe5763419503c46544e273" },
{ "_id" : "5afe5763419503c46544e274" },
{ "_id" : "5afe5763419503c46544e276" }
]
},
{
"_id" : ObjectId("5afe5764419503c46544e278"),
"name" : "sete",
"children" : [ { "_id" : "5afe5763419503c46544e272" } ]
}
That should give a general simulation of what you were trying to work with.
What you attempted was to convert the _id value into a "string" via $project before entering the $graphLookup stage. The reason this fails is whilst you did an initial $project "within" this pipeline, the problem is that the source for $graphLookup in the "from" option is still the unaltered collection and therefore you don't get the correct details on the subsequent "lookup" iterations.
db.strcoll.aggregate([
{ "$match": { "name": "três" } },
{ "$addFields": {
"_id": { "$toString": "$_id" }
}},
{ "$graphLookup": {
"from": "strcoll",
"startWith": "$ancestors._id",
"connectFromField": "ancestors._id",
"connectToField": "_id",
"as": "ANCESTORS_FROM_BEGINNING"
}},
{ "$project": {
"name": 1,
"ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
}}
])
Does not match on the "lookup" therefore:
{
"_id" : "5afe5763419503c46544e277",
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [ ]
}
"Patching" the problem
However that is the core problem and not a failing of $convert or it's aliases itself. In order to make this actually work we can instead create a "view" which presents itself as a collection for the sake of input.
I'll do this the other way around and convert the "strings" to ObjectId via $toObjectId:
db.createView("idview","strcoll",[
{ "$addFields": {
"ancestors": {
"$ifNull": [
{ "$map": {
"input": "$ancestors",
"in": { "_id": { "$toObjectId": "$$this._id" } }
}},
"$$REMOVE"
]
},
"children": {
"$ifNull": [
{ "$map": {
"input": "$children",
"in": { "_id": { "$toObjectId": "$$this._id" } }
}},
"$$REMOVE"
]
}
}}
])
Using the "view" however means that the data is consistently seen with the values converted. So the following aggregation using the view:
db.idview.aggregate([
{ "$match": { "name": "três" } },
{ "$graphLookup": {
"from": "idview",
"startWith": "$ancestors._id",
"connectFromField": "ancestors._id",
"connectToField": "_id",
"as": "ANCESTORS_FROM_BEGINNING"
}},
{ "$project": {
"name": 1,
"ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
}}
])
Returns the expected output:
{
"_id" : ObjectId("5afe5763419503c46544e277"),
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [
ObjectId("5afe5763419503c46544e275"),
ObjectId("5afe5763419503c46544e273"),
ObjectId("5afe5763419503c46544e274"),
ObjectId("5afe5763419503c46544e276"),
ObjectId("5afe5763419503c46544e272")
]
}
Fixing the problem
With all of that said, the real issue here is that you have some data which "looks like" an ObjectId value and is in fact valid as an ObjectId, however it has been recorded as a "string". The basic issue to everything working as it should is that the two "types" are not the same and this results in an equality mismatch as the "joins" are attempted.
So the real fix is still the same as it always has been, which is to instead go through the data and fix it so that the "strings" are actually also ObjectId values. These will then match the _id keys which they are meant to refer to, and you are saving a considerable amount of storage space since an ObjectId takes up a lot less space to store than it's string representation in hexadecimal characters.
Using MongoDB 4.0 methods, you "could" actually use the "$toObjectId" in order to write a new collection, just in much the same matter that we created the "view" earlier:
db.strcoll.aggregate([
{ "$addFields": {
"ancestors": {
"$ifNull": [
{ "$map": {
"input": "$ancestors",
"in": { "_id": { "$toObjectId": "$$this._id" } }
}},
"$$REMOVE"
]
},
"children": {
"$ifNull": [
{ "$map": {
"input": "$children",
"in": { "_id": { "$toObjectId": "$$this._id" } }
}},
"$$REMOVE"
]
}
}}
{ "$out": "fixedcol" }
])
Or of course where you "need" to keep the same collection, then the traditional "loop and update" remains the same as what has always been required:
var updates = [];
db.strcoll.find().forEach(doc => {
var update = { '$set': {} };
if ( doc.hasOwnProperty('children') )
update.$set.children = doc.children.map(e => ({ _id: new ObjectId(e._id) }));
if ( doc.hasOwnProperty('ancestors') )
update.$set.ancestors = doc.ancestors.map(e => ({ _id: new ObjectId(e._id) }));
updates.push({
"updateOne": {
"filter": { "_id": doc._id },
update
}
});
if ( updates.length > 1000 ) {
db.strcoll.bulkWrite(updates);
updates = [];
}
})
if ( updates.length > 0 ) {
db.strcoll.bulkWrite(updates);
updates = [];
}
Which is actually a bit of a "sledgehammer" due to actually overwriting the entire array in a single go. Not a great idea for a production environment, but enough as a demonstration for the purposes of this exercise.
Conclusion
So whilst MongoDB 4.0 will add these "casting" features which can indeed be very useful, their actual intent is not really for cases such as this. They are in fact much more useful as demonstrated in the "conversion" to a new collection using an aggregation pipeline than most other possible uses.
Whilst we "can" create a "view" which transforms the data types to enable things like $lookup and $graphLookup to work where the actual collection data differs, this really is only a "band-aid" on the real problem as the data types really should not differ, and should in fact be permanently converted.
Using a "view" actually means that the aggregation pipeline for construction needs to effectively run every time the "collection" ( actually a "view" ) is accessed, which creates a real overhead.
Avoiding overhead is usually a design goal, therefore correcting such data storage mistakes is imperative to getting real performance out of your application, rather than just working with "brute force" that will only slow things down.
A much safer "conversion" script which applied "matched" updates to each array element. The code here requires NodeJS v10.x and a latest release MongoDB node driver 3.1.x:
const { MongoClient, ObjectID: ObjectId } = require('mongodb');
const EJSON = require('mongodb-extended-json');
const uri = 'mongodb://localhost/';
const log = data => console.log(EJSON.stringify(data, undefined, 2));
(async function() {
try {
const client = await MongoClient.connect(uri);
let db = client.db('test');
let coll = db.collection('strcoll');
let fields = ["ancestors", "children"];
let cursor = coll.find({
$or: fields.map(f => ({ [`${f}._id`]: { "$type": "string" } }))
}).project(fields.reduce((o,f) => ({ ...o, [f]: 1 }),{}));
let batch = [];
for await ( let { _id, ...doc } of cursor ) {
let $set = {};
let arrayFilters = [];
for ( const f of fields ) {
if ( doc.hasOwnProperty(f) ) {
$set = { ...$set,
...doc[f].reduce((o,{ _id },i) =>
({ ...o, [`${f}.$[${f.substr(0,1)}${i}]._id`]: ObjectId(_id) }),
{})
};
arrayFilters = [ ...arrayFilters,
...doc[f].map(({ _id },i) =>
({ [`${f.substr(0,1)}${i}._id`]: _id }))
];
}
}
if (arrayFilters.length > 0)
batch = [ ...batch,
{ updateOne: { filter: { _id }, update: { $set }, arrayFilters } }
];
if ( batch.length > 1000 ) {
let result = await coll.bulkWrite(batch);
batch = [];
}
}
if ( batch.length > 0 ) {
log({ batch });
let result = await coll.bulkWrite(batch);
log({ result });
}
await client.close();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
Produces and executes bulk operations like these for the seven documents:
{
"updateOne": {
"filter": {
"_id": {
"$oid": "5afe5763419503c46544e272"
}
},
"update": {
"$set": {
"children.$[c0]._id": {
"$oid": "5afe5763419503c46544e273"
}
}
},
"arrayFilters": [
{
"c0._id": "5afe5763419503c46544e273"
}
]
}
},
{
"updateOne": {
"filter": {
"_id": {
"$oid": "5afe5763419503c46544e273"
}
},
"update": {
"$set": {
"ancestors.$[a0]._id": {
"$oid": "5afe5763419503c46544e272"
},
"children.$[c0]._id": {
"$oid": "5afe5763419503c46544e277"
}
}
},
"arrayFilters": [
{
"a0._id": "5afe5763419503c46544e272"
},
{
"c0._id": "5afe5763419503c46544e277"
}
]
}
},
{
"updateOne": {
"filter": {
"_id": {
"$oid": "5afe5763419503c46544e274"
}
},
"update": {
"$set": {
"children.$[c0]._id": {
"$oid": "5afe5763419503c46544e277"
}
}
},
"arrayFilters": [
{
"c0._id": "5afe5763419503c46544e277"
}
]
}
},
{
"updateOne": {
"filter": {
"_id": {
"$oid": "5afe5763419503c46544e275"
}
},
"update": {
"$set": {
"children.$[c0]._id": {
"$oid": "5afe5763419503c46544e276"
}
}
},
"arrayFilters": [
{
"c0._id": "5afe5763419503c46544e276"
}
]
}
},
{
"updateOne": {
"filter": {
"_id": {
"$oid": "5afe5763419503c46544e276"
}
},
"update": {
"$set": {
"ancestors.$[a0]._id": {
"$oid": "5afe5763419503c46544e275"
},
"children.$[c0]._id": {
"$oid": "5afe5763419503c46544e277"
}
}
},
"arrayFilters": [
{
"a0._id": "5afe5763419503c46544e275"
},
{
"c0._id": "5afe5763419503c46544e277"
}
]
}
},
{
"updateOne": {
"filter": {
"_id": {
"$oid": "5afe5763419503c46544e277"
}
},
"update": {
"$set": {
"ancestors.$[a0]._id": {
"$oid": "5afe5763419503c46544e273"
},
"ancestors.$[a1]._id": {
"$oid": "5afe5763419503c46544e274"
},
"ancestors.$[a2]._id": {
"$oid": "5afe5763419503c46544e276"
}
}
},
"arrayFilters": [
{
"a0._id": "5afe5763419503c46544e273"
},
{
"a1._id": "5afe5763419503c46544e274"
},
{
"a2._id": "5afe5763419503c46544e276"
}
]
}
},
{
"updateOne": {
"filter": {
"_id": {
"$oid": "5afe5764419503c46544e278"
}
},
"update": {
"$set": {
"children.$[c0]._id": {
"$oid": "5afe5763419503c46544e272"
}
}
},
"arrayFilters": [
{
"c0._id": "5afe5763419503c46544e272"
}
]
}
}

$elemMatch against two Array elements if one fails

A bit odd but this is what I am looking for.
I have an array as follow:
Document 1:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "1234"
}
Document 2:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "0000"
}
I would like to use a single query, and send a filter on ZipCode = 1111 && ZipCode4 = 4321, if this fails, the query should look for ZipCode = 1111 && ZipCode4: 0000
Is there a way to do this in a single query ? or do I need to make 2 calls to my database ?
For matching both data set (11111/4321) and (11111/0000), you can use $or and $and with $elemMatch like the following :
db.test.find({
$or: [{
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "4321" }
}
}]
}, {
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "0000" }
}
}]
}]
})
As you want conditional staging, this is not possible but we can get closer to it like this :
db.test.aggregate([{
$match: {
$or: [{
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "4321" }]
}, {
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "0000" }]
}]
}
}, {
$project: {
Items: 1,
match: {
"$map": {
"input": "$Items",
"as": "val",
"in": {
"$cond": [
{ $and: [{ "$eq": ["$$val.ZipCode", "11111"] }, { "$eq": ["$$val.ZipCode4", "4321"] }] },
true,
false
]
}
}
}
}
}, {
$unwind: "$match"
}, {
$group: {
_id: "$match",
data: {
$push: {
_id: "$_id",
Items: "$Items"
}
}
}
}])
The first $match is for selecting only the items we need
The $project will build a new field that check if this items is from the 1st set of data (11111/4321) or the 2nd set of data (11111/0000).
The $unwind is used to remove the array generated by $map.
The $group group by set of data
So in the end you will have an output like the following :
{ "_id" : true, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394972"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] }, { "_id" : ObjectId("58af69ac594b51730a394974"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] } ] }
{ "_id" : false, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394971"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "0000" } ] } ] }
Your application logic can check if there is _id:true in this output array, just take the corresponding data field for _id:true. If there is _id:false in this object take the corresponding data field for _id:false.
In the last $group, you can also use $addToSet to builds 2 field data1 & data2 for both type of data set but this will be painful to use as it will add null object to the array for each one of the opposite type :
"$addToSet": {
"$cond": [
{ "$eq": ["$_id", true] },
"$data",
null
]
}
Here is a gist

MongoDb aggregate and group by two fields depending on values

I want to aggregate over a collection where a type is given. If the type is foo I want to group by the field author, if the type is bar I want to group by user.
All this should happen in one query.
Example Data:
{
"_id": 1,
"author": {
"someField": "abc",
},
"type": "foo"
}
{
"_id": 2,
"author": {
"someField": "abc",
},
"type": "foo"
}
{
"_id": 3,
"user": {
"someField": "abc",
},
"type": "bar"
}
This user field is only existing if the type is bar.
So basically something like that... tried to express it with an $or.
function () {
var results = db.vote.aggregate( [
{ $or: [ {
{ $match : { type : "foo" } },
{ $group : { _id : "$author", sumAuthor : {$sum : 1} } } },
{ { $match : { type : "bar" } },
{ $group : { _id : "$user", sumUser : {$sum : 1} } }
} ] }
] );
return results;
}
Does someone have a good solution for this?
I think it can be done by
db.c.aggregate([{
$group : {
_id : {
$cond : [{
$eq : [ "$type", "foo"]
}, "author", "user"]
},
sum : {
$sum : 1
}
}
}]);
The solution below can be cleaned up a bit...
For "bar" (note: for "foo", you have to change a bit)
db.vote.aggregate(
{
$project:{
user:{ $ifNull: ["$user", "notbar"]},
type:1
}
},
{
$group:{
_id:{_id:"$user.someField"},
sumUser:{$sum:1}
}
}
)
Also note: In you final answer, anything that is not of type "bar" will have an _id=null
What you want here is the $cond operator, which is a ternary operator returning a specific value where the condition is true or false.
db.vote.aggregate([
{ "$group": {
"_id": null,
"sumUser": {
"$sum": {
"$cond": [ { "$eq": [ "$type", "user" ] }, 1, 0 ]
}
},
"sumAuhtor": {
"$sum": {
"$cond": [ { "$eq": [ "$type", "auhtor" ] }, 1, 0 ]
}
}
}}
])
This basically tests the "type" of the current document and decides whether to pass either 1 or 0 to the $sum operation.
This also avoids errant grouping should the "user" and "author" fields contain the same values as they do in your example. The end result is a single document with the count of both types.