Is it possible to use positional operator '$' in combination with a query on a deeply-nested document array?
Consider the following nested document defining a 'user':
{
username: 'test',
kingdoms: [
{
buildings: [
{
type: 'castle'
},
{
type: 'treasury'
},
...
]
},
...
]
}
We'd like to return the 'castles' for a particular user e.g. in a form:
{
kingdoms: [{
buildings: [{
type: 'castle'
}]
}]
}
Because you cannot use the $ operator twice (https://jira.mongodb.org/browse/server-831) I know that I can't also query for a particular kingdom, so I'm trying to write a find statement for the nth kingdom.
This seems to make sense when updating a deeply-nested sub-document (Mongodb update deeply nested subdocument) but I'm having less success with the find query.
I can return the first kingdom's buildings with the query:
db.users.findOne(
{ username: 'test' },
{ kingdoms: {$slice: [0, 1]}, 'kingdom.buildings': 1 }
);
But this returns all the buildings of that kingdom.
Following the single-level examples of position operator I'm trying a query like this:
db.users.findOne(
{ username: 'test', 'kingdoms.buildings.type': 'castle' },
{ kingdoms: {$slice: [n, 1]}, 'kingdom.buildings.$': 1 }
);
so as to be in the form:
db.collection.find( { <array.field>: <value> ...}, { "<array>.$": 1 } )
as described in the documentation http://docs.mongodb.org/manual/reference/operator/projection/positional/#proj.S
However this fails with the error:
Positional operator does not match the query specifier
Presumably because kingdoms.buildings isn't considered an array. I've also tried kingdoms.0.buildings
It is confusing because this appears to work for updates (according to Mongodb update deeply nested subdocument)
Have I just got the syntax wrong or is this not supported? If so is there a way to achieve something similar?
You get an error from
db.users.findOne(
{ username: 'test', 'kingdoms.buildings.type': 'castle' },
{ kingdoms: {$slice: [n, 1]}, 'kingdom.buildings.$': 1 }
);
because there is a spelling mistake ("kingdom.buildings.$" should be "kingdoms.buildings.$").
However, this way can not accomplish what you expect.
$ is always aimed at kingdoms in the path of kingdoms.buildings - the first array.
This is a way that should be able to solve the problem.
(V2.6+ required)
db.c.aggregate([ {
$match : {
username : 'test',
'kingdoms.buildings.type' : 'castle'
}
}, {
$project : {
_id : 0,
kingdoms : 1
}
}, {
$redact : {
$cond : {
"if" : {
$or : [ {
$gt : [ "$kingdoms", [] ]
}, {
$gt : [ "$buildings", [] ]
}, {
$eq : [ "$type", "castle" ]
} ]
},
"then" : "$$DESCEND",
"else" : "$$PRUNE"
}
}
} ]).pretty();
To only reserve the first element of kingdoms,
db.c.aggregate([ {
$match : {
username : 'test',
'kingdoms.buildings.type' : 'castle'
}
}, {
$redact : {
$cond : {
"if" : {
$or : [ {
$gt : [ "$kingdoms", [] ]
}, {
$gt : [ "$buildings", [] ]
}, {
$eq : [ "$type", "castle" ]
} ]
},
"then" : "$$DESCEND",
"else" : "$$PRUNE"
}
}
}, {
$unwind : "$kingdoms"
}, {
$group : {
_id : "$_id",
kingdom : {
$first : "$kingdoms"
}
}
}, {
$group : {
_id : "$_id",
kingdoms : {
$push : "$kingdom"
}
}
}, {
$project : {
_id : 0,
kingdoms : 1
}
} ]).pretty();
Related
I am trying to generate a new collection with a field 'desc' having into account a condition in field in a documment array. To do so, I am using $cond statement
The origin collection example is the next one:
{
"_id" : ObjectId("5e8ef9a23e4f255bb41b9b40"),
"Brand" : {
"models" : [
{
"name" : "AA"
},
{
"name" : "BB"
}
]
}
}
{
"_id" : ObjectId("5e8ef9a83e4f255bb41b9b41"),
"Brand" : {
"models" : [
{
"name" : "AG"
},
{
"name" : "AA"
}
]
}
}
The query is the next:
db.runCommand({
aggregate: 'cars',
'pipeline': [
{
'$project': {
'desc': {
'$cond': {
if: {
$in: ['$Brand.models.name',['BB','TC','TS']]
},
then: 'Good',
else: 'Bad'
}
}
}
},
{
'$project': {
'desc': 1
}
},
{
$out: 'cars_stg'
}
],
'allowDiskUse': true,
})
The problem is that the $cond statement is always returning the "else" value. I also have tried $or statement with $eq or the $and with $ne, but is always returning "else".
What am I doing wrong, or how should I fix this?
Thanks
Since $Brand.models.name returns an array, we cannot use $in operator.
Instead, we can use $setIntersection which returns an array that contains the elements that appear in every input array
db.cars.aggregate([
{
"$project": {
"desc": {
"$cond": [
{
$gt: [
{
$size: {
$setIntersection: [
"$Brand.models.name",
[
"BB",
"TC",
"TS"
]
]
}
},
0
]
},
"Good",
"Bad"
]
}
}
},
{
"$project": {
"desc": 1
}
},
{
$out: 'cars_stg'
}
])
MongoPlayground | Alternative $reduce
How can I query a MongoDB collection to find documents with a structure as below? The documents have a field called thing which is a subdocument, and the keys for this field are a form of ID number which will generally not be known by the person writing the query (making dot notation difficult and I assume impossible).
{
"_id" : 3,
"_id2" : 234,
"thing":
{
"2340945683":
{"attribute1": "typeA",
"attribute2": "typeB",
"attribute3": "typeA"
},
"349687346":
{"attribute1": "typeC",
"attribute2": "typeB",
"attribute3": "typeA"
}
},
"username": "user1"
}
Say I want to set a filter which will return the document only if some one or more of the fields within thing have the condition "attribute1" : "typeC"?
I need something like
db.collection.find( {thing.ANY_FIELD: $elemMatch:{"attribute1":"typeC"}})
You need to start with $objectToArray to read your keys dynamically. Then you can $map properties along with $anyElementTrue to detect if there's any nested field in thing containing {"attribute1":"typeC"}:
db.collection.aggregate([
{
$match: {
$expr: {
$anyElementTrue: {
$map: {
input: { $objectToArray: "$thing" },
in: { $eq: [ "$$this.v.attribute1", "typeC" ] }
}
}
}
}
}
])
Mongo Playground
My solution to this was to use two aggregate operations, the first one is called objectToArray and it's purpose is to convert a object into a list of objects with keys and values (see the documentation examples), and the reduce to search in this array of key-values, at the end we end up with a boolean "hasAttribute" indicating that the one field matched the value wee are looking for.
Here is the solution:
db.getCollection("thing").aggregate([
{
$addFields: {
hasAttribute: {
$reduce: {
input: {
$objectToArray: "$thing"
},
initialValue: false,
in: {$or: ["$$value", {$eq: ["typeC", "$$this.v.attribute1"]}]}
}
}
}
},
{
$match: {
hasAttribute: true
}
}
])
Here is the sample output and how the boolean value behaves:
{
"_id" : ObjectId("5ddd63c02e5c579c5076c76f"),
"thing" : {
"349687346" : {
"attribute1" : "typeC",
"attribute2" : "typeB",
"attribute3" : "typeA"
},
"2340945683" : {
"attribute1" : "typeA",
"attribute2" : "typeB",
"attribute3" : "typeA"
}
},
"hasAttribute" : true
}
// ----------------------------------------------
{
"_id" : ObjectId("5ddd63d12e5c579c5076c770"),
"thing" : {
"2340945683" : {
"attribute1" : "typeA",
"attribute2" : "typeB",
"attribute3" : "typeA"
}
},
"hasAttribute" : false
}
// ----------------------------------------------
{
"_id" : ObjectId("5ddd63d12e5c579c5076c771"),
"thing" : {
"349687346" : {
"attribute1" : "typeC",
"attribute2" : "typeB",
"attribute3" : "typeA"
}
},
"hasAttribute" : true
}
Ask for clarifications if you need!
My document structure looks like this:
{
"_id" : ObjectId("5aeeda07f3a664c55e830a08"),
"profileId" : ObjectId("5ad84c8c0e71892058b6a543"),
"list" : [
{
"content" : "answered your post",
"createdBy" : ObjectId("5ad84c8c0e71892058b6a540")
},
{
"content" : "answered your post",
"createdBy" : ObjectId("5ad84c8c0e71892058b6a540")
},
{
"content" : "answered your post",
"createdBy" : ObjectId("5ad84c8c0e71892058b6a540")
},
],
}
I want to count array of
list field. And apply condition before slicing that
if the list<=10 then slice all the elements of list
else 10 elements.
P.S I used this query but is returning null.
db.getCollection('post').aggregate([
{
$match:{
profileId:ObjectId("5ada84c8c0e718s9258b6a543")}
},
{$project:{notifs:{$size:"$list"}}},
{$project:{notifications:
{$cond:[
{$gte:["$notifs",10]},
{$slice:["$list",10]},
{$slice:["$list","$notifs"]}
]}
}}
])
Your first $project stage effectively wipes out all result fields but the one(s) that it explicitly projects (only notifs in your case). That's why the second $project stage cannot $slice the list field anymore (it has been removed by the first $project stage).
Also, I think your $cond/$slice combination can be more elegantly expressed using the $min operator. So there's at least the following two fixes for your problem:
Using $addFields:
db.getCollection('post').aggregate([
{ $match: { profileId: ObjectId("5ad84c8c0e71892058b6a543") } },
{ $addFields: { notifs: { $size: "$list" } } },
{ $project: {
notifications: {
$slice: [ "$list", { $min: [ "$notifs", 10 ] } ]
}
}}
])
Using a calculation inside the $project - this avoids a stage so should be preferable.
db.getCollection('post').aggregate([
{ $match: { profileId: ObjectId("5ad84c8c0e71892058b6a543") } },
{ $project: {
notifications: {
$slice: [ "$list", { $min: [ { $size: "$list" }, 10 ] } ]
}
}}
])
I have this lab test in the mongodb course i am currently taking, the movies collection have a title field and the instruction says:
Using only $project aggregation.
find the movie titles composed of only 1 word like "Cinderella" and "3-25" should count where as "Cast Away" would not.
Use $split String expression and $size Array expression.
Here's a sample document from movies collection:
{
"_id" : ObjectId("573a1390f29313caabcd4192"),
"title" : "The Conjuring of a Woman at the House of Robert Houdin",
"year" : 1896,
"runtime" : 1,
"cast" : [
"Jeanne d'Alcy",
"Georges M�li�s"
]
}
And here's my code:
var pipeline = [
{ $project: {
"title": { $split: ["$title"," "] }
} },
{ $project: {
"_id": 0,
"title_size": {$eq: [{$size: "$title"}, 1]},
"Movie": "$title"
} }
]
db.movies.aggregate(pipeline)
The $eq returns boolean values true and false, not what i expected, then i tried the $literal: 1as the second expression of $eq but i get the same boolean values
What i wanted to achieved is this:
{ "title_size" : 1, "Movie" : [ "Cinderella" ] }
But how?
[
{
$project: {
splitedTitles: {$split: ["$title", " "]}
}
},
{
$match : { splitedTitles : { $size: 1 } }
}
]
As stated in the documentation, this is not possible.
AND Queries With Multiple Expressions Specifying the Same Operator
Consider the following example:
db.inventory.find( {
$and : [
{ $or : [ { price : 0.99 }, { price : 1.99 } ] },
{ $or : [ { sale : true }, { qty : { $lt : 20 } } ] }
]
} )
This query will return all select all documents where:
the price field value equals 0.99 or 1.99, and
the sale field value is equal to true or the qty field value is less than 20.
This query cannot be constructed using an implicit AND operation, because it uses the $or operator more than once.
What is a workaround to query something like this? This query returns no results on MongoDB 3.2. I have tested the $or blocks separately and they are working fine, but not when they are wrapped in $and block. I assumed I didn't read the documentation incorrectly that this is not supposed to work. The only alternative I have is to push the data to ElasticSearch and query it there instead, but that's also just a workaround.
{
"$and": [
{
"$or": [
{
"title": {
"$regex": "^.*html .*$",
"$options": "i"
}
},
{
"keywords": {
"$regex": "^.*html .*$",
"$options": "i"
}
}
]
},
{
"$or": [
{
"public": true
},
{
"domain": "cozybid"
}
]
}
]
}
the documentation doesn't say that this is impossible. It only says
This query cannot be constructed using an implicit AND operation,
because it uses the $or operator more than once.
this means that this will work :
db.inventory.find( {
$and : [
{ $or : [ { price : 0.99 }, { price : 1.99 } ] },
{ $or : [ { sale : true }, { qty : { $lt : 20 } } ] }
]
} )
but this won't, because it's an implicit $and with two $or
db.inventory.find({
{ $or : [ { price : 0.99 }, { price : 1.99 } ] },
{ $or : [ { sale : true }, { qty : { $lt : 20 } } ] }
})
try it online: mongoplayground.net/p/gL_0gKzGA-u
Here is a working case with an implicit $and:
db.inventory.find({ price: { $ne: 1.99, $exists: true } })
I guess the problem you're facing is that there is no document matching your request in your collection