1

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

2
  • 1
    Why is "flag" an integer field in the sample document? Perhaps I am missing something here. Commented Mar 13, 2017 at 12:53
  • my_array is an array of documents, each of those inner documents may or may not contain a field flag with some value (which is not important) Commented Mar 13, 2017 at 13:13

3 Answers 3

1

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"
            }
        }
    }]
)
Sign up to request clarification or add additional context in comments.

4 Comments

while this sounds like it may work, it'll require a full table scan, not using my index for "my_array.field", I've added a solution using aggregation that works, but I don't really like both solutions.
I should have mentioned about $redact not using index. I think these are pretty much two solutions you have. $unwind does add a memory overhead by unwinding each array. So depending on your data one can perform better than other. Not sure if this would be any different but you can change the solution to use $project and $match instead of $redact
You can also { $match : { "my_array.flag" : { $exists : true } } } before $redact to filter the array before sending it through $redact. I have updated answer.
ok, i'll accept your solution as it contains less steps then mine. one thing I would have changed in your solution is cond: { $gt : ["$$array.flag", null] } which acts like $exists, and is more readable
0

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"
            }
        }
    }])

1 Comment

the first stage won't filter the array, it'll match documents where at least one array element contains the flag. However, I think this can be done with aggregation, I'll try playing with it
0

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:

  1. 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)
  2. Unwind the my_array array, to be able to process each element as a separate doc
  3. Filter the separated docs from elements not containing flag
  4. Group again by _id, and count the elements (that contain the flag field)
  5. 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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.