Mongodb $nin querying sub-document - mongodb

Here is the sample document of my MongoDB:
user: {
_id:1,
name:'xyz',
age:12,
mobile:21321312,
transaction:[
{
trans_id:1,
prod:'a',
purchasedAt:ISODate("2015-02-01"),
},
{
trans_id:2,
prod:'b',
purchasedAt:ISODate("2015-02-01")
},
{
trans_id:3,
prod:'c',
purchasedAt:ISODate("2014-11-24")
}
]
}
I want to get the users who have purchased product 'a' on date '2015-02-01' but not have purchased the product 'b' or 'c' on same day. So I tried querying:
db.user.find({transaction:{$elemMatch:{prod:'a',purchasedAt:ISODate("2015-02-01")}}, transaction:{$elemMatch:{prod:{$nin:['b', 'c']}, purchasedAt:ISODate("2015-02-01")}}})
But the query seems to return wrong result. It contains some users who have purchased product 'c' on the same day. So I tried:
db.user.find({transaction:{$elemMatch:{prod:{$in:['a'], $nin:['b','c']}, purchasedAt:ISODate("2015-02-01")}}})
db.user.find({$and:[{transaction:{$elemMatch:{prod:'a',purchasedAt:ISODate("2015-02-01")}}}, {transaction:{$elemMatch:{prod:{$nin:['b', 'c']}, purchasedAt:ISODate("2015-02-01")}}}]})
But none seems to work. I always get the product with same purchased date which is in $nin part. I have tried other queries also but those are of same kind as above (like querying with dot '.' operator) and are trivial to mention here. Is there any way to get the result I want?

Try this:
db.test.find({
"$and" : [
{ "transaction" : {
"$elemMatch" : { "prod" : "a", "purchasedAt" : ISODate("2015-02-01") }
} },
{ "transaction" : { "$not" : {
"$elemMatch" : { "prod" : "b", "purchasedAt" : ISODate("2015-02-01") }
} } },
{ "transaction" : { "$not" : {
"$elemMatch" : { "prod" : "c", "purchasedAt" : ISODate("2015-02-01") }
} } }
]
})

Following two ways you can find out your results
1> Using mongo aggregation
db.collectionName.aggregate({
"$unwind": "$user.transaction"
}, {
"$match": {
"$and": [{
"user.transaction.prod": "a"
}, {
"user.transaction.prod": {
"$nin": ["b", "c"]
}
}, {
"user.transaction.purchasedAt": ISODate("2015-02-01T00:00:00Z")
}]
}
}).pretty()
2> Using find with projection
db.collectionName.find({
"user.transaction.prod": "a",
"user.transaction.purchasedAt": ISODate("2015-02-01T00:00:00Z")
},
{
"user.transaction.$": 1
}).pretty()

Related

mongodb match all conditions with a single array element

I have an array of tests like this,
{
"tests" : [
{
"testId" : "1",
"details" : {
"status" : "Completed"
}
},
{
"testId" : "2",
"details" : {
"status" : "InProgress"
}
},
{
"testId" : "3",
"details" : {
"status" : "Completed"
}
},
]
}
I want to search for complete tests with id as 1 or 2.
So I wrote a match query like this
{
$match : { $and : [ { "tests.testId" : { $in : [1, 2] } }
,{ "tests.details.status" : "Completed" }
]
}
}
The problem with this query is it fetches all documents with Ids as 1 or 2 even though the status is not Completed. For example the above query fetches these two documents
{
"testId" : "1",
"details" : {
"status" : "Completed"
}
},
{
"testId" : "2",
"details" : {
"status" : "InProgress"
}
},
What I need is to the query to search for a parituclat array element for both the condition.
I also tried with $elemMatch. It still doesn't work. Any help is much appreciated.
We can use aggregate pipeline with $filter to return the matched array elements only
try this
db.collection.aggregate([
{
$match: { // to filter the documents
$and: [
{
"tests.testId": { $in: ["1", "2"] }
},
{
"tests.details.status": "Completed"
}
]
}
},
{
$project: { // then use this $project stage to use the $filter operator in it
tests: {
$filter: { // this is to filter the tests array
input: "$tests",
as: "test",
cond: {
$and: [
{
$in: [
"$$test.testId", ["1", "2"]
]
},
{
$eq: [
"$$test.details.status",
"Completed"
]
}
]
}
}
}
}
}
])
you can test it here Mongo Playground
hope it helps
Note, If testId is of type ObjectId, then you have to use ObjectIds not strings in the aggregate pipeline

