MongoDB aggregation: how to find out if an array contains multiple elements - mongodb

I have following data.
[
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"key": "key1",
"value": "value1"
},
{
"key": "key2",
"value": "value2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
]
}
]
}
]
playground
I want to keep the entry whose nestedArray contains 2 matching elements and remove others. The 2 elements are below
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
So that the result will be below
[
{
"_id": 1,
"array": [
{
"name": "name2",
"nestedArray": [
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
]
}
]
}
]
The one whose name="name1" is removed since it has only one matching element.
Feels like we could use $elemMatch but couldn't figure it out.

First, Unwind the array so that you can easily access the nestedArray .
Second, use $all and $elementMatch on nestedArray
db.collection.aggregate([
{
$unwind: "$array"
},
{
$match: {
"array.nestedArray": {
$all: [
{
"$elemMatch": {
key: "key1",
value: "abc"
}
},
{
"$elemMatch": {
key: "key2",
value: "value2"
}
}
]
}
}
},
{
$group: {
_id: "$_id",
array: {
"$push": "$array"
}
}
}
])
playground

You didn't really specify how you input looks but the strategy will stay the same regardless, We will iterate over the array with $filter and match only documents that their subarray's size is equal to a filtered subarray based on the given input, like so:
// the current input structure.
inputArray = [
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
];
db.collection.aggregate([
{
$project: {
array: {
$filter: {
input: "$array",
as: "element",
cond: {
$and: [
{
$eq: [
{
$size: {
$filter: {
input: "$$element.nestedArray",
as: "nested",
cond: {
"$setIsSubset": [
[
"$$nested"
],
inputArray
]
}
}
},
},
{
$size: "$$element.nestedArray"
}
]
},
{ // this is required for an exact match otherwise subsets are possible, if you wan't to allow it delete this equality completly.
$eq: [
{
$size: "$$element.nestedArray"
},
{
$size: inputArray
}
]
}
]
}
}
}
}
}
])
Again if the input is coming in a different structure you'll have to change the $eq conditions

Related

MongoDB: get documents by last element value in nested array

