I would like to query with a specified list of array elements such that documents returned can only contain the elements I pass, but need not contain all of them.

Given documents like:

{
  name: "Article 1",
  tags: ["Funny", "Rad"]
}

{
  name: "Article 2",
  tags: ["Cool", "Rad"]
}

{
  name: "Article 3",
  tags: ["Rad"]
}

Here are some example arrays and their respective results.

  • ["Rad"] should return Article 3
  • ["Rad", "Cool"] should return Article 2 and Article 3
  • ["Funny", "Cool"] should return nothing, since there are no articles with only one of those tags or both

I'm sure I can pull this off with $where but I'd like to avoid that for obvious reasons.

Solution 1

You can do this by combining multiple operators:

db.test.find({tags: {$not: {$elemMatch: {$nin: ['Rad', 'Cool']}}}})

The $elemMatch with the $nin is finding the docs where a single tags element is neither 'Rad' nor 'Cool', and then the parent $not inverts the match to return all the docs where that didn't match any elements.

However, this will also return docs where tags is either missing or has no elements. To exclude those you need to add a qualifier that ensures tags has at least one element:

db.test.find({
    tags: {$not: {$elemMatch: {$nin: ['Rad', 'Cool']}}},
    'tags.0': {$exists: true}
})

Solution 2

The accepted answer works, but isn't optimised. Since this is the top result on Google, here's a better solution.

I went all the way back to version 2.2 in the docs, which is the oldest version available, and all of them state:

If the field holds an array, then the $in operator selects the documents whose field holds an array that contains at least one element that matches a value in the specified array (e.g. <value1>, <value2>, etc.)

Source

So you can just do

db.test.find({tags: {$in: ['Rad', 'Cool']}})

which will return any entries where the tags contain either 'Rad', 'Cool', or both and use an index if available.