MongoDB: projection $ when find document into nested arrays

I have the following document of collection "user" than contains two nested arrays:
{
"person" : {
"personId" : 78,
"firstName" : "Mario",
"surname1" : "LOPEZ",
"surname2" : "SEGOVIA"
},
"accounts" : [
{
"accountId" : 42,
"accountRegisterDate" : "2018-01-04",
"banks" : [
{
"bankId" : 1,
"name" : "Bank LTD",
},
{
"bankId" : 2,
"name" : "Bank 2 Corp",
}
]
},
{
"accountId" : 43,
"accountRegisterDate" : "2018-01-04",
"banks" : [
{
"bankId" : 3,
"name" : "Another Bank",
},
{
"bankId" : 4,
"name" : "BCT bank",
}
]
}
]
}
I'm trying to get a query that will find this document and get only this subdocument at output:
{
"bankId" : 3,
"name" : "Another Bank",
}
I'm getting really stucked. If I run this query:
{ "accounts.banks.bankId": "3" }
Gets the whole document. And I've trying combinations of projection with no success:
{"accounts.banks.0.$": 1} //returns two elements of array "banks"
{"accounts.banks.0": 1} //empty bank array
Maybe that's not the way to query for this and I'm going in bad direction.
Can you please help me?
You can try following solution:
db.user.aggregate([
{ $unwind: "$accounts" },
{ $match: { "accounts.banks.bankId": 3 } },
{
$project: {
items: {
$filter: {
input: "$accounts.banks",
as: "bank",
cond: { $eq: [ "$$bank.bankId", 3 ] }
}
}
}
},
{
$replaceRoot : {
newRoot: { $arrayElemAt: [ "$items", 0 ] }
}
}
])
To be able to filter accounts by bankId you need to $unwind them. Then you can match accounts to the one having bankId equal to 3. Since banks is another nested array, you can filter it using $filter operator. This will give you one element nested in items array. To get rid of the nesting you can use $replaceRoot with $arrayElemAt.

Find in 2 levels of nested arrays in MongoDB

After googling a lot, I can't locate something that can handle this. And it's supposed to be simple, I guess. I've this simple json...
{
"_id" : ObjectId("555bd34329de3cf232434ef2"),
"clients" : [
{
"Client1" : {
"positions" : [
{
"systemId" : "xxx1",
"type" : "alfa"
},
{
"systemId" : "xxx2",
"type" : "bravo"
},
{
"systemId" : "xxx3",
"type" : "charlie"
}
]
},
"Client2" : {
"positions" : [
{
"systemId" : "xxx4",
"type" : "alfa"
},
{
"systemId" : "xxx5",
"type" : "bravo"
},
{
"systemId" : "xxx6",
"type" : "charlie"
}
]
}
}
]
}
And I'm trying to perform a query based on the {systemId} into positions array that's inside an array that's inside another array. I can easily use find() in a single level array. But at this time I need an additional depth and I really facing difficulty. Can someone give me a help ?
tyvm !
If you want to find out Client1.positions with systemId and Client2.positions with systemId use following aggregation :
db.collectionName.aggregate([
{
"$unwind": "$clients"
},
{
"$unwind": "$clients.Client1.positions"
},
{
"$unwind": "$clients.Client2.positions"
},
{
"$match": {
"clients.Client1.positions.systemId": "xxx1",
"clients.Client2.positions.systemId": "xxx4"
}
}
]).pretty()
If you want to find out only Client1 then remove "$unwind": "$clients.Client2.positions" and in match "clients.Client2.positions.systemId": "xxx4"
As per your sample data, clients contains different objects like Client1 Client2 etc which further contains positions array of objects. In this case to find systemId, You need to use $elemMatch as following:
db.collection.find({
"clients": {
$elemMatch: {
"Client2.positions": {
$elemMatch: {
"systemId": "xxx4"
}
}
}
}
})

