Find all entries where one of attributes within array is empty - mongodb

I've following mongodb query:
db
.getCollection("entries")
.find({
$and: [
{
"array.attribute_1": {
$exists: true,
$not: {
$size: 0
}
}
},
{
$or: [
{ "array.attribute_2": { $exists: true, $size: 0 } },
{ "array.attribute_2": { $exists: true, $eq: {} } }
]
},
]
})
And example of my document:
{
_id: 'foo',
array: [
{attribute_1: [], attribute_2: []},
{attribute_1: ['bar'], attribute_2: []}
]
}
In my understanding my query should find all entries that have at least one element within array that has existent and not empty attribute_1 and existent empty array or empty object attribute_2. However, this query finds all entries that has all elements within array that has existent and not empty attribute_1 and existent empty array or empty object attribute_2. As such, my foo entry won't be found.
What should be the correct formula for my requirements?

$find would find the first document with the matching criteria and in your case that first document contains all the arrays. You either need to use $project with $filter or aggregation with $unwind and $match.
Something like this:
db.collection.aggregate([
{ $unwind: "$array" },
{
$match: {
$and: [
{ "array.attribute_1.0": { $exists: true }},
{
$or: [
{ "array.attribute_2.0": { $exists: false } },
{ "array.attribute_2.0": { $eq: {} } }
]
}
]
}
}
])
You can see it working here
Also since you are trying to find out if array is empty and exists at the same time using .0 with $exists is a quick and one statement way to get the same result as with both $exists and $size.

Related

Run a loop through an array of objects in the $project pipeline of MongoDB

I have used the $group pipeline and inside it, I have used $addToSet which has created an array of objects which contains two values - {subGenre (string), flag (bool)}. In the next pipeline of $project, I need to run a loop through this array and I need to select only that element of the array where flag is false.
So my code looks like this:
let data = await Books.aggregate(
[
{
$group: {
_id: genre,
price: { $sum: "$price" },
data: {
$addToSet: {
subGenre: "$subGenre",
flag: "$flagSelectGenre"
}
}
}
}
]
);
This would return documents like:
_id: {
genre: "suspense",
},
price: 10210.6,
data: [
{
subGenre: "Thriller",
flag: false,
},
{
subGenre: "jumpScare",
flag: true,
}
{
subGenre: "horror",
flag: false,
}
]
After this, I need to run a $project pipeline where I have to only project that element of the data array where the flag is true. The flag will be true for only one element.
$project: {
price: "$price",
subGenre: {$...... } // some condition on data array??
}
The final output should look like this:
price: 10210.6,
subGenre: "jumpScare",
You can do it like this:
$filter - to filter items from data array, where flag property is equal to true.
$first - to get first item from above array.
$getField - to get value of subGenre property of the above item.
db.collection.aggregate([
{
"$project": {
"_id": 0,
"price": 1,
"data": {
"$getField": {
"field": "subGenre",
"input": {
"$first": {
"$filter": {
"input": "$data",
"cond": "$$this.flag"
}
}
}
}
}
}
}
])
Working example
You can use $filter array operator to loop and filter elements by required conditions,
$filter to iterate loop of data array, if flag is true then return element
$let to define a variable and store the above filter result
$first to return the first element from the filtered result, you can also use $arrayElemAt if you are using lower version of the MongoDB
{
$project: {
price: 1,
subGenre: {
$let: {
vars: {
data: {
$filter: {
input: "$data",
cond: "$$this.flag"
}
}
},
in: { $first: "$$data.subGenre" }
}
}
}
}
Playground
Another approach using $indexOfArray and $arrayElemAt operators,
$indexOfArray will find the matching element index of the array
$arrayElemAt to get specific element by specifying the index of the element
{
$project: {
price: 1,
subGenre: {
$arrayElemAt: [
"$data.subGenre",
{ $indexOfArray: ["$data.flag", true] }
]
}
}
}
Playground

Filter by last element in nested array in MongoDb

let's assume I have this object in mongo:
{
"_id": "1",
"Test" {
"Array": [ new NumberInt("10") ]
}
}
Now I would like to use this filter to check if last element in an array satisfy my condition:
{ "Test.Array.-1": { "$gte": new NumberInt("10") } }
But it doesn't work as I expected it to. Is it impossible to filter by last element in an array or do I do something wrong here?
Chain up $gte with $arrayElemAt in a $expr
db.collection.find({
$expr: {
$gte: [
{
$arrayElemAt: [
"$Test.Array",
-1
]
},
10
]
}
})
Mongo Playground

