query for number of array elements containing some field in mongodb - arrays

I have a collection of documents that look like this:
{
my_array : [
{
name : "...",
flag : 1 // optional
},
...
]
}
How can I query for documents that exactly one of their elements contain the flag field?
This will return documents where my_array length is exactly 1:
db.col.find({ "my_array" : { $size : 1 } })
This will return documents where at least one of my_array's object contain the flag field:
db.col.find({ "my_array.flag" : { $exists : true } })
And this will look for documents where flag is an array of size one:
db.col.find({ "my_array.flag" : { $size : 1 } })
I want to somehow combine the first two queries, checking for some inner field existence, and then querying for the size of the filtered array

You can try $redact with $filter for your query.
$filter with $ifNull to keep the matching element followed by $size and $redact and compare result with 1 to keep and else remove the document.
db.col.aggregate(
[{
$match: {
"my_array.flag": {
$exists: true
}
}
}, {
$redact: {
$cond: {
if: {
$eq: [{
$size: {
$filter: {
input: "$my_array",
as: "array",
cond: {
$eq: [{
$ifNull: ["$$array.flag", null]
}, "$$array.flag"]
}
}
}
}, 1]
},
then: "$$KEEP",
else: "$$PRUNE"
}
}
}]
)

Use Aggregation framework in mongodb,
pipeline stage 1: Match ,
checking for some inner field existence,
{ "my_array.flag" : { $exists : true } }
pipleline stage 2: Project,
querying for the size of the filtered array,
{$project:{ "my_array":{$size:"$my_array"}}}
mongoshell Command ,
db.col.aggregate([{
$match: {
"my_array.flag": {
$exists: true
}
}
}, {
$project: {
"my_array": {
$size: "$my_array"
}
}
}])

Following the current answers about using aggregate, here's a working solution using aggregation:
db.col.aggregate([
{ $match : { "my_array.flag" : { $exists : true } } },
{ $unwind : "$my_array" },
{ $match : { "my_array.flag" : { $exists : true } } },
{ $group : { _id : "$_id", count_flags : { $sum : 1 } } },
{ $match : { "count_flags" : 1 } }
])
Here are the steps explained:
Filter to only documents that contain at least one flag field (this will use my index to dramatically reduce the number of documents I need to process)
Unwind the my_array array, to be able to process each element as a separate doc
Filter the separated docs from elements not containing flag
Group again by _id, and count the elements (that contain the flag field)
Filter by the new count_flags field
I don't really like this solution, it seems like a lot of steps to get something that should have basic support in a standard mongo find query.

Related

How to Find and Update Multiple Array Elements in mongodb

> db.Dashboards.find({user:{$regex:"adams"}}).pretty();
{
"_id" : ObjectId("123"),
"user" : "adam.adams",
"widgets" : [
{
"_id" : ObjectId("124"),
"column" : 1,
"configMap" : {
"coleg" : "bas.baser,cer.ceras,tom.tomsa"
},
}
]
}
I have a Mongo database that keeps records like the one above, unfortunately I need to find all users who have "cer.ceras" in the "coleg" field and then replace this with "per.peras"
I try with
db.Dashboards.find({widgets:{"$elemMatch":{configMap:{"$elemMatch":{coleg:/.*ceras.*/}}}}}}).pretty();
But I'm not finding anything for me
This may a bit complex.
Filter:
The criteria should work with $regex to find the occurrence of the text.
Update:
Require the update with aggregation pipeline.
$set - Set widgets field.
1.1. $map - Iterate the element in the widgets array and return a new array.
1.1.1. $mergeObjects - Merge current iterate document with the result of 1.1.1.1.
1.1.1.1. A document with configMap array. With $mergeObjects to merge current iterate configMap document with the result 1.1.1.1.1.
1.1.1.1.1. A document with coleg field. With $replaceAll to replace all matching text "cer.ceras" to "per.peras".
Update Options
{ multi: true } aims to update multiple documents.
db.collection.update({
widgets: {
$elemMatch: {
"configMap.coleg": {
$regex: "cer\\.ceras"
}
}
}
},
[
{
$set: {
widgets: {
$map: {
input: "$widgets",
in: {
$mergeObjects: [
"$$this",
{
configMap: {
$mergeObjects: [
"$$this.configMap",
{
coleg: {
$replaceAll: {
input: "$$this.configMap.coleg",
find: "cer.ceras",
replacement: "per.peras"
}
}
}
]
}
}
]
}
}
}
}
}
],
{
multi: true
})
Demo # Mongo Playground

