MongoDB aggregate $match using dynamic field path - mongodb

I want to match documents in my pipeline based on whether the field to match is contained within an array that is within my documents.
Example document to match:
{
'wishlist': ['123','456','789'],
'productId': '123'
}
Example match aggregation:
{
$match: {
'productId': {$in: '$wishlist'}
}
}
This isn't working - error is '$in needs an array' - but '$wishlist' is an array? so clearly the stage isn't picking up the path reference.
How would I get something like this to work?
Thanks!

If you want to match the internal field of the document, you can use $expr expression operator, and I see that field has an array value then you have to use $in operator's aggregation syntax,
{
$match: {
$expr: {
$in: ["$productId", '$wishlist']
}
}
}

Related

MongoDB $elemMatch comparison to field in same document

I'm wanting to create an aggregation step to match documents where the value of a field in a document exists within an array in the same document.
In a very worked example (note this is very simplified; this will be fitting into a larger existing pipeline), given documents:
{
"_id":{"$oid":"61a9085af9733d0274c41990"},
"myArray":[
{"$oid":"61a9085af9733d0274c41991"},
{"$oid":"61a9085af9733d0274c41992"},
{"$oid":"61a9085af9733d0274c41993"}
],
"myField":{"$oid":"61a9085af9733d0274c41991"} // < In 'myArray' collection
}
and
{
"_id":{"$oid":"61a9085af9733d0274c41990"},
"myArray":[
{"$oid":"61a9085af9733d0274c41991"},
{"$oid":"61a9085af9733d0274c41992"},
{"$oid":"61a9085af9733d0274c41993"}
],
"myField":{"$oid":"61a9085af9733d0274c41994"} // < Not in 'myArray' collection
}
I want to match the first one because the value of myField exists in the collection, but not the second document.
It feels like this should be a really simple $elemMatch operation with an $eq operator, but I can't make it work and every example I've found uses literals. What I've got currently is below, and I've tried with various combinations of quotes and dollar signs round myField.
[{
$match: {
myArray: {
$elemMatch: {
$eq: '$this.myField'
}
}
}
}]
Am I doing something very obviously wrong? Is it not possible to use the value of a field in the same document with an $eq?
Hoping that someone can come along and point out where I'm being stupid :)
Thanks
You can simply do a $in in an aggregation pipeline.
db.collection.aggregate([
{
"$match": {
$expr: {
"$in": [
"$myField",
"$myArray"
]
}
}
}
])
Here is the Mongo playground for your reference.

Cannot use aggregation operations in $set inside updateMany

I have a mongoDB (4.4.8) collection where I want to change the value of some field based on its previous value. For example, I want to convert all strings to uppercase.
For this, I use the following query:
db.collection.updateMany(
{ field: { $regex: "[a-z]+"}},
{ $set: { field: { $toUpper: "$field" } } }
)
when executing the query, it gives me the following error:
MongoError: The dollar ($) prefixed field '$toUpper' in 'field.$toUpper' is not valid for storage
The same occurs if I use similar operations such as $concat (with an array parameter) to append something to the field.
When I look up similar questions, it all uses update and tells me to use updateMany instead, or it says that it only works in mongoDB >= 4.2. However, I have both of these things.
If I am correct, you are able to use aggregation syntax (among which $toUpper) in conjunction with $set inside updateMany queries for these newer versions of mongoDB.
Does anyone know what I'm doing wrong here?
As in the comments of J.F. and turivishal, I managed to solve this by changing it into the following:
db.collection.updateMany(
{ field: { $regex: "[a-z]+"}},
[ { $set: { field: { $toUpper: "$field" } } } ]
)

The $in operator inside $project, $match or find()

I have a find query that uses $in to check whether the specified array is contained within the collection string array:
db.Doc.find({ tags: { '$in': ['tag1','tag2'] } })
I am in the process of refactoring this query to use the aggregation framework, but I can't find the equivalent $in comparison operator at the $project or $match aggregation stages.
Is it possible to use the $in comparison operator at the $project or $match stages of an aggregation query.
To answer your question: yes, but not as you would expect. It is possible to use the $in operator at the $project or $match stages of an aggregation query, but the usage and the purpose aren't quite the same in each.
There are two extremely different types of the "same" $in operator (making a semantic confusion):
Non-aggregational $in: Usually narrows down the results, like a filter. It has no way to add information to the result set, if it doesn't match. Can be used both within find() collection method and inside the aggregational (quite confusing semantic ah?) $match.
Aggregational $in: Usually adds boolean information to the result set, can be used as a logic expression inside $cond, and might also remove some results when is used with $redact. Can be used in $project, $addFields, etc. (but cannot (!) be used within find() or $match). The structure is: { $in: [ <needle expression>, <array haystack expression> ] }, and all of this grey line becomes either true or false (I used PHP's documentation's in_array needle-heystack semantic to better explain). So, { $in [ 'foo', [ 'foo', 'bar', 'baz' ] ] } is true because foo is inside the array.
However, in the previous non-aggregational $in, the { maybeFooField: { $in: [ 'foo', 'bar', 'baz' ] } } structure query simply narrows down the result set, and it doesn't result in a boolean true or false.
Going back to your refactoring, the question is what are your intended results? Why did you switch to the aggregation framework from the beginning?
If you only want to narrow down or filter out the result set, and then use some other aggregation computations, use the simple non-aggregational $in operator.
db.Doc.aggregate([
{ $match: { tags: {$in: ['tag1','tag2'] } } } // non-aggregational $in
])
However, if you want to add information based on the existence or absence of certain tags, use the aggregational $in operator.
db.Doc.aggregate([
{ $project: { hasAnyTag: {$in: [$tags, ['tag1', 'tag2'] ] } } } // aggregational $in
])
Note, you have more aggregational operators to play with arrays, like: $setIntersection and $setIsSubset.
The query: db.Doc.find({ tags: { '$in': ['tag1','tag2'] } }) is equivalent to:
db.Doc.aggregate([
{$match:{tags: {$in: ['tag1','tag2'] }}}
])
And when u use $in at projection like below:
db.Doc.aggregate([
{$project:{tags: {$in: ['tag1','tag2'] }}}
])
Result will be tags:true or tags:false depending upon whether there's match or not.