Find in nested array with compare on last field

I have a collection with documents like this one:
{
f1: {
firstArray: [
{
secondArray: [{status: "foo1"}, {status: "foo2"}, {status: "foo3"}]
}
]
}
}
My expected result includes documents that have at least one item in firstArray, which is last object status on the secondArray is included in an input array of values (eg. ["foo3"]).
I don't must use aggregate.
I tried:
{
"f1.firstArray": {
$elemMatch: {
"secondArray.status": {
$in: ["foo3"],
},
otherField: "bar",
},
},
}
You can use an aggregation pipeline with $match and $filter, to keep only documents that their size of matching last items are greater than zero:
db.collection.aggregate([
{$match: {
$expr: {
$gt: [
{$size: {
$filter: {
input: "$f1.firstArray",
cond: {$in: [{$last: "$$this.secondArray.status"}, ["foo3"]]}
}
}
},
0
]
}
}
}
])
See how it works on the playground example
If you know that the secondArray have always 3 items you can do:
db.collection.find({
"f1.firstArray": {
$elemMatch: {
"secondArray.2.status": {
$in: ["foo3"]
}
}
}
})
But otherwise I don't think you can check only the last item without an aggregaation. The idea is that a regular find allows you to write a query that do not use values that are specific for each document. If the size of the array can be different on each document or even on different items on the same document, you need to use an aggregation pipeline

Mongodb comparing using a field with another field in an array of Objects

I have the below structure for my collection:
{
"price":123,
"totalPrices": [
{
"totPrice":123
}
]
}
I am trying to query for all the documents in my collection where price is not equals to totalPrice.totPrice (so above should not be returned).
But it keeps returning the documents which have equal prices as well (such as above sample).
This is the query I'm using:
{
$where : "this.price!== this.totalPrices.totPrice",
totalPrice:{$size:1}
}
What am I doing wrong :(
First, you need to match the size of the array totalPrices is equal to 1. Second, you need to unwind the totalPrices, since it's an array field. Last, you should match the equality of price and totalPrices.totPrice. Try the below code:
db.collection.aggregate([
{
$match: {
$expr: {
$eq: [
{
$size: "$totalPrices"
},
1
]
}
}
},
{
$unwind: "$totalPrices"
},
{
$match: {
$expr: {
$ne: [
"$price",
"$totalPrices.totPrice"
]
}
}
}
])
MongoPlayGroundLink

MongoDB, How Do I combine a find and sort with the $cond in aggregation?

I have written a find query, which works, the find query returns records where name and level exist
db.docs.find( { $and: [{name:{$exists:true}},{level:{ $exists:true}} ] },{_id:0, name:1}).sort({"name":1})
and now want to combine it with something like the code below which also works, but needs to be merged with the above to pull the correct data
db.docs.aggregate(
[
{
$project:
{
_id:0,
name: 1,
Honours:
{
$cond: { if: { $gte: [ "$level", 8 ] }, then: "True", else: "False" }
}
}
}
]
)
The find query returns records where name and level exist, but I need to enhance the result with new column called Honours, showing True of False depending on whether the level is gte (greater than or equal to 8)
So I am basically trying to combine the above find filter with the $cond function (which I found and modified example here : $cond)
I tried the below and a few other permutations to try and make find and sort with the $project and$cond aggregate, but it returned errors. I am just very new to how to construct mongodb syntax to make it all fit together. Can anyone please help?
db.docs.aggregate(
[{{ $and: [{name:{$exists:true}},{level:{ $exists:true}} ] },{_id:0, name:1}).sort({"name":1}
{
$project:
{
_id:0,
name: 1,
Honours:
{
$cond: { if: { $gte: [ "$level", 8 ] }, then: "True", else: "False" }
}
}
}
}
]
)
Try below aggregation pipeline :
db.docs.aggregate([
/** $match is used to filter docs kind of .find(), lessen the dataset size for further stages */
{
$match: {
$and: [{ name: { $exists: true } }, { level: { $exists: true } }]
}
},
/** $project works as projection - w.r.t. this projection it will lessen the each document size for further stages */
{
$project: {
_id: 0,
name: 1,
Honours: {
$cond: { if: { $gte: ["$level", 8] }, then: "True", else: "False" }
}
}
},
/** $sort should work as .sort() */
{ $sort: { name: 1 } }
]);