Removing an element in a mongoDB array based on the position of element with dynamically defined array

My question is a combination of:
This question: Removing the array element in mongoDB based on the position of element
And this question: mongodb set object where key value dynamically changes
I know you can define the field (array) dynamically like this:
{ [arrayName]: { <condition> } }
But, I then want to remove a certain element in a dynamically defined array by specifying the position (which is also defined dynamically). In other words, the function that processes this query is coming in which two parameters: the array's name and the index of the element to remove.
The options given by the selected answer were the following:
Option 1, does not work (in general), adapted to my case this looks like:
{ $pull : { [arrayName] : { $gt: index-1, $lt: index+1 } } }
Option 2, I cannot use dynamically defined values in field selectors with quotation marks (as far as I am aware):
{ $pull : "[arrayName].[index]" }
or
{ $pull : "[arrayName].$": index }
Option 3, is different method but can't use it for the same reason:
{ $unset: { "[arrayName].[index]": 1 } } // Won't work
{ $pull: { [arrayName]: null } } // Would probably work
The only workarounds I can think of right now involve significantly changing the design which would be a shame. Any help is appreciated!
PS: I'm using mongoose as a driver on the latest version as of today (v6.3.5) and MongoDB version 5.0.8
On Mongo version 4.2+ You can use pipelined updates to achieve this, you can get it done in multiple ways, here is what I consider the easiest two ways:
using $slice and $concatArrays to remove a certain element:
db.collection.update({},
[
{
$set: {
[arrayName]: {
$concatArrays: [
{
$slice: [
`$${arrayName}`,
index,
]
},
{
$slice: [
`$${arrayName}`,
index + 1,
{
$size: `$${arrayName}`
}
]
}
]
}
}
}
])
Mongo Playground
using $filter and $zip to filter out based on index:
db.collection.updateOne(
{},
[
{
"$set": {
[arrayName]: {
$map: {
input: {
$filter: {
input: {
$zip: {
inputs: [
{
$range: [
0,
{
$size: `$${arrayName}`
}
]
},
`$${arrayName}`
]
}
},
cond: {
$ne: [
{
"$arrayElemAt": [
"$$this",
0
]
},
index
]
}
}
},
in: {
$arrayElemAt: [
"$$this",
1
]
}
}
}
}
}
])
Alternatively you can just prepare

How to find count in mongodb using $in and using greater than and lesser than?

