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

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

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:

Filter deeply nested array in MongoDB

I have a requirement to filter a mongo collection with deeply nested array data. The document has 3 levels of nesting. Below is the sample document. The requirement is to filter the data with "status" as "verified" and also filter "array1" and "array2" based on condition and only return record which has matching data.
To summarise the filter params,
"status":"verified",
"name": "john",
"city": "mexico"
[
{
"_id": "111",
"array1": [
{
"name": "john",
"array2": [
{
"city": "mexico",
"array3": [
{
"address": "address1",
"status": "verified"
},
{
"address": "address2",
"status": "unverified"
}
]
}
]
}
]
},
{
"_id": "112",
"array1": [
{
"name": "john",
"array2": [
{
"city": "mexico",
"array3": [
{
"address": "address1",
"status": "unverified"
},
{
"address": "address2",
"status": "unverified"
}
]
}
]
}
]
}
]
The expected output is as below,
{
"_id": "111",
"array1": [
{
"name": "john",
"array2": [
{
"city": "mexico",
"array3": [
{
"address": "address1",
"status": "verified"
}
]
}
]
}
]
}
Here's how to do it using nested $filter and $map, as you'll see the syntax is not very clean due to the schema being complex to work with.
Without knowing your product I recommend you revisit it, it might be worth to restructure depending on your common access patterns.
db.collection.aggregate([
{
$match: {
"array1.array2.array3.status": "verified"
}
},
{
$addFields: {
array1: {
$filter: {
input: {
$map: {
input: "$array1",
as: "mapone",
in: {
"$mergeObjects": [
"$$mapone",
{
array2: {
$filter: {
input: {
$map: {
input: "$$mapone.array2",
as: "maptwo",
in: {
"$mergeObjects": [
"$$maptwo",
{
array3: {
$filter: {
input: "$$maptwo.array3",
as: "three",
cond: {
$eq: [
"$$three.status",
"verified"
]
}
}
}
}
]
}
}
},
as: "filtertwo",
cond: {
$and: [
{
$gt: [
{
$size: [
"$$filtertwo.array3"
]
},
0
]
},
{
$eq: [
"$$filtertwo.city",
"mexico"
]
}
]
}
}
}
}
]
}
}
},
as: "filterone",
cond: {
$and: [
{
$gt: [
{
$size: [
"$$filterone.array2"
]
},
0
]
},
{
$eq: [
"$$filterone.name",
"john"
]
}
]
}
}
}
}
}
])
Mongo 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 aggregation: how to find out if an array contains multiple elements

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

MongoDB - Merge Array of Strings With 2-Dimensional Array

I have a MongoDB document which looks like this:
"skills" : [
[
ObjectId("5dbaf9dd95e131fb3e23ef47"),
ObjectId("5dc3cd49796b017513c1bc6d"),
ObjectId("5dc3cd52796b017513c1bc75"),
ObjectId("5dc3cd5b796b017513c1bc82")
],
[
ObjectId("5dbaf9dd95e131fb3e23ef47"),
ObjectId("5dbaf7fc251cee32ce4d3f84")
]
],
"names" : [
"John Davis",
"Dan Malko"
]
(Array of names - string, and Array of arrays of skills)
And I want the result to be:
"skillsWithNames": [
{
"name": "John Davis",
"skills": [
ObjectId("5dbaf9dd95e131fb3e23ef47"),
ObjectId("5dc3cd49796b017513c1bc6d"),
ObjectId("5dc3cd52796b017513c1bc75"),
ObjectId("5dc3cd5b796b017513c1bc82")
]
},
{
"name":"Dan Malko",
"skills":[
ObjectId("5dbaf9dd95e131fb3e23ef47"),
ObjectId("5dbaf7fc251cee32ce4d3f84")
]
}
]
(Array which contains objects, where each object contains a name and the skills).
The indexes are the same for both arrays, so skills[0] belong to the person's name[0].
What is the correct query to do this?
Thanks.
You can use below aggregation
db.collection.aggregate([
{ "$project": {
"skillsWithName": {
"$map": {
"input": "$skills",
"in": {
"name": {
"$arrayElemAt": [
"$names",
{ "$indexOfArray": ["$skills", "$$this"] }
]
},
"skills": "$$this"
}
}
}
}}
])
Output
[
{
"skillsWithName": [
{
"name": "John Davis",
"skills": [
ObjectId("5dbaf9dd95e131fb3e23ef47"),
ObjectId("5dc3cd49796b017513c1bc6d"),
ObjectId("5dc3cd52796b017513c1bc75"),
ObjectId("5dc3cd5b796b017513c1bc82")
]
},
{
"name": "Dan Malko",
"skills": [
ObjectId("5dbaf9dd95e131fb3e23ef47"),
ObjectId("5dbaf7fc251cee32ce4d3f84")
]
}
]
}
]