This question is slightly different from others since I need to get the whole documents and not just specific fields.
I need to filter documents(all of the document, not just specific fields), according to the last elements value of a nested array. (doc.array[i].innerArray[innerArray.length - 1].desiredField)
Documents are looking like this:
[
{
"_id": 0,
"matches": [
{
"name": "match 1",
"ids": [
{
"innerName": "1234"
},
{
"innerName": "3"
}
]
}
]
},
{
"_id": 1,
"matches": [
{
"name": "match 5",
"ids": [
{
"innerName": "123"
},
{
"innerName": "1"
}
]
},
{
"name": "match 5",
"ids": [
{
"innerName": "1"
},
{
"innerName": "1234"
},
]
},
]
}
]
So if we filter according to innerName = '1234', this is the result:
{
"_id": 1,
"matches": [
{
"name": "match 5",
"ids": [
{
"innerName": "123"
},
{
"innerName": "1"
}
]
},
{
"name": "match 5",
"ids": [
{
"innerName": "1"
},
{
"innerName": "1234"
},
]
}
One option is:
db.collection.find({
$expr: {
$in: [
"1234",
{$reduce: {
input: "$matches",
initialValue: [],
in: {$concatArrays: ["$$value", [{$last: "$$this.ids.innerName"}]]}
}
}
]
}
})
See how it works on the playground example
Another option:
db.collection.aggregate([
{
$match: {
$expr: {
$gt: [
{
$size: {
$filter: {
input: "$matches",
cond: {
$in: [
{
$last: "$$this.ids.innerName"
},
[
"1234"
]
]
}
}
}
},
0
]
}
}
}
])
Explained:
Match only documents where size of array is >0 for those who has "1234" in last nested array element.
Playground:

MongoDB - How to find and update elements in a nested array

Here is the collection:
db.employees.insertMany([
{
"data": {
"category": [
{
"name": "HELLO",
"subcategory": [
"EDUCATION",
"ART",
]
},
{
"name": "HELLO",
"subcategory": [
"GG",
"ART",
]
},
{
"name": "HELLO",
"subcategory": [
"EDUCATION",
"SHORE",
]
}
]
}
},
{
"data": {
"category": [
{
"name": "HELLO",
"subcategory": [
"EDUCATION",
"HELLO",
]
}
]
}
},
{
"data": {
"category": [
{
"name": "HELLO",
"subcategory": [
"GG",
"ART",
]
}
]
}
}
]);
What I want is to locate the elements in 'category' with a 'subcategory' that contains 'EDUCATION' and replace 'EDUCATION' with another string, let's say 'SPORTS'.
I tried a couple of commands but nothing really did the job:
db.employees.updateMany({
"data.category.subcategory": "EDUCATION"
},
{
"$set": {
"data.category.$": {
"subcategory": "SPORTS"
}
}
})
What I saw is that it doesn't update the element by replacing it and it doesn't replace every element that meets the criteria.
Think that MongoDB Update with Aggregation Pipeline fulfills your scenario.
$set - Set data.category value.
1.1. $map - Iterate each element in data.category and return an array.
1.1.1. $mergeObjects - Merge the current document with the document with subcategory field from 1.1.1.1.
1.1.1.1 $map - Iterate each value from the subcategory array. With $cond to replace the word EDUCATION with SPORTS if fulfilled, else use existing value ($$this).
db.employees.updateMany({
"data.category.subcategory": "EDUCATION"
},
[
{
"$set": {
"data.category": {
$map: {
input: "$data.category",
in: {
$mergeObjects: [
"$$this",
{
subcategory: {
$map: {
input: "$$this.subcategory",
in: {
$cond: {
if: {
$eq: [
"$$this",
"EDUCATION"
]
},
then: "SPORTS",
else: "$$this"
}
}
}
}
}
]
}
}
}
}
}
]
Sample Mongo Playground
Here's another way to do it using "arrayFilters".
db.collection.update({
"data.category.subcategory": "EDUCATION"
},
{
"$set": {
"data.category.$[].subcategory.$[elem]": "SPORTS"
}
},
{
"arrayFilters": [
{ "elem": "EDUCATION" }
],
"multi": true
})
Try it on mongoplayground.net.

MongoDB - How to filter document objects within array?

I have a problem with filtering fields(objects) from my document using find function.
Here is my db.collection.find(..) document:
{
"_id": BinData(3, "Uz+QwtoVMt7hjpqMrLxVhQ=="),
"name": "jeorge",
"permissions": [
{
"key": "group.staff",
"value": true,
"context": [
{ "key": "server", "value": "test" }
]
},
{
"key": "group.tester",
"value": true,
"context": [
{ "key": "server", "value": "test" }
]
},
{
"key": "test.test",
"value": true
},
{
"key": "group.default",
"value": true
},
{
"key": "group.helper",
"value": true
}
]
}
How can I filter my document in two ways?
a) Display only fields with nested context object.
b) Display only fields, which hasn't got nested context objects.
Also I need to check if parent's object property key contains string "group" as value (If not - skip it).
For situation b, I have tried this function, but it prints result with only the first matched element (based on mongodb's documentation).
db.collection.find(
{
"_id": BinData(3, "Uz+QwtoVMt7hjpqMrLxVhQ==")
},
{
"permissions": {
$elemMatch: {
"key": {
$regex: "group",
},
"value": true
}
}
}
);
Is it possible by single query? Thanks!
db.collection.aggregate([
{
$match: { _id: BinData(3, "Uz+QwtoVMt7hjpqMrLxVhQ==") }
},
{
$set: {
permissions: {
$filter: {
input: "$permissions",
as: "p",
cond: {
$and: [
"$$p.context",
{ $regexMatch: { input: "$$p.key", regex: "group" } }
]
}
}
}
}
}
])
mongoplayground
db.collection.aggregate([
{
$match: {
_id: BinData(3, "Uz+QwtoVMt7hjpqMrLxVhQ==")
}
},
{
$set: {
permissions: {
$filter: {
input: "$permissions",
as: "p",
cond: {
$and: [
{
$not: [ "$$p.context" ]
},
{
$regexMatch: {
input: "$$p.key",
regex: "group"
}
}
]
}
}
}
}
}
])
mongoplayground

Querying an array of key value pairs using $and with nested $and logical operator

Given the following MongoDB collection:
[
{
"_id": 1,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Apples"
]
}
]
},
{
"_id": 2,
"basket": [
{
"key": "A",
"value": [
"Oranges"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
},
{
"_id": 3,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
},
{
"_id": 4,
"basket": [
{
"key": "A",
"value": [
"Oranges"
]
},
{
"key": "B",
"value": [
"Apples"
]
}
]
}
]
I want to query this collection to get all documents where "Bananas" appears on basket 'A' and 'B', meaning that the expected result would be:
[
{
"_id": 3,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
}
]
This is the actual outcome:
[
{
"_id": 1,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Apples"
]
}
]
},
{
"_id": 2,
"basket": [
{
"key": "A",
"value": [
"Oranges"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
},
{
"_id": 3,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
}
]
So, for some reason that I don't quite get why, I'm getting all the documents where "Bananas" appear in any basket when I should be getting only the document with the _id: 3
This is the query I'm using:
{
$and: [
{
$and: [
{ "basket.value": { $in: ["Bananas"] } },
{ "basket.key": { $eq: "A" } }
]
},
{
$and: [
{ "basket.value": { $in: ["Bananas"] } },
{ "basket.key": { $eq: "B" } }
]
}
]
}
Try running a query that uses the $expr operator as it allows you to leverage aggregation pipeline operators which are best suited for the above query. Essentially you need to filter the basket array on the above condition i.e. where the value "Bananas" is in the values array AND where the key value is either 'A' OR 'B'. This condition as expressed in the aggregation pipeline is as follows:
{
$and: [
{ $in: ['Bananas', '$$basket_element.value'] },
{ $in: ['$$basket_element.key', ['A', 'B'] ] }
]
}
The length of this filtered array should be equal to 2 for the overall query to be satisfied thus you would need the operators
$filter - returns the basket array filtered on the above condition
$size - returns the length of the filtered array
$eq - is the conditional operator used by $expr query to compare the size of the filtered array, which should have exactly 2 elements.
Overall, your query should be as follows:
db.collection.find({
$expr: {
$eq: [
{ $size: {
$filter: {
input: '$basket',
as: 'basket_element',
cond: {
$and: [
{ $in: ['Bananas', '$$basket_element.value'] },
{ $in: ['$$basket_element.key', ['A', 'B'] ] }
]
}
}
} },
2
]
}
})
Mongo Playground

mongo $filter and multiple condition in sub-sub array

I have this portion of collections:
[{
"_id": ObjectId("604f3ae3194f2135b0ade569"),
"parameters": [
{
"_id": ObjectId("602b7455f4b4bf5b41662ec1"),
"name": "Purpose",
"options": [
{
"id": ObjectId("602b764ff4b4bf5b41662ec2"),
"name": "debug",
"sel": true
},
{
"id": ObjectId("602b767df4b4bf5b41662ec3"),
"name": "performance",
"sel": false
},
{
"id": ObjectId("602b764ff4b4bf5b41662ec4"),
"name": "security",
"sel": true
},
{
"id": ObjectId("602b767df4b4bf5b41662ec5"),
"name": "Not Applicable",
"sel": false
}
],
"type": "multiple"
}]
}]
And this query:
db.testCollection.aggregate([
{ $unwind: "$parameters" },
{
$match: {
"parameters._id": ObjectId("602b7455f4b4bf5b41662ec1"),
}
},
{
$addFields: {
match: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $eq: ["$$option.id", ObjectId('602b764ff4b4bf5b41662ec2')] },
{ $eq: ["$$option.sel", true] }
]
}
}
}
}
}
])
The result is a new array match with, in this case,
"match": [
{
"id": ObjectId("602b764ff4b4bf5b41662ec2"),
"name": "debug",
"sel": true
}
]
How I can match, for collections, match for example
"id": ObjectId("602b764ff4b4bf5b41662ec2") and
"id": ObjectId("602b764ff4b4bf5b41662ec4")
and have a result array with or two results or empty?
I am not sure it is possible in single condition inside $filter, but you can try with $let,
$let to declare a variable called filtered and store your filtered result
$in to check condition if filtered element size is 2 then return filtered result otherwise blank array
You have to put the number that you are matching number of ids in filter in $eq condition
{
$addFields: {
match: {
$let: {
vars: {
filtered: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{
$in: [
"$$option.id",
[ObjectId("602b764ff4b4bf5b41662ec2"), ObjectId("602b767df4b4bf5b41662ec3")]
]
},
{ $eq: ["$$option.sel", true] }
]
}
}
}
},
in: {
$cond: [
{ $eq: [{ $size: "$$filtered" }, 2] },
"$$filtered",
[]
]
}
}
}
}
}
Playground