i have document in Test collection as given below:
{
"_id" : "3233123123213123123",
"sequence":30,
"containIds":["fgx"]
}
{
"_id" : "534545345345345345",
"sequence":70,
"containIds":["abc", "fgx"]
}
{
"_id" : "56756676767676767",
"sequence":160,
"containIds":[]
}
db.getCollection('Test').find({"containIds":{$in: [ "fgx", "containIds" ]}}).count(); // result 1
and
db.getCollection(Test').count({sequence:{ $gt : 0, $lt : 99}})//result 2
but i want to combine both query in one and need to get a result. //result should be 1
do i need to aggregate and project? how to write query for both using $in and ($gt and $lt) in mongodb?
You can try aggregate() method and $facet stage to separate both query and result,
first, you can change the name, put your match criteria and $count to get total records
second, you can change the name, put your match criteria and $count to get total records
db.getCollection('Test').aggregate([
{
$facet: {
first: [
{ $match: { "containIds": { $in: ["fgx", "containIds"] } } },
{ $count: "count" }
],
second: [
{ $match: { sequence: { $gt: 0, $lt: 99 } } },
{ $count: "count" }
]
}
}
])
Playground

mongodb: query in deep arrays

I am using MongoDb to store following JSON. How do I search for all the articles with "2d641b7c-3d74-4cfa-8267-d5a01ed2614b" in pageLayouts array.
{
"magazine": {
"articles": [
{
"articleLayouts": [
{
"pageLayouts": [
"2d641b7c-3d74-4cfa-8267-d5a01ed2614b"
]
}
]
}
]
}
}
In MongoDb documentation, they only specify searching for elements in array that only 1 level deep. For example: Searching in "http://docs.mongodb.org/manual/reference/bios-example-collection/"
db.bios.find(
{
awards: {
$elemMatch: {
award: "Turing Award",
year: { $gt: 1980 }
}
}
}
)
How do I search deeper arrays? as in the articles array in first JSON?
The $elemMatch operator matches documents that contain an array field with at least one element that matches all the specified query criteria.
and query as :
db.collectionName.find({"magazine.articles":{"$elemMatch":{"articleLayouts":{"$elemMatch":{"pageLayouts":{"$in":["2d641b7c-3d74-4cfa-8267-d5a01ed2614b"]}}}}}}).pretty()
You could use aggregation framework for this, in particular the $unwind operator as it deconstructs an array field from the input documents to output a document for each element. Each output document replaces the array with an element value. Thus you can do a $match query further down the pipeline to filter the documents and return the ones that match your criteria.
The following aggregation pipeline will return documents with "2d641b7c-3d74-4cfa-8267-d5a01ed2614b" in pageLayouts array:
var pipeline = [
{
"$match": {
"magazine.articles.articleLayouts.pageLayouts": "2d641b7c-3d74-4cfa-8267-d5a01ed2614b"
}
},
{
"$unwind": "$articles"
},
{
"$unwind": "$articles.articleLayouts"
},
{
"$unwind": "$articles.articleLayouts.pageLayouts"
},
{
"$match": {
"magazine.articles.articleLayouts.pageLayouts": "2d641b7c-3d74-4cfa-8267-d5a01ed2614b"
}
}
];
db.collection.aggregate(pipeline);

Find Query - Filter by array size after $elemMatch

Is it possible to return records based on the size of the array after $elemMatch has filtered it down?
For example, if I have many records in a collection like the following:
[
{
contents: [
{
name: "yorkie",
},
{
name: "dairy milk",
},
{
name: "yorkie",
},
]
},
// ...
]
And I wanted to find all records in which their contents field contained 2 array items with their name field equal to "yorkie", how would I do this? To clarify, the array could contain other items, but the criteria is met so long as 2 of those array items have the matching field:value.
I'm aware I can use $elemMatch (or contents.name) to return records where the array contains at least one item matching that name, and I'm aware I can also use $size to filter based on the exact number of array items in the record's field. Is there a way that they can be both combined?
Not in a find query, but it can be done with an aggregation:
db.test.aggregate([
{ "$match" : { "contents.name" : "yorkie" } },
{ "$unwind" : "$contents" },
{ "$match" : { "contents.name" : "yorkie" } },
{ "$group" : { "_id" : "$_id", "sz" : { "$sum" : 1 } } }, // use $first to include other fields
{ "$match" : { "sz" : { "$gte" : 2 } } }
])
I interpreted
the criteria is met so long as 2 of those array items have the matching field:value
as meaning the criteria is met if at least 2 array items have the matching value in name.
I know this thread is old, but today you can just use find
db.test.find({
"$expr": {
"$gt": [
{
"$reduce": {
"input": "$contents",
"initialValue": 0,
"in": {
"$cond": {
"if": {
"$eq": ["$$this.name", 'yorkie']
},
"then": {
"$add": ["$$value", 1]
},
"else": "$$value"
}
}
}
},
1
]
}
})
The reduce will do the trick here, and will return the number of objects that match the criteria

Resources