In MongoDB how to find documents where property is object but not Array

I have collection users where each contains property gyms e.g.:
{
"_id" : ObjectId("aaaaa"),
"firstName" : "first",
"lastName" : "second",
"email" : "aaa#aaaa",
"password" : "aaaaa",
"gyms" : [
{
"name" : "aaaaaa",
"address" : "aaaaaaaaaaaaa"
}
]
}
However when I run db.users.aggregate(..) I get:
exception: Value at end of $unwind field path '$gyms' must be an Array, but is a Object
It seems that some of the users documents contain gym:{} or no gym at all rather than array. I need to find these documents, how would I do that?
EDIT:
Aggregate command I run:
db.users.aggregate({ $unwind: "$gyms" }, { $match: { "gyms.address": { $exists: true } } } )
Using {$type: "object"} gives you results containing documents with arrays too. So, to check for pure objects, just add an extra check to ensure that arrays are discarded, viz., obj.0.key: {$exists: false}.
In your case,
db.users.find({"gyms": {$type: "object"}, "gyms.0.name": {$exists: false}}).map(function(u) {
// modify u.gyms to your satisfaction
db.conversations.save(c);
})
Try this may be it also help you
db.users.aggregate([{
"$match": {
"$nor": [{
"gyms": {
"$exists": false
}
}, {
"gyms": {
"$size": 0
}
}, {
"gyms": {
"$size": 1
}
}]
}
}, {
"$unwind": "$gyms"
}]).pretty()
Try something like this:
> db.test.drop()
> db.test.insert({ "x" : ["an array"] })
> db.test.insert({ "x" : { "type" : "object" } })
> db.test.find({ "x" : { "$type" : 3 } })
{ "x" : { "type" : "object" } }

Dynamic keys from values in MongoDB

Say I have this:
{
"_id" : "ENVD",
"years" : [
{
"year" : "2013",
"avgInstructor" : 5.144999999999998
},
{
"year" : "2012",
"avgInstructor" : 5.194436090225564
}
]
}
I need to be able to find the difference in the avgInstructor field from 2012-13. I was thinking I could transform the keys somehow using a $project which would make the year be the key, and the avgInstructor rating be the value. So it would look like this:
{
"_id" : "ENVD",
"years" : {
"2013" : 5.144999999999998,
"2012" : 5.194436090225564
}
}
Is this possible? Keep in mind my main goal is to be able to run a subtraction like this pseudocode : years['2013'].avgInstructor - years['2013'].avgInstructor. So if you see an easier way, that would be great as well. I am not sure of the best way to go about this in the context of an aggregation pipeline. Can someone help?
For people that ends up here looking for a solution to transform an array to an object using aggregation in a more recent version of MongoDB:
MongoDB 3.4.4 introduced $arrayToObject
You have to $map years
{
$set: {
years: {
$map: {
input: "$years",
as: "year",
in: [
"$$year.year",
"$$year.avgInstructor"
]
}
}
}
}
to be like this
{
"_id" : "ENVD",
"years": [
["2013", 5.144999999999998],
["2012", 5.194436090225564]
]
}
then use $arrayToObject
{
$set: {
years: {
$arrayToObject: "$years"
}
}
}
Or combine the two steps together
{
$set: {
years: {
$arrayToObject: {
$map: {
input: "$years",
as: "year",
in: [
"$$year.year",
"$$year.avgInstructor"
]
}
}
}
}
}
A Possible answer ...
Unwind first
project to make it a little easier to handle
Sort on _id and then on year
group by _id to pickup the first and the last value
final projection to get subtracted value
db.coll.aggregate ( [
{ "$unwind" : "$years" } ,
{ $project : { "year" : "$years.year", "avgInstructor" : "$years.avgInstructor" } },
{ $sort : { "_id" : 1, "year" : 1 } },
{ $group : { "_id" : "$_id", "val_min" : { $first : "$avgInstructor" }, "val_max" : { $last : "$avgInstructor" } } },
{ $project : { "diff" : { $subtract : [ "$val_min", "$val_max" ] } } }
] )