MongoDB: get documents by last element value in nested array - mongodb

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:

Related

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

How to compare a field to a field of a array in mongodb

I have a documents like below. I want to retrieve all documents whose address.city == "newyork" and address.id == active.
[
{
"name": "star1",
"active": 1,
"address": [
{
"id": 1,
"city": "newyork"
},
{
"id": 2,
"city": "sydney"
}
]
},
{
"name": "star2",
"active": 2,
"address": [
{
"id": 1,
"city": "newyork"
},
{
"id": 2,
"city": "london"
}
]
}
]
I have written below query and it Partially works, But It is not returning complete document. I can't use unwind. Do we have any solution without using "unwind". Is it possible to solve a problem only with $match
db.collection.aggregate([
{
$unwind: "$address"
},
{
$match: {
$expr: {
$eq: [
"$active",
"$address.id"
]
},
"address.city": "newyork"
}
}
])
Maybe something like this:
db.collection.aggregate([
{
"$addFields": {
"address": {
"$filter": {
"input": "$address",
"as": "a",
"cond": {
$and: [
{
$eq: [
"$$a.id",
"$active"
]
},
{
$eq: [
"$$a.city",
"newyork"
]
}
]
}
}
}
}
},
{
$match: {
address: {
$ne: []
}
}
}
])
Explained:
Use addFields/filter to match only matching documents in the array.
Remove the documents with empty address from the array for the cases where no subdocuments is found.
Playground
In case you need to match the whole document containing at least one entry having {address.id==active and address.city==newyork } here is an option:
db.collection.aggregate([
{
$match: {
$expr: {
"$in": [
{
id: "$active",
city: "newyork"
},
"$address"
]
}
}
}
])
Explained:
Match only documents having at least one object in address array with id==$active and city=="newyork"
Playground
In case we expect different order inside the address objects , the more correct option is as follow:
db.collection.aggregate([
{
$match: {
$expr: {
$or: [
{
"$in": [
{
id: "$active",
city: "newyork"
},
"$address"
]
},
{
"$in": [
{
city: "newyork",
id: "$active"
},
"$address"
]
}
]
}
}
}
])
Explained:
Match only documents having at least one object in array with { id==$active and city=="newyork" } or { city=="newyork" and id==$active }
Playground 2

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

Use $Redact with a regular expression

In an aggregation pipeline, I am trying to filter some elements of an array of objects, based on the value of a field in this object.
Let's say that I have this entry:
{
"_id": "5b8911d346d19645f8a66bf4",
"title": "test task",
"creation_date": "2018-08-31T10:00:51.598Z",
"logs": [
{
"_id": "5b89126c46d19645f8a66bfb",
"content": "Running"
},
{
"_id": "5b89128646d19645f8a66bfd",
"content": "Stopping"
},
{
"_id": "5b89128646d19645f8a66bfd",
"content": "Stopped"
}
]
}
My objectif is to filter only the logs containing the stop word in their content:
{
"_id": "5b8911d346d19645f8a66bf4",
"title": "test task",
"creation_date": "2018-08-31T10:00:51.598Z",
"logs": [
{
"_id": "5b89128646d19645f8a66bfd",
"content": "Stopping"
},
{
"_id": "5b89128646d19645f8a66bfd",
"content": "Stopped"
}
]
}
I tried to use $redact to eliminate all the logs that does not contain the stop word:
$redact: {
$cond: {
if: { $match: { "logs.content": { $regex: "stop", $options: 'i' }}},
then: "$$KEEP",
else: "$$PRUNE"
}
}
but I keep getting the error:
Unrecognized expression '$match'
You can try below aggregation
db.collection.aggregate([
{ "$addFields": {
"logs": {
"$filter": {
"input": "$logs",
"cond": {
"$ne": [
{ "$indexOfBytes": [
{ "$toUpper": "$$this.content" },
{ "$toUpper": "stop" }
]},
-1
]
}
}
}
}}
])
Output
[
{
"_id": "5b8911d346d19645f8a66bf4",
"creation_date": "2018-08-31T10:00:51.598Z",
"logs": [
{
"_id": "5b89128646d19645f8a66bfd",
"content": "Stopping"
},
{
"_id": "5b89128646d19645f8a66bfd",
"content": "Stopped"
}
],
"title": "test task"
}
]
As per your requirement below query is working and it is properly tested
db.users.aggregate(
// Pipeline
[
// Stage 1
{
$unwind: {
path : "$logs",
preserveNullAndEmptyArrays : true // optional
}
},
// Stage 2
{
$group: {
_id: "$_id",
"title" :{$last:"$title"} ,
"creation_date" :{$last:"$creation_date"},
logs: {
$push: {
$cond: [ {$or:[{"$eq":[{ "$substr": [ "$logs.content", 0, 4 ] }, "Stop"]},{"$eq":[{ "$substr": [ "$logs.content", 0, 4 ] }, "stop"]}]},{"_id":"$logs._id","content":"$logs.content"},null]
}
}
}
},
// Stage 3
{
$project: {
logs: {
$filter: {
input: "$logs",
as: "log",
cond: { $ne: [ "$$log", null ] }
}
}
}
},
]
// Created with Studio 3T, the IDE for MongoDB - https://studio3t.com/
);