MongoDB's Aggregation Framework: project only matching element of an array

I have a "class" document as:
{
className: "AAA",
students: [
{name:"An", age:"13"},
{name:"Hao", age:"13"},
{name:"John", age:"14"},
{name:"Hung", age:"12"}
]
}
And i want to get the student who has name is "An", get only matching element in array "students". I can do that with function find() as:
>db.class.find({"students.name":"An"}, {"students.$":true})
{
"_id" : ObjectId("548b01815a06570735b946c1"),
"students" : [
{
"name" : "An",
"age" : "13"
}
]}
It's fine, but when i do the same with Aggregation as following, it get error:
db.class.aggregate([
{$match:{"students.name":'An'}},
{$project:{"students.$":true}}
])
Error is:
uncaught exception: aggregate failed: {
"errmsg" : "exception: FieldPath field names may not start with '$'.",
"code" : 16410,
"ok" : 0
}
Why? I can't use "$" for array in $project operator of aggregate() while can use this one in project operator of find().
From the docs:
Use $ in the projection document of the find() method or the findOne()
method when you only need one particular array element in selected
documents.
The positional operator $ cannot be used in an aggregation pipeline projection stage. It is not recognized there.
This makes sense, because, when you execute a projection along with a find query, the input to the projection part of the query is a single document that has matched the query.The context of the match is known even during projection. So for each document that matches the query, the projection operator is applied then and there before the next match is found.
db.class.find({"students.name":"An"}, {"students.$":true})
In case of:
db.class.aggregate([
{$match:{"students.name":'An'}},
{$project:{"students.$":true}}
])
The aggregation pipeline is a set of stages. Each stage is completely unaware and independent of its previous or next stages. A set of documents pass a stage completely before being passed on to the next stage in the pipeline. The first stage in this case being the $match stage, all the documents are filtered based on the match condition. The input to the projection stage is now a set of documents that have been filtered as part of the match stage.
So a positional operator in the projection stage makes no sense, since in the current stage it doesn't know on what basis the fields had been filtered. Therefore, $ operators are not allowed as part of the field paths.
Why does the below work?
db.class.aggregate([
{ $match: { "students.name": "An" },
{ $unwind: "$students" },
{ $project: { "students": 1 } }
])
As you see, the projection stage gets a set of documents as input, and projects the required fields. It is independent of its previous and next stages.
Try using the unwind operator in the pipeline: http://docs.mongodb.org/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
Your aggregation would look like
db.class.aggregate([
{ $match: { "students.name": "An" },
{ $unwind: "$students" },
{ $project: { "students": 1 } }
])
You can use $filter to selects a subset of an array to return based on the specified condition.
db.class.aggregate([
{
$match:{
"className": "AAA"
}
},
{
$project: {
$filter: {
input: "$students",
as: "stu",
cond: { $eq: [ "$$stu.name", "An" ] }
}
}
])
The following example filters the Students array to only include documents that have a name equal to "An".

How to check order of Array element in Mongodb?

In MongoDB, is there any easy way to check Order of element in Array? For example I have a document like this:
{
_id: 1,
tags: ["mongodb", "rethinkdb", "couchbase", "others"]
}
I would like to check in tags field if mongodb come before rethinkdb or not(lets see in array element, mongodb=0, rethinkdb=1 index, so mongodb come first and our case match.)?
but if there is another document (like below) where rethinkdb comes before mongodb,It case does not match.
{
_id: 2,
tags: ["rethinkdb", "mongodb", "couchbase"]
}
Here mongodb(1) comes after rethinkdb(0) so our case does not match.
Your question is not really as clear as you think it is, and thus why there are several ways to answer it:
If you are looking just to find out if a document has "mongodb" as the first element of the array then you just issue a query like this:
db.collection.find({ "tags.0": "mongodb" })
And that will return only the documents that match the given value at the specified index position using "dot notation".
If you actually expect to match if an array is in an "expected order" then you can get some help from the aggregation pipeline and set operators that are available and other features in MongoDB 2.6:
db.collection.aggregate([
{ "$project": {
"$_id": "$$ROOT",
"matched": { "$setEquals": [
"$tags",
["mongodb", "rethinkdb", "couchbase", "others"]
]}
}},
{ "$match": { "matched": true }}
])
Or if your want is to make sure that the "mongodb" value comes before the "rethinkdb" value, then you will need to evaluate in JavaScript with mapReduce, or something equally not nice like the $where operator:
db.collection.find({
"$where": function() {
return this.tags.indexOf("mongodb") < this.tags.indexOf("rethinkdb");
}
})